From e990b34957cc60b0d81a1f3bdfc13f067228a9c0 Mon Sep 17 00:00:00 2001 From: Norbert Preining Date: Fri, 20 Aug 2021 03:43:52 +0100 Subject: [PATCH 1/1] Import ktorrent_21.08.0.orig.tar.xz [dgit import orig ktorrent_21.08.0.orig.tar.xz] --- .git-blame-ignore-revs | 4 + .gitignore | 7 + CMakeLists.txt | 240 + COPYING | 346 + ChangeLog | 975 ++ LICENSES/GPL-2.0-only.txt | 311 + LICENSES/GPL-2.0-or-later.txt | 311 + LICENSES/GPL-3.0-only.txt | 604 + LICENSES/LGPL-2.0-only.txt | 444 + LICENSES/LicenseRef-KDE-Accepted-GPL.txt | 12 + LICENSES/Qt-Commercial-exception-1.0.txt | 4 + Messages.sh | 5 + RoadMap | 12 + ...rg.freedesktop.PowerManagement.Inhibit.xml | 20 + dbus_xml/org.freedesktop.PowerManagement.xml | 33 + dbus_xml/org.freedesktop.ScreenSaver.xml | 41 + doc/CMakeLists.txt | 3 + doc/index.docbook | 71 + ktmagnetdownloader/CMakeLists.txt | 9 + ktmagnetdownloader/magnetdownloader.cpp | 46 + ktmagnetdownloader/magnettest.cpp | 131 + ktmagnetdownloader/magnettest.h | 38 + ktorrent/CMakeLists.txt | 131 + ktorrent/core.cpp | 1262 ++ ktorrent/core.h | 270 + ktorrent/dialogs/addpeersdlg.cpp | 66 + ktorrent/dialogs/addpeersdlg.h | 43 + ktorrent/dialogs/addpeersdlg.ui | 126 + ktorrent/dialogs/fileselectdlg.cpp | 670 + ktorrent/dialogs/fileselectdlg.h | 102 + ktorrent/dialogs/fileselectdlg.ui | 400 + ktorrent/dialogs/importdialog.cpp | 373 + ktorrent/dialogs/importdialog.h | 64 + ktorrent/dialogs/importdialog.ui | 133 + ktorrent/dialogs/missingfilesdlg.cpp | 154 + ktorrent/dialogs/missingfilesdlg.h | 63 + ktorrent/dialogs/missingfilesdlg.ui | 92 + ktorrent/dialogs/pastedialog.cpp | 140 + ktorrent/dialogs/pastedialog.h | 48 + ktorrent/dialogs/pastedlgbase.ui | 103 + ktorrent/dialogs/speedlimitsdlg.cpp | 153 + ktorrent/dialogs/speedlimitsdlg.h | 46 + ktorrent/dialogs/speedlimitsdlg.ui | 135 + ktorrent/dialogs/speedlimitsmodel.cpp | 233 + ktorrent/dialogs/speedlimitsmodel.h | 68 + ktorrent/dialogs/spinboxdelegate.cpp | 68 + ktorrent/dialogs/spinboxdelegate.h | 33 + ktorrent/dialogs/torrentcreatordlg.cpp | 404 + ktorrent/dialogs/torrentcreatordlg.h | 80 + ktorrent/dialogs/torrentcreatordlg.ui | 426 + ktorrent/groups/groupfiltermodel.cpp | 52 + ktorrent/groups/groupfiltermodel.h | 48 + ktorrent/groups/grouppolicydlg.cpp | 61 + ktorrent/groups/grouppolicydlg.h | 34 + ktorrent/groups/grouppolicydlg.ui | 229 + ktorrent/groups/groupswitcher.cpp | 244 + ktorrent/groups/groupswitcher.h | 120 + ktorrent/groups/groupview.cpp | 194 + ktorrent/groups/groupview.h | 87 + ktorrent/groups/groupviewmodel.cpp | 412 + ktorrent/groups/groupviewmodel.h | 93 + ktorrent/gui.cpp | 525 + ktorrent/gui.h | 141 + ktorrent/icons/128-apps-ktorrent.png | Bin 0 -> 6045 bytes .../icons/16-actions-kt-change-tracker.png | Bin 0 -> 676 bytes ktorrent/icons/16-actions-kt-check-data.png | Bin 0 -> 616 bytes ktorrent/icons/16-actions-kt-chunks.png | Bin 0 -> 235 bytes ktorrent/icons/16-actions-kt-encrypted.png | Bin 0 -> 676 bytes ktorrent/icons/16-actions-kt-info-widget.png | Bin 0 -> 560 bytes ktorrent/icons/16-actions-kt-magnet.png | Bin 0 -> 585 bytes ktorrent/icons/16-actions-kt-pause.png | Bin 0 -> 489 bytes .../icons/16-actions-kt-queue-manager.png | Bin 0 -> 411 bytes ktorrent/icons/16-actions-kt-remove.png | Bin 0 -> 425 bytes .../icons/16-actions-kt-restore-defaults.png | Bin 0 -> 795 bytes .../16-actions-kt-set-max-download-speed.png | Bin 0 -> 875 bytes .../16-actions-kt-set-max-upload-speed.png | Bin 0 -> 841 bytes ktorrent/icons/16-actions-kt-show-hide.png | Bin 0 -> 400 bytes .../icons/16-actions-kt-show-statusbar.png | Bin 0 -> 484 bytes ktorrent/icons/16-actions-kt-speed-limits.png | Bin 0 -> 769 bytes ktorrent/icons/16-actions-kt-start-all.png | Bin 0 -> 543 bytes ktorrent/icons/16-actions-kt-start.png | Bin 0 -> 506 bytes ktorrent/icons/16-actions-kt-stop-all.png | Bin 0 -> 546 bytes ktorrent/icons/16-actions-kt-stop.png | Bin 0 -> 512 bytes ktorrent/icons/16-actions-kt-upnp.png | Bin 0 -> 638 bytes ktorrent/icons/16-apps-ktorrent.png | Bin 0 -> 498 bytes ktorrent/icons/22-actions-kt-magnet.png | Bin 0 -> 940 bytes ktorrent/icons/22-actions-kt-pause.png | Bin 0 -> 749 bytes ktorrent/icons/22-actions-kt-remove.png | Bin 0 -> 620 bytes .../22-actions-kt-set-max-download-speed.png | Bin 0 -> 1358 bytes .../22-actions-kt-set-max-upload-speed.png | Bin 0 -> 1283 bytes ktorrent/icons/22-actions-kt-speed-limits.png | Bin 0 -> 1143 bytes ktorrent/icons/22-actions-kt-start-all.png | Bin 0 -> 857 bytes ktorrent/icons/22-actions-kt-start.png | Bin 0 -> 764 bytes ktorrent/icons/22-actions-kt-stop-all.png | Bin 0 -> 871 bytes ktorrent/icons/22-actions-kt-stop.png | Bin 0 -> 753 bytes ktorrent/icons/22-apps-ktorrent.png | Bin 0 -> 737 bytes ktorrent/icons/256-apps-ktorrent.png | Bin 0 -> 12848 bytes ktorrent/icons/32-actions-kt-info-widget.png | Bin 0 -> 1516 bytes ktorrent/icons/32-actions-kt-magnet.png | Bin 0 -> 1528 bytes ktorrent/icons/32-actions-kt-pause.png | Bin 0 -> 1170 bytes .../icons/32-actions-kt-queue-manager.png | Bin 0 -> 885 bytes ktorrent/icons/32-actions-kt-remove.png | Bin 0 -> 966 bytes .../32-actions-kt-set-max-download-speed.png | Bin 0 -> 2320 bytes .../32-actions-kt-set-max-upload-speed.png | Bin 0 -> 2150 bytes ktorrent/icons/32-actions-kt-speed-limits.png | Bin 0 -> 1938 bytes ktorrent/icons/32-actions-kt-start-all.png | Bin 0 -> 1375 bytes ktorrent/icons/32-actions-kt-start.png | Bin 0 -> 1218 bytes ktorrent/icons/32-actions-kt-stop-all.png | Bin 0 -> 1376 bytes ktorrent/icons/32-actions-kt-stop.png | Bin 0 -> 1184 bytes ktorrent/icons/32-actions-kt-upnp.png | Bin 0 -> 1528 bytes ktorrent/icons/32-apps-ktorrent.png | Bin 0 -> 1126 bytes .../48-actions-kt-bandwidth-scheduler.png | Bin 0 -> 2640 bytes .../icons/48-actions-kt-change-tracker.png | Bin 0 -> 2939 bytes ktorrent/icons/48-actions-kt-check-data.png | Bin 0 -> 2528 bytes ktorrent/icons/48-actions-kt-chunks.png | Bin 0 -> 377 bytes ktorrent/icons/48-actions-kt-info-widget.png | Bin 0 -> 2438 bytes ktorrent/icons/48-actions-kt-magnet.png | Bin 0 -> 2572 bytes ktorrent/icons/48-actions-kt-pause.png | Bin 0 -> 1865 bytes ktorrent/icons/48-actions-kt-plugins.png | Bin 0 -> 1781 bytes .../icons/48-actions-kt-queue-manager.png | Bin 0 -> 1160 bytes ktorrent/icons/48-actions-kt-remove.png | Bin 0 -> 1592 bytes .../icons/48-actions-kt-restore-defaults.png | Bin 0 -> 3315 bytes .../48-actions-kt-set-max-download-speed.png | Bin 0 -> 4362 bytes .../48-actions-kt-set-max-upload-speed.png | Bin 0 -> 3787 bytes ktorrent/icons/48-actions-kt-show-hide.png | Bin 0 -> 1559 bytes .../icons/48-actions-kt-show-statusbar.png | Bin 0 -> 2014 bytes ktorrent/icons/48-actions-kt-speed-limits.png | Bin 0 -> 3485 bytes ktorrent/icons/48-actions-kt-start-all.png | Bin 0 -> 1834 bytes ktorrent/icons/48-actions-kt-start.png | Bin 0 -> 1932 bytes ktorrent/icons/48-actions-kt-stop-all.png | Bin 0 -> 1869 bytes ktorrent/icons/48-actions-kt-stop.png | Bin 0 -> 1891 bytes ktorrent/icons/48-actions-kt-upnp.png | Bin 0 -> 2408 bytes ktorrent/icons/48-apps-ktorrent.png | Bin 0 -> 1693 bytes ktorrent/icons/64-actions-kt-magnet.png | Bin 0 -> 4429 bytes ktorrent/icons/64-apps-ktorrent.png | Bin 0 -> 2689 bytes ktorrent/icons/CMakeLists.txt | 62 + ktorrent/icons/sc-actions-kt-magnet.svgz | Bin 0 -> 12976 bytes .../sc-actions-kt-set-max-download-speed.svgz | Bin 0 -> 10612 bytes .../sc-actions-kt-set-max-upload-speed.svgz | Bin 0 -> 9869 bytes .../icons/sc-actions-kt-speed-limits.svgz | Bin 0 -> 6823 bytes ktorrent/ipfilterlist.cpp | 208 + ktorrent/ipfilterlist.h | 62 + ktorrent/ipfilterwidget.cpp | 194 + ktorrent/ipfilterwidget.h | 49 + ktorrent/ipfilterwidget.ui | 116 + ktorrent/ktorrent.notifyrc | 754 + ktorrent/ktorrentui.rc | 33 + ktorrent/kttorrentactivityui.rc | 97 + ktorrent/main.cpp | 245 + ktorrent/org.kde.ktorrent.appdata.xml | 814 + ktorrent/org.kde.ktorrent.desktop | 162 + ktorrent/pref/advancedpref.cpp | 47 + ktorrent/pref/advancedpref.h | 32 + ktorrent/pref/advancedpref.ui | 331 + ktorrent/pref/btpref.cpp | 30 + ktorrent/pref/btpref.h | 25 + ktorrent/pref/btpref.ui | 224 + ktorrent/pref/colorpref.cpp | 60 + ktorrent/pref/colorpref.h | 26 + ktorrent/pref/colorpref.ui | 211 + ktorrent/pref/generalpref.cpp | 69 + ktorrent/pref/generalpref.h | 26 + ktorrent/pref/generalpref.ui | 261 + ktorrent/pref/networkpref.cpp | 102 + ktorrent/pref/networkpref.h | 38 + ktorrent/pref/networkpref.ui | 337 + ktorrent/pref/prefdialog.cpp | 161 + ktorrent/pref/prefdialog.h | 89 + ktorrent/pref/proxypref.cpp | 61 + ktorrent/pref/proxypref.h | 35 + ktorrent/pref/proxypref.ui | 348 + ktorrent/pref/qmpref.cpp | 41 + ktorrent/pref/qmpref.h | 32 + ktorrent/pref/qmpref.ui | 356 + ktorrent/pref/recommendedsettingsdlg.cpp | 213 + ktorrent/pref/recommendedsettingsdlg.h | 51 + ktorrent/pref/recommendedsettingsdlg.ui | 349 + ktorrent/statusbar.cpp | 113 + ktorrent/statusbar.h | 67 + ktorrent/statusbarofflineindicator.cpp | 65 + ktorrent/statusbarofflineindicator.h | 28 + ktorrent/tools/magnetmodel.cpp | 169 + ktorrent/tools/magnetmodel.h | 62 + ktorrent/tools/magnetview.cpp | 142 + ktorrent/tools/magnetview.h | 56 + ktorrent/tools/queuemanagermodel.cpp | 507 + ktorrent/tools/queuemanagermodel.h | 129 + ktorrent/tools/queuemanagerwidget.cpp | 304 + ktorrent/tools/queuemanagerwidget.h | 87 + ktorrent/torrentactivity.cpp | 260 + ktorrent/torrentactivity.h | 104 + ktorrent/trayicon.cpp | 423 + ktorrent/trayicon.h | 176 + ktorrent/view/propertiesdlg.cpp | 67 + ktorrent/view/propertiesdlg.h | 41 + ktorrent/view/propertiesdlg.ui | 94 + ktorrent/view/scanextender.cpp | 132 + ktorrent/view/scanextender.h | 43 + ktorrent/view/scanextender.ui | 238 + ktorrent/view/torrentsearchbar.cpp | 88 + ktorrent/view/torrentsearchbar.h | 49 + ktorrent/view/view.cpp | 868 + ktorrent/view/view.h | 225 + ktorrent/view/viewdelegate.cpp | 342 + ktorrent/view/viewdelegate.h | 120 + ktorrent/view/viewjobtracker.cpp | 50 + ktorrent/view/viewjobtracker.h | 35 + ktorrent/view/viewmodel.cpp | 758 + ktorrent/view/viewmodel.h | 203 + ktorrent/view/viewselectionmodel.cpp | 22 + ktorrent/view/viewselectionmodel.h | 32 + ktupnptest/CMakeLists.txt | 15 + ktupnptest/main.cpp | 56 + ktupnptest/upnptestwidget.cpp | 88 + ktupnptest/upnptestwidget.h | 38 + ktupnptest/upnptestwidget.ui | 149 + libktcore/CMakeLists.txt | 77 + libktcore/config-ktcore.h.cmake | 24 + libktcore/dbus/dbus.cpp | 284 + libktcore/dbus/dbus.h | 154 + libktcore/dbus/dbusgroup.cpp | 168 + libktcore/dbus/dbusgroup.h | 53 + libktcore/dbus/dbussettings.cpp | 791 + libktcore/dbus/dbussettings.h | 186 + libktcore/dbus/dbustorrent.cpp | 487 + libktcore/dbus/dbustorrent.h | 138 + libktcore/dbus/dbustorrentfile.cpp | 76 + libktcore/dbus/dbustorrentfile.h | 44 + libktcore/dbus/dbustorrentfilestream.cpp | 78 + libktcore/dbus/dbustorrentfilestream.h | 62 + libktcore/groups/allgroup.cpp | 26 + libktcore/groups/allgroup.h | 27 + libktcore/groups/functiongroup.cpp | 11 + libktcore/groups/functiongroup.h | 43 + libktcore/groups/group.cpp | 83 + libktcore/groups/group.h | 198 + libktcore/groups/groupmanager.cpp | 328 + libktcore/groups/groupmanager.h | 157 + libktcore/groups/torrentgroup.cpp | 191 + libktcore/groups/torrentgroup.h | 53 + libktcore/groups/ungroupedgroup.cpp | 34 + libktcore/groups/ungroupedgroup.h | 33 + libktcore/gui/centralwidget.cpp | 110 + libktcore/gui/centralwidget.h | 64 + libktcore/gui/extender.cpp | 19 + libktcore/gui/extender.h | 51 + libktcore/gui/tabbarwidget.cpp | 228 + libktcore/gui/tabbarwidget.h | 86 + libktcore/interfaces/activity.cpp | 73 + libktcore/interfaces/activity.h | 100 + libktcore/interfaces/coreinterface.cpp | 17 + libktcore/interfaces/coreinterface.h | 271 + libktcore/interfaces/functions.cpp | 155 + libktcore/interfaces/functions.h | 30 + libktcore/interfaces/guiinterface.cpp | 18 + libktcore/interfaces/guiinterface.h | 115 + libktcore/interfaces/plugin.cpp | 29 + libktcore/interfaces/plugin.h | 140 + libktcore/interfaces/prefpageinterface.cpp | 33 + libktcore/interfaces/prefpageinterface.h | 75 + .../interfaces/torrentactivityinterface.cpp | 34 + .../interfaces/torrentactivityinterface.h | 84 + libktcore/ktorrent.kcfg | 354 + libktcore/ktversion.h | 19 + libktcore/plugin/pluginactivity.cpp | 54 + libktcore/plugin/pluginactivity.h | 42 + libktcore/plugin/pluginmanager.cpp | 185 + libktcore/plugin/pluginmanager.h | 83 + libktcore/settings.kcfgc | 8 + libktcore/torrent/basicjobprogresswidget.cpp | 79 + libktcore/torrent/basicjobprogresswidget.h | 41 + libktcore/torrent/basicjobprogresswidget.ui | 67 + libktcore/torrent/chunkbar.cpp | 134 + libktcore/torrent/chunkbar.h | 54 + libktcore/torrent/chunkbarrenderer.cpp | 134 + libktcore/torrent/chunkbarrenderer.h | 35 + libktcore/torrent/jobprogresswidget.cpp | 27 + libktcore/torrent/jobprogresswidget.h | 71 + libktcore/torrent/jobtracker.cpp | 141 + libktcore/torrent/jobtracker.h | 59 + libktcore/torrent/magnetmanager.cpp | 537 + libktcore/torrent/magnetmanager.h | 174 + libktcore/torrent/queuemanager.cpp | 868 + libktcore/torrent/queuemanager.h | 307 + libktcore/torrent/torrentfilelistmodel.cpp | 276 + libktcore/torrent/torrentfilelistmodel.h | 47 + libktcore/torrent/torrentfilemodel.cpp | 77 + libktcore/torrent/torrentfilemodel.h | 145 + libktcore/torrent/torrentfiletreemodel.cpp | 705 + libktcore/torrent/torrentfiletreemodel.h | 96 + libktcore/util/indexofcompare.h | 26 + libktcore/util/itemselectionmodel.cpp | 87 + libktcore/util/itemselectionmodel.h | 45 + libktcore/util/mmapfile.cpp | 266 + libktcore/util/mmapfile.h | 119 + libktcore/util/stringcompletionmodel.cpp | 69 + libktcore/util/stringcompletionmodel.h | 45 + libktcore/util/treefiltermodel.cpp | 41 + libktcore/util/treefiltermodel.h | 29 + logo.png | Bin 0 -> 2949 bytes plugins/CMakeLists.txt | 47 + plugins/bwscheduler/CMakeLists.txt | 38 + plugins/bwscheduler/bwprefpage.cpp | 38 + plugins/bwscheduler/bwprefpage.h | 35 + plugins/bwscheduler/bwprefpage.ui | 233 + plugins/bwscheduler/bwschedulerplugin.cpp | 229 + plugins/bwscheduler/bwschedulerplugin.h | 61 + .../bwschedulerpluginsettings.kcfgc | 7 + plugins/bwscheduler/edititemdlg.cpp | 153 + plugins/bwscheduler/edititemdlg.h | 51 + plugins/bwscheduler/edititemdlg.ui | 331 + plugins/bwscheduler/guidanceline.cpp | 49 + plugins/bwscheduler/guidanceline.h | 42 + plugins/bwscheduler/ktbwschedulerplugin.kcfg | 33 + .../bwscheduler/ktorrent_bwscheduler.desktop | 106 + plugins/bwscheduler/ktorrent_bwschedulerui.rc | 16 + plugins/bwscheduler/schedule.cpp | 393 + plugins/bwscheduler/schedule.h | 189 + plugins/bwscheduler/scheduleeditor.cpp | 210 + plugins/bwscheduler/scheduleeditor.h | 96 + plugins/bwscheduler/schedulegraphicsitem.cpp | 317 + plugins/bwscheduler/schedulegraphicsitem.h | 58 + plugins/bwscheduler/weekdaymodel.cpp | 75 + plugins/bwscheduler/weekdaymodel.h | 39 + plugins/bwscheduler/weekscene.cpp | 297 + plugins/bwscheduler/weekscene.h | 142 + plugins/bwscheduler/weekview.cpp | 135 + plugins/bwscheduler/weekview.h | 107 + plugins/downloadorder/CMakeLists.txt | 24 + plugins/downloadorder/downloadorderdialog.cpp | 186 + plugins/downloadorder/downloadorderdialog.h | 51 + .../downloadorder/downloadordermanager.cpp | 162 + plugins/downloadorder/downloadordermanager.h | 86 + plugins/downloadorder/downloadordermodel.cpp | 366 + plugins/downloadorder/downloadordermodel.h | 74 + plugins/downloadorder/downloadorderplugin.cpp | 122 + plugins/downloadorder/downloadorderplugin.h | 57 + plugins/downloadorder/downloadorderwidget.ui | 152 + .../ktorrent_downloadorder.desktop | 111 + .../downloadorder/ktorrent_downloadorderui.rc | 7 + plugins/infowidget/CMakeLists.txt | 69 + plugins/infowidget/addtrackersdialog.cpp | 58 + plugins/infowidget/addtrackersdialog.h | 34 + plugins/infowidget/availabilitychunkbar.cpp | 51 + plugins/infowidget/availabilitychunkbar.h | 33 + plugins/infowidget/chunkdownloadmodel.cpp | 258 + plugins/infowidget/chunkdownloadmodel.h | 74 + plugins/infowidget/chunkdownloadview.cpp | 117 + plugins/infowidget/chunkdownloadview.h | 57 + plugins/infowidget/chunkdownloadview.ui | 273 + plugins/infowidget/downloadedchunkbar.cpp | 96 + plugins/infowidget/downloadedchunkbar.h | 39 + plugins/infowidget/fileview.cpp | 611 + plugins/infowidget/fileview.h | 110 + plugins/infowidget/flagdb.cpp | 101 + plugins/infowidget/flagdb.h | 55 + plugins/infowidget/geoipmanager.cpp | 59 + plugins/infowidget/geoipmanager.h | 51 + plugins/infowidget/infowidgetplugin.cpp | 288 + plugins/infowidget/infowidgetplugin.h | 75 + .../infowidget/infowidgetpluginsettings.kcfgc | 7 + plugins/infowidget/iwfilelistmodel.cpp | 275 + plugins/infowidget/iwfilelistmodel.h | 49 + plugins/infowidget/iwfiletreemodel.cpp | 330 + plugins/infowidget/iwfiletreemodel.h | 51 + plugins/infowidget/iwprefpage.cpp | 20 + plugins/infowidget/iwprefpage.h | 23 + plugins/infowidget/iwprefpage.ui | 124 + plugins/infowidget/ktinfowidgetplugin.kcfg | 31 + .../infowidget/ktorrent_infowidget.desktop | 112 + plugins/infowidget/monitor.cpp | 88 + plugins/infowidget/monitor.h | 47 + plugins/infowidget/peerview.cpp | 122 + plugins/infowidget/peerview.h | 59 + plugins/infowidget/peerviewmodel.cpp | 410 + plugins/infowidget/peerviewmodel.h | 86 + plugins/infowidget/statustab.cpp | 304 + plugins/infowidget/statustab.h | 42 + plugins/infowidget/statustab.ui | 426 + plugins/infowidget/trackermodel.cpp | 293 + plugins/infowidget/trackermodel.h | 77 + plugins/infowidget/trackerview.cpp | 270 + plugins/infowidget/trackerview.h | 59 + plugins/infowidget/trackerview.ui | 76 + plugins/infowidget/webseedsmodel.cpp | 176 + plugins/infowidget/webseedsmodel.h | 59 + plugins/infowidget/webseedstab.cpp | 169 + plugins/infowidget/webseedstab.h | 64 + plugins/infowidget/webseedstab.ui | 85 + plugins/ipfilter/CMakeLists.txt | 34 + plugins/ipfilter/convertdialog.cpp | 108 + plugins/ipfilter/convertdialog.h | 59 + plugins/ipfilter/convertdialog.ui | 106 + plugins/ipfilter/convertthread.cpp | 159 + plugins/ipfilter/convertthread.h | 58 + plugins/ipfilter/downloadandconvertjob.cpp | 291 + plugins/ipfilter/downloadandconvertjob.h | 74 + plugins/ipfilter/ipblockingprefpage.cpp | 206 + plugins/ipfilter/ipblockingprefpage.h | 61 + plugins/ipfilter/ipblockingprefpage.ui | 278 + plugins/ipfilter/ipblocklist.cpp | 110 + plugins/ipfilter/ipblocklist.h | 64 + plugins/ipfilter/ipfilterplugin.cpp | 142 + plugins/ipfilter/ipfilterplugin.h | 63 + plugins/ipfilter/ipfilterpluginsettings.kcfgc | 7 + plugins/ipfilter/ktipfilterplugin.kcfg | 26 + plugins/ipfilter/ktorrent_ipfilter.desktop | 111 + plugins/ipfilter/tests/CMakeLists.txt | 2 + plugins/ipfilter/tests/ipblocklisttest.cpp | 49 + plugins/logviewer/CMakeLists.txt | 25 + plugins/logviewer/ktlogviewerplugin.kcfg | 26 + plugins/logviewer/ktorrent_logviewer.desktop | 113 + plugins/logviewer/logflags.cpp | 228 + plugins/logviewer/logflags.h | 67 + plugins/logviewer/logflagsdelegate.cpp | 89 + plugins/logviewer/logflagsdelegate.h | 32 + plugins/logviewer/logprefpage.cpp | 65 + plugins/logviewer/logprefpage.h | 35 + plugins/logviewer/logprefwidget.ui | 132 + plugins/logviewer/logviewer.cpp | 127 + plugins/logviewer/logviewer.h | 52 + plugins/logviewer/logviewerplugin.cpp | 137 + plugins/logviewer/logviewerplugin.h | 58 + .../logviewer/logviewerpluginsettings.kcfgc | 7 + plugins/magnetgenerator/CMakeLists.txt | 25 + .../ktmagnetgeneratorplugin.kcfg | 34 + .../ktorrent_magnetgenerator.desktop | 100 + .../ktorrent_magnetgeneratorui.rc | 7 + .../magnetgenerator/magnetgeneratorplugin.cpp | 126 + .../magnetgenerator/magnetgeneratorplugin.h | 44 + .../magnetgeneratorpluginsettings.kcfgc | 7 + .../magnetgeneratorprefwidget.cpp | 44 + .../magnetgeneratorprefwidget.h | 28 + .../magnetgeneratorprefwidget.ui | 117 + plugins/mediaplayer/CMakeLists.txt | 49 + plugins/mediaplayer/ktmediaplayerplugin.kcfg | 16 + .../mediaplayer/ktorrent_mediaplayer.desktop | 115 + plugins/mediaplayer/ktorrent_mediaplayerui.rc | 12 + plugins/mediaplayer/mediacontroller.cpp | 109 + plugins/mediaplayer/mediacontroller.h | 42 + plugins/mediaplayer/mediacontroller.ui | 156 + plugins/mediaplayer/mediafile.cpp | 227 + plugins/mediaplayer/mediafile.h | 158 + plugins/mediaplayer/mediafilestream.cpp | 122 + plugins/mediaplayer/mediafilestream.h | 59 + plugins/mediaplayer/mediamodel.cpp | 229 + plugins/mediaplayer/mediamodel.h | 77 + plugins/mediaplayer/mediaplayer.cpp | 217 + plugins/mediaplayer/mediaplayer.h | 128 + plugins/mediaplayer/mediaplayeractivity.cpp | 374 + plugins/mediaplayer/mediaplayeractivity.h | 90 + plugins/mediaplayer/mediaplayerplugin.cpp | 61 + plugins/mediaplayer/mediaplayerplugin.h | 38 + .../mediaplayerpluginsettings.kcfgc | 7 + plugins/mediaplayer/mediaview.cpp | 145 + plugins/mediaplayer/mediaview.h | 78 + plugins/mediaplayer/playlist.cpp | 319 + plugins/mediaplayer/playlist.h | 65 + plugins/mediaplayer/playlistwidget.cpp | 246 + plugins/mediaplayer/playlistwidget.h | 97 + plugins/mediaplayer/videochunkbar.cpp | 109 + plugins/mediaplayer/videochunkbar.h | 48 + plugins/mediaplayer/videowidget.cpp | 261 + plugins/mediaplayer/videowidget.h | 81 + plugins/scanfolder/CMakeLists.txt | 25 + .../scanfolder/ktorrent_scanfolder.desktop | 109 + plugins/scanfolder/ktscanfolderplugin.kcfg | 39 + plugins/scanfolder/scanfolder.cpp | 78 + plugins/scanfolder/scanfolder.h | 48 + plugins/scanfolder/scanfolderplugin.cpp | 97 + plugins/scanfolder/scanfolderplugin.h | 46 + .../scanfolder/scanfolderpluginsettings.kcfgc | 7 + plugins/scanfolder/scanfolderprefpage.cpp | 128 + plugins/scanfolder/scanfolderprefpage.h | 45 + plugins/scanfolder/scanfolderprefpage.ui | 162 + plugins/scanfolder/scanthread.cpp | 183 + plugins/scanfolder/scanthread.h | 83 + plugins/scanfolder/torrentloadqueue.cpp | 153 + plugins/scanfolder/torrentloadqueue.h | 94 + plugins/scanforlostfiles/CMakeLists.txt | 25 + plugins/scanforlostfiles/fsproxymodel.h | 90 + .../ktorrent_scanforlostfiles.desktop | 55 + .../ktscanforlostfilesplugin.kcfg | 16 + plugins/scanforlostfiles/nodeoperations.cpp | 199 + plugins/scanforlostfiles/nodeoperations.h | 137 + .../scanforlostfilesplugin.cpp | 125 + .../scanforlostfiles/scanforlostfilesplugin.h | 57 + .../scanforlostfilespluginsettings.kcfgc | 7 + .../scanforlostfilesprefpage.cpp | 55 + .../scanforlostfilesprefpage.h | 41 + .../scanforlostfilesprefpage.ui | 80 + .../scanforlostfilesthread.cpp | 80 + .../scanforlostfiles/scanforlostfilesthread.h | 51 + .../scanforlostfileswidget.cpp | 201 + .../scanforlostfiles/scanforlostfileswidget.h | 54 + .../scanforlostfileswidget.ui | 159 + plugins/scripting/CMakeLists.txt | 35 + plugins/scripting/api/scriptablegroup.cpp | 37 + plugins/scripting/api/scriptablegroup.h | 34 + plugins/scripting/api/scriptingmodule.cpp | 139 + plugins/scripting/api/scriptingmodule.h | 67 + plugins/scripting/ktorrent_scripting.desktop | 108 + plugins/scripting/ktorrent_scriptingui.rc | 27 + plugins/scripting/script.cpp | 158 + plugins/scripting/script.h | 132 + plugins/scripting/scriptdelegate.cpp | 178 + plugins/scripting/scriptdelegate.h | 44 + plugins/scripting/scriptingplugin.cpp | 222 + plugins/scripting/scriptingplugin.h | 51 + plugins/scripting/scriptmanager.cpp | 245 + plugins/scripting/scriptmanager.h | 79 + plugins/scripting/scriptmodel.cpp | 287 + plugins/scripting/scriptmodel.h | 85 + plugins/scripting/scriptproperties.ui | 131 + plugins/scripting/scripts/CMakeLists.txt | 4 + .../scripts/auto_remove/CMakeLists.txt | 3 + .../scripts/auto_remove/auto_remove.desktop | 108 + .../scripts/auto_remove/auto_remove.py | 74 + .../scripts/auto_remove/auto_remove.ui | 41 + .../scripts/auto_resume/CMakeLists.txt | 3 + .../scripts/auto_resume/auto_resume.desktop | 104 + .../scripts/auto_resume/auto_resume.py | 83 + .../scripts/auto_resume/auto_resume.ui | 123 + .../email_notifications/CMakeLists.txt | 4 + .../email_notifications.desktop | 110 + .../email_notifications.py | 169 + .../email_notifications/emailconfig.ui | 163 + plugins/scripting/scripts/test.py | 24 + .../scripts/tracker_groups/CMakeLists.txt | 2 + .../tracker_groups/tracker_groups.desktop | 102 + .../scripts/tracker_groups/tracker_groups.py | 101 + plugins/search/CMakeLists.txt | 43 + plugins/search/home/CMakeLists.txt | 18 + plugins/search/home/body-background.jpg | Bin 0 -> 299494 bytes plugins/search/home/box-bottom-left.png | Bin 0 -> 421 bytes plugins/search/home/box-bottom-middle.png | Bin 0 -> 468 bytes plugins/search/home/box-bottom-right.png | Bin 0 -> 430 bytes plugins/search/home/box-center.png | Bin 0 -> 119101 bytes plugins/search/home/box-middle-left.png | Bin 0 -> 492 bytes plugins/search/home/box-middle-right.png | Bin 0 -> 519 bytes plugins/search/home/box-top-left.png | Bin 0 -> 417 bytes plugins/search/home/box-top-middle.png | Bin 0 -> 367 bytes plugins/search/home/box-top-right.png | Bin 0 -> 451 bytes plugins/search/home/home.css | 20 + plugins/search/home/home.html | 97 + plugins/search/home/ktorrent-icon.png | Bin 0 -> 15035 bytes plugins/search/home/ktorrent_infopage.css | 257 + plugins/search/ktorrent_search.desktop | 114 + plugins/search/ktorrent_searchui.rc | 10 + plugins/search/ktsearchplugin.kcfg | 36 + plugins/search/magneturlschemehandler.cpp | 21 + plugins/search/magneturlschemehandler.h | 23 + plugins/search/opensearch/CMakeLists.txt | 8 + plugins/search/opensearch/btdb.in/favicon.ico | Bin 0 -> 15086 bytes .../search/opensearch/btdb.in/opensearch.xml | 19 + .../search/opensearch/btdig.com/favicon.ico | Bin 0 -> 4286 bytes .../opensearch/btdig.com/opensearch.xml | 9 + .../opensearch/duckduckgo.com/favicon.ico | Bin 0 -> 32988 bytes .../opensearch/duckduckgo.com/opensearch.xml | 9 + .../opensearch/torrentproject.se/favicon.ico | Bin 0 -> 1150 bytes .../torrentproject.se/opensearch.xml | 10 + plugins/search/opensearchdownloadjob.cpp | 138 + plugins/search/opensearchdownloadjob.h | 63 + plugins/search/proxy_helper.cpp | 37 + plugins/search/proxy_helper.h | 33 + plugins/search/searchactivity.cpp | 304 + plugins/search/searchactivity.h | 72 + plugins/search/searchengine.cpp | 184 + plugins/search/searchengine.h | 92 + plugins/search/searchenginelist.cpp | 340 + plugins/search/searchenginelist.h | 97 + plugins/search/searchplugin.cpp | 121 + plugins/search/searchplugin.h | 62 + plugins/search/searchpluginsettings.kcfgc | 7 + plugins/search/searchpref.ui | 227 + plugins/search/searchprefpage.cpp | 188 + plugins/search/searchprefpage.h | 59 + plugins/search/searchtoolbar.cpp | 182 + plugins/search/searchtoolbar.h | 63 + plugins/search/searchwidget.cpp | 232 + plugins/search/searchwidget.h | 81 + plugins/search/webview.cpp | 159 + plugins/search/webview.h | 109 + plugins/shutdown/CMakeLists.txt | 37 + plugins/shutdown/ktorrent_shutdown.desktop | 114 + plugins/shutdown/ktorrent_shutdownui.rc | 16 + plugins/shutdown/shutdowndlg.cpp | 135 + plugins/shutdown/shutdowndlg.h | 42 + plugins/shutdown/shutdowndlg.ui | 84 + plugins/shutdown/shutdownplugin.cpp | 153 + plugins/shutdown/shutdownplugin.h | 47 + plugins/shutdown/shutdownruleset.cpp | 339 + plugins/shutdown/shutdownruleset.h | 144 + plugins/shutdown/shutdowntorrentmodel.cpp | 247 + plugins/shutdown/shutdowntorrentmodel.h | 81 + plugins/stats/CMakeLists.txt | 38 + plugins/stats/Conns.ui | 44 + plugins/stats/ConnsTabPage.cc | 264 + plugins/stats/ConnsTabPage.h | 86 + plugins/stats/DartTestfile.txt | 11 + plugins/stats/DisplaySettings.ui | 986 ++ plugins/stats/DisplaySettingsPage.cc | 25 + plugins/stats/DisplaySettingsPage.h | 28 + plugins/stats/PluginPage.cc | 20 + plugins/stats/PluginPage.h | 59 + plugins/stats/Settings.ui | 339 + plugins/stats/SettingsPage.cc | 34 + plugins/stats/SettingsPage.h | 53 + plugins/stats/Spd.ui | 56 + plugins/stats/SpdTabPage.cc | 215 + plugins/stats/SpdTabPage.h | 94 + plugins/stats/StatsPlugin.cc | 93 + plugins/stats/StatsPlugin.h | 72 + plugins/stats/drawer/ChartDrawer.cc | 51 + plugins/stats/drawer/ChartDrawer.h | 198 + plugins/stats/drawer/ChartDrawerData.cc | 83 + plugins/stats/drawer/ChartDrawerData.h | 159 + plugins/stats/drawer/KPlotWgtDrawer.cc | 418 + plugins/stats/drawer/KPlotWgtDrawer.h | 110 + plugins/stats/drawer/PlainChartDrawer.cc | 521 + plugins/stats/drawer/PlainChartDrawer.h | 141 + plugins/stats/ktorrent_stats.desktop | 113 + plugins/stats/ktstatsplugin.kcfg | 180 + plugins/stats/statspluginsettings.kcfgc | 7 + plugins/syndication/CMakeLists.txt | 43 + plugins/syndication/feedlist.cpp | 259 + plugins/syndication/feedlist.h | 54 + plugins/syndication/feedlistdelegate.cpp | 50 + plugins/syndication/feedlistdelegate.h | 29 + plugins/syndication/feedlistview.cpp | 48 + plugins/syndication/feedlistview.h | 43 + plugins/syndication/feedretriever.cpp | 79 + plugins/syndication/feedretriever.h | 49 + plugins/syndication/feedwidget.cpp | 227 + plugins/syndication/feedwidget.h | 67 + plugins/syndication/feedwidget.ui | 201 + plugins/syndication/feedwidgetmodel.cpp | 180 + plugins/syndication/feedwidgetmodel.h | 53 + plugins/syndication/filter.cpp | 431 + plugins/syndication/filter.h | 355 + plugins/syndication/filtereditor.cpp | 255 + plugins/syndication/filtereditor.h | 67 + plugins/syndication/filtereditor.ui | 521 + plugins/syndication/filterlist.cpp | 91 + plugins/syndication/filterlist.h | 30 + plugins/syndication/filterlistmodel.cpp | 121 + plugins/syndication/filterlistmodel.h | 44 + plugins/syndication/filterlistview.cpp | 44 + plugins/syndication/filterlistview.h | 43 + .../icons/16-actions-kt-add-feeds.png | Bin 0 -> 670 bytes .../icons/16-actions-kt-add-filters.png | Bin 0 -> 836 bytes .../icons/16-actions-kt-remove-feeds.png | Bin 0 -> 629 bytes .../icons/16-actions-kt-remove-filters.png | Bin 0 -> 756 bytes .../icons/22-actions-kt-add-feeds.png | Bin 0 -> 1023 bytes .../icons/22-actions-kt-add-filters.png | Bin 0 -> 1281 bytes .../icons/22-actions-kt-remove-feeds.png | Bin 0 -> 936 bytes .../icons/22-actions-kt-remove-filters.png | Bin 0 -> 1114 bytes .../icons/32-actions-kt-add-feeds.png | Bin 0 -> 1654 bytes .../icons/32-actions-kt-add-filters.png | Bin 0 -> 2001 bytes .../icons/32-actions-kt-remove-feeds.png | Bin 0 -> 1513 bytes .../icons/32-actions-kt-remove-filters.png | Bin 0 -> 1704 bytes plugins/syndication/icons/CMakeLists.txt | 6 + .../icons/hisc-action-kt-add-feeds.svgz | Bin 0 -> 6745 bytes .../icons/hisc-action-kt-add-filters.svgz | Bin 0 -> 6971 bytes .../icons/hisc-action-kt-remove-feeds.svgz | Bin 0 -> 5501 bytes .../icons/hisc-action-kt-remove-filters.svgz | Bin 0 -> 5602 bytes plugins/syndication/ktfeed.cpp | 478 + plugins/syndication/ktfeed.h | 209 + .../syndication/ktorrent_syndication.desktop | 108 + plugins/syndication/ktorrent_syndicationui.rc | 10 + plugins/syndication/linkdownloader.cpp | 215 + plugins/syndication/linkdownloader.h | 60 + plugins/syndication/managefiltersdlg.cpp | 174 + plugins/syndication/managefiltersdlg.h | 50 + plugins/syndication/managefiltersdlg.ui | 144 + plugins/syndication/syndicationactivity.cpp | 269 + plugins/syndication/syndicationactivity.h | 61 + plugins/syndication/syndicationplugin.cpp | 100 + plugins/syndication/syndicationplugin.h | 49 + plugins/syndication/syndicationtab.cpp | 105 + plugins/syndication/syndicationtab.h | 62 + plugins/upnp/CMakeLists.txt | 24 + plugins/upnp/ktorrent_upnp.desktop | 111 + plugins/upnp/ktupnpplugin.kcfg | 13 + plugins/upnp/routermodel.cpp | 184 + plugins/upnp/routermodel.h | 65 + plugins/upnp/upnpplugin.cpp | 76 + plugins/upnp/upnpplugin.h | 42 + plugins/upnp/upnppluginsettings.kcfgc | 7 + plugins/upnp/upnpwidget.cpp | 153 + plugins/upnp/upnpwidget.h | 62 + plugins/upnp/upnpwidget.ui | 65 + plugins/zeroconf/CMakeLists.txt | 21 + plugins/zeroconf/ktorrent_zeroconf.desktop | 110 + plugins/zeroconf/torrentservice.cpp | 111 + plugins/zeroconf/torrentservice.h | 57 + plugins/zeroconf/zeroconfplugin.cpp | 106 + plugins/zeroconf/zeroconfplugin.h | 58 + po/ar/ktorrent.po | 12669 +++++++++++++++ po/ast/ktorrent.po | 8602 ++++++++++ po/be/ktorrent.po | 9343 +++++++++++ po/bg/ktorrent.po | 10220 ++++++++++++ po/bs/ktorrent.po | 9364 +++++++++++ po/ca/docs/ktorrent/index.docbook | 156 + po/ca/ktorrent.po | 9297 +++++++++++ po/ca@valencia/ktorrent.po | 9298 +++++++++++ po/cs/ktorrent.po | 8966 ++++++++++ po/da/ktorrent.po | 11532 +++++++++++++ po/de/docs/ktorrent/index.docbook | 170 + po/de/ktorrent.po | 13483 ++++++++++++++++ po/el/ktorrent.po | 12257 ++++++++++++++ po/en_GB/ktorrent.po | 12608 +++++++++++++++ po/eo/ktorrent.po | 12214 ++++++++++++++ po/es/docs/ktorrent/index.docbook | 184 + po/es/ktorrent.po | 9296 +++++++++++ po/et/docs/ktorrent/index.docbook | 171 + po/et/ktorrent.po | 11427 +++++++++++++ po/eu/ktorrent.po | 10761 ++++++++++++ po/fi/ktorrent.po | 9506 +++++++++++ po/fr/docs/ktorrent/index.docbook | 170 + po/fr/ktorrent.po | 12919 +++++++++++++++ po/ga/ktorrent.po | 10740 ++++++++++++ po/gl/ktorrent.po | 9554 +++++++++++ po/he/ktorrent.po | 8656 ++++++++++ po/hi/ktorrent.po | 9419 +++++++++++ po/hr/ktorrent.po | 8622 ++++++++++ po/hu/ktorrent.po | 11627 +++++++++++++ po/ia/ktorrent.po | 8604 ++++++++++ po/is/ktorrent.po | 10678 ++++++++++++ po/it/docs/ktorrent/index.docbook | 184 + po/it/ktorrent.po | 9405 +++++++++++ po/ja/ktorrent.po | 9222 +++++++++++ po/kk/ktorrent.po | 9424 +++++++++++ po/km/ktorrent.po | 9470 +++++++++++ po/ko/ktorrent.po | 9194 +++++++++++ po/lt/ktorrent.po | 9205 +++++++++++ po/lv/ktorrent.po | 12052 ++++++++++++++ po/mr/ktorrent.po | 8616 ++++++++++ po/nb/ktorrent.po | 8987 ++++++++++ po/nds/ktorrent.po | 13196 +++++++++++++++ po/nl/docs/ktorrent/index.docbook | 156 + po/nl/ktorrent.po | 11996 ++++++++++++++ po/nn/ktorrent.po | 8834 ++++++++++ po/pl/ktorrent.po | 11580 +++++++++++++ po/pt/docs/ktorrent/index.docbook | 170 + po/pt/ktorrent.po | 9288 +++++++++++ po/pt_BR/docs/ktorrent/index.docbook | 170 + po/pt_BR/ktorrent.po | 9268 +++++++++++ po/ro/ktorrent.po | 12406 ++++++++++++++ po/ru/docs/ktorrent/index.docbook | 189 + po/ru/ktorrent.po | 9348 +++++++++++ po/se/ktorrent.po | 8583 ++++++++++ po/sk/ktorrent.po | 9245 +++++++++++ po/sl/ktorrent.po | 9316 +++++++++++ po/sq/ktorrent.po | 10636 ++++++++++++ po/sr/ktorrent.po | 8990 +++++++++++ po/sv/docs/ktorrent/index.docbook | 170 + po/sv/ktorrent.po | 11528 +++++++++++++ po/tr/ktorrent.po | 9302 +++++++++++ po/ug/ktorrent.po | 8684 ++++++++++ po/uk/docs/ktorrent/index.docbook | 170 + po/uk/ktorrent.po | 9294 +++++++++++ po/zh_CN/docs/ktorrent/index.docbook | 162 + po/zh_CN/ktorrent.po | 8827 ++++++++++ po/zh_TW/ktorrent.po | 9019 +++++++++++ scripts/GetExtragearSite.sh | 9 + scripts/kcfg_qobject_gen.py | 225 + 767 files changed, 625778 insertions(+) create mode 100644 .git-blame-ignore-revs create mode 100644 .gitignore create mode 100644 CMakeLists.txt create mode 100644 COPYING create mode 100644 ChangeLog create mode 100644 LICENSES/GPL-2.0-only.txt create mode 100644 LICENSES/GPL-2.0-or-later.txt create mode 100644 LICENSES/GPL-3.0-only.txt create mode 100644 LICENSES/LGPL-2.0-only.txt create mode 100644 LICENSES/LicenseRef-KDE-Accepted-GPL.txt create mode 100644 LICENSES/Qt-Commercial-exception-1.0.txt create mode 100755 Messages.sh create mode 100644 RoadMap create mode 100644 dbus_xml/org.freedesktop.PowerManagement.Inhibit.xml create mode 100644 dbus_xml/org.freedesktop.PowerManagement.xml create mode 100644 dbus_xml/org.freedesktop.ScreenSaver.xml create mode 100644 doc/CMakeLists.txt create mode 100644 doc/index.docbook create mode 100644 ktmagnetdownloader/CMakeLists.txt create mode 100644 ktmagnetdownloader/magnetdownloader.cpp create mode 100644 ktmagnetdownloader/magnettest.cpp create mode 100644 ktmagnetdownloader/magnettest.h create mode 100644 ktorrent/CMakeLists.txt create mode 100644 ktorrent/core.cpp create mode 100644 ktorrent/core.h create mode 100644 ktorrent/dialogs/addpeersdlg.cpp create mode 100644 ktorrent/dialogs/addpeersdlg.h create mode 100644 ktorrent/dialogs/addpeersdlg.ui create mode 100644 ktorrent/dialogs/fileselectdlg.cpp create mode 100644 ktorrent/dialogs/fileselectdlg.h create mode 100644 ktorrent/dialogs/fileselectdlg.ui create mode 100644 ktorrent/dialogs/importdialog.cpp create mode 100644 ktorrent/dialogs/importdialog.h create mode 100644 ktorrent/dialogs/importdialog.ui create mode 100644 ktorrent/dialogs/missingfilesdlg.cpp create mode 100644 ktorrent/dialogs/missingfilesdlg.h create mode 100644 ktorrent/dialogs/missingfilesdlg.ui create mode 100644 ktorrent/dialogs/pastedialog.cpp create mode 100644 ktorrent/dialogs/pastedialog.h create mode 100644 ktorrent/dialogs/pastedlgbase.ui create mode 100644 ktorrent/dialogs/speedlimitsdlg.cpp create mode 100644 ktorrent/dialogs/speedlimitsdlg.h create mode 100644 ktorrent/dialogs/speedlimitsdlg.ui create mode 100644 ktorrent/dialogs/speedlimitsmodel.cpp create mode 100644 ktorrent/dialogs/speedlimitsmodel.h create mode 100644 ktorrent/dialogs/spinboxdelegate.cpp create mode 100644 ktorrent/dialogs/spinboxdelegate.h create mode 100644 ktorrent/dialogs/torrentcreatordlg.cpp create mode 100644 ktorrent/dialogs/torrentcreatordlg.h create mode 100644 ktorrent/dialogs/torrentcreatordlg.ui create mode 100644 ktorrent/groups/groupfiltermodel.cpp create mode 100644 ktorrent/groups/groupfiltermodel.h create mode 100644 ktorrent/groups/grouppolicydlg.cpp create mode 100644 ktorrent/groups/grouppolicydlg.h create mode 100644 ktorrent/groups/grouppolicydlg.ui create mode 100644 ktorrent/groups/groupswitcher.cpp create mode 100644 ktorrent/groups/groupswitcher.h create mode 100644 ktorrent/groups/groupview.cpp create mode 100644 ktorrent/groups/groupview.h create mode 100644 ktorrent/groups/groupviewmodel.cpp create mode 100644 ktorrent/groups/groupviewmodel.h create mode 100644 ktorrent/gui.cpp create mode 100644 ktorrent/gui.h create mode 100644 ktorrent/icons/128-apps-ktorrent.png create mode 100644 ktorrent/icons/16-actions-kt-change-tracker.png create mode 100644 ktorrent/icons/16-actions-kt-check-data.png create mode 100644 ktorrent/icons/16-actions-kt-chunks.png create mode 100644 ktorrent/icons/16-actions-kt-encrypted.png create mode 100644 ktorrent/icons/16-actions-kt-info-widget.png create mode 100644 ktorrent/icons/16-actions-kt-magnet.png create mode 100644 ktorrent/icons/16-actions-kt-pause.png create mode 100644 ktorrent/icons/16-actions-kt-queue-manager.png create mode 100644 ktorrent/icons/16-actions-kt-remove.png create mode 100644 ktorrent/icons/16-actions-kt-restore-defaults.png create mode 100644 ktorrent/icons/16-actions-kt-set-max-download-speed.png create mode 100644 ktorrent/icons/16-actions-kt-set-max-upload-speed.png create mode 100644 ktorrent/icons/16-actions-kt-show-hide.png create mode 100644 ktorrent/icons/16-actions-kt-show-statusbar.png create mode 100644 ktorrent/icons/16-actions-kt-speed-limits.png create mode 100644 ktorrent/icons/16-actions-kt-start-all.png create mode 100644 ktorrent/icons/16-actions-kt-start.png create mode 100644 ktorrent/icons/16-actions-kt-stop-all.png create mode 100644 ktorrent/icons/16-actions-kt-stop.png create mode 100644 ktorrent/icons/16-actions-kt-upnp.png create mode 100644 ktorrent/icons/16-apps-ktorrent.png create mode 100644 ktorrent/icons/22-actions-kt-magnet.png create mode 100644 ktorrent/icons/22-actions-kt-pause.png create mode 100644 ktorrent/icons/22-actions-kt-remove.png create mode 100644 ktorrent/icons/22-actions-kt-set-max-download-speed.png create mode 100644 ktorrent/icons/22-actions-kt-set-max-upload-speed.png create mode 100644 ktorrent/icons/22-actions-kt-speed-limits.png create mode 100644 ktorrent/icons/22-actions-kt-start-all.png create mode 100644 ktorrent/icons/22-actions-kt-start.png create mode 100644 ktorrent/icons/22-actions-kt-stop-all.png create mode 100644 ktorrent/icons/22-actions-kt-stop.png create mode 100644 ktorrent/icons/22-apps-ktorrent.png create mode 100644 ktorrent/icons/256-apps-ktorrent.png create mode 100644 ktorrent/icons/32-actions-kt-info-widget.png create mode 100644 ktorrent/icons/32-actions-kt-magnet.png create mode 100644 ktorrent/icons/32-actions-kt-pause.png create mode 100644 ktorrent/icons/32-actions-kt-queue-manager.png create mode 100644 ktorrent/icons/32-actions-kt-remove.png create mode 100644 ktorrent/icons/32-actions-kt-set-max-download-speed.png create mode 100644 ktorrent/icons/32-actions-kt-set-max-upload-speed.png create mode 100644 ktorrent/icons/32-actions-kt-speed-limits.png create mode 100644 ktorrent/icons/32-actions-kt-start-all.png create mode 100644 ktorrent/icons/32-actions-kt-start.png create mode 100644 ktorrent/icons/32-actions-kt-stop-all.png create mode 100644 ktorrent/icons/32-actions-kt-stop.png create mode 100644 ktorrent/icons/32-actions-kt-upnp.png create mode 100644 ktorrent/icons/32-apps-ktorrent.png create mode 100644 ktorrent/icons/48-actions-kt-bandwidth-scheduler.png create mode 100644 ktorrent/icons/48-actions-kt-change-tracker.png create mode 100644 ktorrent/icons/48-actions-kt-check-data.png create mode 100644 ktorrent/icons/48-actions-kt-chunks.png create mode 100644 ktorrent/icons/48-actions-kt-info-widget.png create mode 100644 ktorrent/icons/48-actions-kt-magnet.png create mode 100644 ktorrent/icons/48-actions-kt-pause.png create mode 100644 ktorrent/icons/48-actions-kt-plugins.png create mode 100644 ktorrent/icons/48-actions-kt-queue-manager.png create mode 100644 ktorrent/icons/48-actions-kt-remove.png create mode 100644 ktorrent/icons/48-actions-kt-restore-defaults.png create mode 100644 ktorrent/icons/48-actions-kt-set-max-download-speed.png create mode 100644 ktorrent/icons/48-actions-kt-set-max-upload-speed.png create mode 100644 ktorrent/icons/48-actions-kt-show-hide.png create mode 100644 ktorrent/icons/48-actions-kt-show-statusbar.png create mode 100644 ktorrent/icons/48-actions-kt-speed-limits.png create mode 100644 ktorrent/icons/48-actions-kt-start-all.png create mode 100644 ktorrent/icons/48-actions-kt-start.png create mode 100644 ktorrent/icons/48-actions-kt-stop-all.png create mode 100644 ktorrent/icons/48-actions-kt-stop.png create mode 100644 ktorrent/icons/48-actions-kt-upnp.png create mode 100644 ktorrent/icons/48-apps-ktorrent.png create mode 100644 ktorrent/icons/64-actions-kt-magnet.png create mode 100644 ktorrent/icons/64-apps-ktorrent.png create mode 100644 ktorrent/icons/CMakeLists.txt create mode 100644 ktorrent/icons/sc-actions-kt-magnet.svgz create mode 100644 ktorrent/icons/sc-actions-kt-set-max-download-speed.svgz create mode 100644 ktorrent/icons/sc-actions-kt-set-max-upload-speed.svgz create mode 100644 ktorrent/icons/sc-actions-kt-speed-limits.svgz create mode 100644 ktorrent/ipfilterlist.cpp create mode 100644 ktorrent/ipfilterlist.h create mode 100644 ktorrent/ipfilterwidget.cpp create mode 100644 ktorrent/ipfilterwidget.h create mode 100644 ktorrent/ipfilterwidget.ui create mode 100644 ktorrent/ktorrent.notifyrc create mode 100644 ktorrent/ktorrentui.rc create mode 100644 ktorrent/kttorrentactivityui.rc create mode 100644 ktorrent/main.cpp create mode 100644 ktorrent/org.kde.ktorrent.appdata.xml create mode 100755 ktorrent/org.kde.ktorrent.desktop create mode 100644 ktorrent/pref/advancedpref.cpp create mode 100644 ktorrent/pref/advancedpref.h create mode 100644 ktorrent/pref/advancedpref.ui create mode 100644 ktorrent/pref/btpref.cpp create mode 100644 ktorrent/pref/btpref.h create mode 100644 ktorrent/pref/btpref.ui create mode 100644 ktorrent/pref/colorpref.cpp create mode 100644 ktorrent/pref/colorpref.h create mode 100644 ktorrent/pref/colorpref.ui create mode 100644 ktorrent/pref/generalpref.cpp create mode 100644 ktorrent/pref/generalpref.h create mode 100644 ktorrent/pref/generalpref.ui create mode 100644 ktorrent/pref/networkpref.cpp create mode 100644 ktorrent/pref/networkpref.h create mode 100644 ktorrent/pref/networkpref.ui create mode 100644 ktorrent/pref/prefdialog.cpp create mode 100644 ktorrent/pref/prefdialog.h create mode 100644 ktorrent/pref/proxypref.cpp create mode 100644 ktorrent/pref/proxypref.h create mode 100644 ktorrent/pref/proxypref.ui create mode 100644 ktorrent/pref/qmpref.cpp create mode 100644 ktorrent/pref/qmpref.h create mode 100644 ktorrent/pref/qmpref.ui create mode 100644 ktorrent/pref/recommendedsettingsdlg.cpp create mode 100644 ktorrent/pref/recommendedsettingsdlg.h create mode 100644 ktorrent/pref/recommendedsettingsdlg.ui create mode 100644 ktorrent/statusbar.cpp create mode 100644 ktorrent/statusbar.h create mode 100644 ktorrent/statusbarofflineindicator.cpp create mode 100644 ktorrent/statusbarofflineindicator.h create mode 100644 ktorrent/tools/magnetmodel.cpp create mode 100644 ktorrent/tools/magnetmodel.h create mode 100644 ktorrent/tools/magnetview.cpp create mode 100644 ktorrent/tools/magnetview.h create mode 100644 ktorrent/tools/queuemanagermodel.cpp create mode 100644 ktorrent/tools/queuemanagermodel.h create mode 100644 ktorrent/tools/queuemanagerwidget.cpp create mode 100644 ktorrent/tools/queuemanagerwidget.h create mode 100644 ktorrent/torrentactivity.cpp create mode 100644 ktorrent/torrentactivity.h create mode 100644 ktorrent/trayicon.cpp create mode 100644 ktorrent/trayicon.h create mode 100644 ktorrent/view/propertiesdlg.cpp create mode 100644 ktorrent/view/propertiesdlg.h create mode 100644 ktorrent/view/propertiesdlg.ui create mode 100644 ktorrent/view/scanextender.cpp create mode 100644 ktorrent/view/scanextender.h create mode 100644 ktorrent/view/scanextender.ui create mode 100644 ktorrent/view/torrentsearchbar.cpp create mode 100644 ktorrent/view/torrentsearchbar.h create mode 100644 ktorrent/view/view.cpp create mode 100644 ktorrent/view/view.h create mode 100644 ktorrent/view/viewdelegate.cpp create mode 100644 ktorrent/view/viewdelegate.h create mode 100644 ktorrent/view/viewjobtracker.cpp create mode 100644 ktorrent/view/viewjobtracker.h create mode 100644 ktorrent/view/viewmodel.cpp create mode 100644 ktorrent/view/viewmodel.h create mode 100644 ktorrent/view/viewselectionmodel.cpp create mode 100644 ktorrent/view/viewselectionmodel.h create mode 100644 ktupnptest/CMakeLists.txt create mode 100644 ktupnptest/main.cpp create mode 100644 ktupnptest/upnptestwidget.cpp create mode 100644 ktupnptest/upnptestwidget.h create mode 100644 ktupnptest/upnptestwidget.ui create mode 100644 libktcore/CMakeLists.txt create mode 100644 libktcore/config-ktcore.h.cmake create mode 100644 libktcore/dbus/dbus.cpp create mode 100644 libktcore/dbus/dbus.h create mode 100644 libktcore/dbus/dbusgroup.cpp create mode 100644 libktcore/dbus/dbusgroup.h create mode 100644 libktcore/dbus/dbussettings.cpp create mode 100644 libktcore/dbus/dbussettings.h create mode 100644 libktcore/dbus/dbustorrent.cpp create mode 100644 libktcore/dbus/dbustorrent.h create mode 100644 libktcore/dbus/dbustorrentfile.cpp create mode 100644 libktcore/dbus/dbustorrentfile.h create mode 100644 libktcore/dbus/dbustorrentfilestream.cpp create mode 100644 libktcore/dbus/dbustorrentfilestream.h create mode 100644 libktcore/groups/allgroup.cpp create mode 100644 libktcore/groups/allgroup.h create mode 100644 libktcore/groups/functiongroup.cpp create mode 100644 libktcore/groups/functiongroup.h create mode 100644 libktcore/groups/group.cpp create mode 100644 libktcore/groups/group.h create mode 100644 libktcore/groups/groupmanager.cpp create mode 100644 libktcore/groups/groupmanager.h create mode 100644 libktcore/groups/torrentgroup.cpp create mode 100644 libktcore/groups/torrentgroup.h create mode 100644 libktcore/groups/ungroupedgroup.cpp create mode 100644 libktcore/groups/ungroupedgroup.h create mode 100644 libktcore/gui/centralwidget.cpp create mode 100644 libktcore/gui/centralwidget.h create mode 100644 libktcore/gui/extender.cpp create mode 100644 libktcore/gui/extender.h create mode 100644 libktcore/gui/tabbarwidget.cpp create mode 100644 libktcore/gui/tabbarwidget.h create mode 100644 libktcore/interfaces/activity.cpp create mode 100644 libktcore/interfaces/activity.h create mode 100644 libktcore/interfaces/coreinterface.cpp create mode 100644 libktcore/interfaces/coreinterface.h create mode 100644 libktcore/interfaces/functions.cpp create mode 100644 libktcore/interfaces/functions.h create mode 100644 libktcore/interfaces/guiinterface.cpp create mode 100644 libktcore/interfaces/guiinterface.h create mode 100644 libktcore/interfaces/plugin.cpp create mode 100644 libktcore/interfaces/plugin.h create mode 100644 libktcore/interfaces/prefpageinterface.cpp create mode 100644 libktcore/interfaces/prefpageinterface.h create mode 100644 libktcore/interfaces/torrentactivityinterface.cpp create mode 100644 libktcore/interfaces/torrentactivityinterface.h create mode 100644 libktcore/ktorrent.kcfg create mode 100644 libktcore/ktversion.h create mode 100644 libktcore/plugin/pluginactivity.cpp create mode 100644 libktcore/plugin/pluginactivity.h create mode 100644 libktcore/plugin/pluginmanager.cpp create mode 100644 libktcore/plugin/pluginmanager.h create mode 100644 libktcore/settings.kcfgc create mode 100644 libktcore/torrent/basicjobprogresswidget.cpp create mode 100644 libktcore/torrent/basicjobprogresswidget.h create mode 100644 libktcore/torrent/basicjobprogresswidget.ui create mode 100644 libktcore/torrent/chunkbar.cpp create mode 100644 libktcore/torrent/chunkbar.h create mode 100644 libktcore/torrent/chunkbarrenderer.cpp create mode 100644 libktcore/torrent/chunkbarrenderer.h create mode 100644 libktcore/torrent/jobprogresswidget.cpp create mode 100644 libktcore/torrent/jobprogresswidget.h create mode 100644 libktcore/torrent/jobtracker.cpp create mode 100644 libktcore/torrent/jobtracker.h create mode 100644 libktcore/torrent/magnetmanager.cpp create mode 100644 libktcore/torrent/magnetmanager.h create mode 100644 libktcore/torrent/queuemanager.cpp create mode 100644 libktcore/torrent/queuemanager.h create mode 100644 libktcore/torrent/torrentfilelistmodel.cpp create mode 100644 libktcore/torrent/torrentfilelistmodel.h create mode 100644 libktcore/torrent/torrentfilemodel.cpp create mode 100644 libktcore/torrent/torrentfilemodel.h create mode 100644 libktcore/torrent/torrentfiletreemodel.cpp create mode 100644 libktcore/torrent/torrentfiletreemodel.h create mode 100644 libktcore/util/indexofcompare.h create mode 100644 libktcore/util/itemselectionmodel.cpp create mode 100644 libktcore/util/itemselectionmodel.h create mode 100644 libktcore/util/mmapfile.cpp create mode 100644 libktcore/util/mmapfile.h create mode 100644 libktcore/util/stringcompletionmodel.cpp create mode 100644 libktcore/util/stringcompletionmodel.h create mode 100644 libktcore/util/treefiltermodel.cpp create mode 100644 libktcore/util/treefiltermodel.h create mode 100644 logo.png create mode 100644 plugins/CMakeLists.txt create mode 100644 plugins/bwscheduler/CMakeLists.txt create mode 100644 plugins/bwscheduler/bwprefpage.cpp create mode 100644 plugins/bwscheduler/bwprefpage.h create mode 100644 plugins/bwscheduler/bwprefpage.ui create mode 100644 plugins/bwscheduler/bwschedulerplugin.cpp create mode 100644 plugins/bwscheduler/bwschedulerplugin.h create mode 100644 plugins/bwscheduler/bwschedulerpluginsettings.kcfgc create mode 100644 plugins/bwscheduler/edititemdlg.cpp create mode 100644 plugins/bwscheduler/edititemdlg.h create mode 100644 plugins/bwscheduler/edititemdlg.ui create mode 100644 plugins/bwscheduler/guidanceline.cpp create mode 100644 plugins/bwscheduler/guidanceline.h create mode 100644 plugins/bwscheduler/ktbwschedulerplugin.kcfg create mode 100644 plugins/bwscheduler/ktorrent_bwscheduler.desktop create mode 100644 plugins/bwscheduler/ktorrent_bwschedulerui.rc create mode 100644 plugins/bwscheduler/schedule.cpp create mode 100644 plugins/bwscheduler/schedule.h create mode 100644 plugins/bwscheduler/scheduleeditor.cpp create mode 100644 plugins/bwscheduler/scheduleeditor.h create mode 100644 plugins/bwscheduler/schedulegraphicsitem.cpp create mode 100644 plugins/bwscheduler/schedulegraphicsitem.h create mode 100644 plugins/bwscheduler/weekdaymodel.cpp create mode 100644 plugins/bwscheduler/weekdaymodel.h create mode 100644 plugins/bwscheduler/weekscene.cpp create mode 100644 plugins/bwscheduler/weekscene.h create mode 100644 plugins/bwscheduler/weekview.cpp create mode 100644 plugins/bwscheduler/weekview.h create mode 100644 plugins/downloadorder/CMakeLists.txt create mode 100644 plugins/downloadorder/downloadorderdialog.cpp create mode 100644 plugins/downloadorder/downloadorderdialog.h create mode 100644 plugins/downloadorder/downloadordermanager.cpp create mode 100644 plugins/downloadorder/downloadordermanager.h create mode 100644 plugins/downloadorder/downloadordermodel.cpp create mode 100644 plugins/downloadorder/downloadordermodel.h create mode 100644 plugins/downloadorder/downloadorderplugin.cpp create mode 100644 plugins/downloadorder/downloadorderplugin.h create mode 100644 plugins/downloadorder/downloadorderwidget.ui create mode 100644 plugins/downloadorder/ktorrent_downloadorder.desktop create mode 100644 plugins/downloadorder/ktorrent_downloadorderui.rc create mode 100644 plugins/infowidget/CMakeLists.txt create mode 100644 plugins/infowidget/addtrackersdialog.cpp create mode 100644 plugins/infowidget/addtrackersdialog.h create mode 100644 plugins/infowidget/availabilitychunkbar.cpp create mode 100644 plugins/infowidget/availabilitychunkbar.h create mode 100644 plugins/infowidget/chunkdownloadmodel.cpp create mode 100644 plugins/infowidget/chunkdownloadmodel.h create mode 100644 plugins/infowidget/chunkdownloadview.cpp create mode 100644 plugins/infowidget/chunkdownloadview.h create mode 100644 plugins/infowidget/chunkdownloadview.ui create mode 100644 plugins/infowidget/downloadedchunkbar.cpp create mode 100644 plugins/infowidget/downloadedchunkbar.h create mode 100644 plugins/infowidget/fileview.cpp create mode 100644 plugins/infowidget/fileview.h create mode 100644 plugins/infowidget/flagdb.cpp create mode 100644 plugins/infowidget/flagdb.h create mode 100644 plugins/infowidget/geoipmanager.cpp create mode 100644 plugins/infowidget/geoipmanager.h create mode 100644 plugins/infowidget/infowidgetplugin.cpp create mode 100644 plugins/infowidget/infowidgetplugin.h create mode 100644 plugins/infowidget/infowidgetpluginsettings.kcfgc create mode 100644 plugins/infowidget/iwfilelistmodel.cpp create mode 100644 plugins/infowidget/iwfilelistmodel.h create mode 100644 plugins/infowidget/iwfiletreemodel.cpp create mode 100644 plugins/infowidget/iwfiletreemodel.h create mode 100644 plugins/infowidget/iwprefpage.cpp create mode 100644 plugins/infowidget/iwprefpage.h create mode 100644 plugins/infowidget/iwprefpage.ui create mode 100644 plugins/infowidget/ktinfowidgetplugin.kcfg create mode 100644 plugins/infowidget/ktorrent_infowidget.desktop create mode 100644 plugins/infowidget/monitor.cpp create mode 100644 plugins/infowidget/monitor.h create mode 100644 plugins/infowidget/peerview.cpp create mode 100644 plugins/infowidget/peerview.h create mode 100644 plugins/infowidget/peerviewmodel.cpp create mode 100644 plugins/infowidget/peerviewmodel.h create mode 100644 plugins/infowidget/statustab.cpp create mode 100644 plugins/infowidget/statustab.h create mode 100644 plugins/infowidget/statustab.ui create mode 100644 plugins/infowidget/trackermodel.cpp create mode 100644 plugins/infowidget/trackermodel.h create mode 100644 plugins/infowidget/trackerview.cpp create mode 100644 plugins/infowidget/trackerview.h create mode 100644 plugins/infowidget/trackerview.ui create mode 100644 plugins/infowidget/webseedsmodel.cpp create mode 100644 plugins/infowidget/webseedsmodel.h create mode 100644 plugins/infowidget/webseedstab.cpp create mode 100644 plugins/infowidget/webseedstab.h create mode 100644 plugins/infowidget/webseedstab.ui create mode 100644 plugins/ipfilter/CMakeLists.txt create mode 100644 plugins/ipfilter/convertdialog.cpp create mode 100644 plugins/ipfilter/convertdialog.h create mode 100644 plugins/ipfilter/convertdialog.ui create mode 100644 plugins/ipfilter/convertthread.cpp create mode 100644 plugins/ipfilter/convertthread.h create mode 100644 plugins/ipfilter/downloadandconvertjob.cpp create mode 100644 plugins/ipfilter/downloadandconvertjob.h create mode 100644 plugins/ipfilter/ipblockingprefpage.cpp create mode 100644 plugins/ipfilter/ipblockingprefpage.h create mode 100644 plugins/ipfilter/ipblockingprefpage.ui create mode 100644 plugins/ipfilter/ipblocklist.cpp create mode 100644 plugins/ipfilter/ipblocklist.h create mode 100644 plugins/ipfilter/ipfilterplugin.cpp create mode 100644 plugins/ipfilter/ipfilterplugin.h create mode 100644 plugins/ipfilter/ipfilterpluginsettings.kcfgc create mode 100644 plugins/ipfilter/ktipfilterplugin.kcfg create mode 100644 plugins/ipfilter/ktorrent_ipfilter.desktop create mode 100644 plugins/ipfilter/tests/CMakeLists.txt create mode 100644 plugins/ipfilter/tests/ipblocklisttest.cpp create mode 100644 plugins/logviewer/CMakeLists.txt create mode 100644 plugins/logviewer/ktlogviewerplugin.kcfg create mode 100644 plugins/logviewer/ktorrent_logviewer.desktop create mode 100644 plugins/logviewer/logflags.cpp create mode 100644 plugins/logviewer/logflags.h create mode 100644 plugins/logviewer/logflagsdelegate.cpp create mode 100644 plugins/logviewer/logflagsdelegate.h create mode 100644 plugins/logviewer/logprefpage.cpp create mode 100644 plugins/logviewer/logprefpage.h create mode 100644 plugins/logviewer/logprefwidget.ui create mode 100644 plugins/logviewer/logviewer.cpp create mode 100644 plugins/logviewer/logviewer.h create mode 100644 plugins/logviewer/logviewerplugin.cpp create mode 100644 plugins/logviewer/logviewerplugin.h create mode 100644 plugins/logviewer/logviewerpluginsettings.kcfgc create mode 100644 plugins/magnetgenerator/CMakeLists.txt create mode 100644 plugins/magnetgenerator/ktmagnetgeneratorplugin.kcfg create mode 100644 plugins/magnetgenerator/ktorrent_magnetgenerator.desktop create mode 100644 plugins/magnetgenerator/ktorrent_magnetgeneratorui.rc create mode 100644 plugins/magnetgenerator/magnetgeneratorplugin.cpp create mode 100644 plugins/magnetgenerator/magnetgeneratorplugin.h create mode 100644 plugins/magnetgenerator/magnetgeneratorpluginsettings.kcfgc create mode 100644 plugins/magnetgenerator/magnetgeneratorprefwidget.cpp create mode 100644 plugins/magnetgenerator/magnetgeneratorprefwidget.h create mode 100644 plugins/magnetgenerator/magnetgeneratorprefwidget.ui create mode 100644 plugins/mediaplayer/CMakeLists.txt create mode 100644 plugins/mediaplayer/ktmediaplayerplugin.kcfg create mode 100644 plugins/mediaplayer/ktorrent_mediaplayer.desktop create mode 100644 plugins/mediaplayer/ktorrent_mediaplayerui.rc create mode 100644 plugins/mediaplayer/mediacontroller.cpp create mode 100644 plugins/mediaplayer/mediacontroller.h create mode 100644 plugins/mediaplayer/mediacontroller.ui create mode 100644 plugins/mediaplayer/mediafile.cpp create mode 100644 plugins/mediaplayer/mediafile.h create mode 100644 plugins/mediaplayer/mediafilestream.cpp create mode 100644 plugins/mediaplayer/mediafilestream.h create mode 100644 plugins/mediaplayer/mediamodel.cpp create mode 100644 plugins/mediaplayer/mediamodel.h create mode 100644 plugins/mediaplayer/mediaplayer.cpp create mode 100644 plugins/mediaplayer/mediaplayer.h create mode 100644 plugins/mediaplayer/mediaplayeractivity.cpp create mode 100644 plugins/mediaplayer/mediaplayeractivity.h create mode 100644 plugins/mediaplayer/mediaplayerplugin.cpp create mode 100644 plugins/mediaplayer/mediaplayerplugin.h create mode 100644 plugins/mediaplayer/mediaplayerpluginsettings.kcfgc create mode 100644 plugins/mediaplayer/mediaview.cpp create mode 100644 plugins/mediaplayer/mediaview.h create mode 100644 plugins/mediaplayer/playlist.cpp create mode 100644 plugins/mediaplayer/playlist.h create mode 100644 plugins/mediaplayer/playlistwidget.cpp create mode 100644 plugins/mediaplayer/playlistwidget.h create mode 100644 plugins/mediaplayer/videochunkbar.cpp create mode 100644 plugins/mediaplayer/videochunkbar.h create mode 100644 plugins/mediaplayer/videowidget.cpp create mode 100644 plugins/mediaplayer/videowidget.h create mode 100644 plugins/scanfolder/CMakeLists.txt create mode 100644 plugins/scanfolder/ktorrent_scanfolder.desktop create mode 100644 plugins/scanfolder/ktscanfolderplugin.kcfg create mode 100644 plugins/scanfolder/scanfolder.cpp create mode 100644 plugins/scanfolder/scanfolder.h create mode 100644 plugins/scanfolder/scanfolderplugin.cpp create mode 100644 plugins/scanfolder/scanfolderplugin.h create mode 100644 plugins/scanfolder/scanfolderpluginsettings.kcfgc create mode 100644 plugins/scanfolder/scanfolderprefpage.cpp create mode 100644 plugins/scanfolder/scanfolderprefpage.h create mode 100644 plugins/scanfolder/scanfolderprefpage.ui create mode 100644 plugins/scanfolder/scanthread.cpp create mode 100644 plugins/scanfolder/scanthread.h create mode 100644 plugins/scanfolder/torrentloadqueue.cpp create mode 100644 plugins/scanfolder/torrentloadqueue.h create mode 100644 plugins/scanforlostfiles/CMakeLists.txt create mode 100644 plugins/scanforlostfiles/fsproxymodel.h create mode 100644 plugins/scanforlostfiles/ktorrent_scanforlostfiles.desktop create mode 100644 plugins/scanforlostfiles/ktscanforlostfilesplugin.kcfg create mode 100644 plugins/scanforlostfiles/nodeoperations.cpp create mode 100644 plugins/scanforlostfiles/nodeoperations.h create mode 100644 plugins/scanforlostfiles/scanforlostfilesplugin.cpp create mode 100644 plugins/scanforlostfiles/scanforlostfilesplugin.h create mode 100644 plugins/scanforlostfiles/scanforlostfilespluginsettings.kcfgc create mode 100644 plugins/scanforlostfiles/scanforlostfilesprefpage.cpp create mode 100644 plugins/scanforlostfiles/scanforlostfilesprefpage.h create mode 100644 plugins/scanforlostfiles/scanforlostfilesprefpage.ui create mode 100644 plugins/scanforlostfiles/scanforlostfilesthread.cpp create mode 100644 plugins/scanforlostfiles/scanforlostfilesthread.h create mode 100644 plugins/scanforlostfiles/scanforlostfileswidget.cpp create mode 100644 plugins/scanforlostfiles/scanforlostfileswidget.h create mode 100644 plugins/scanforlostfiles/scanforlostfileswidget.ui create mode 100644 plugins/scripting/CMakeLists.txt create mode 100644 plugins/scripting/api/scriptablegroup.cpp create mode 100644 plugins/scripting/api/scriptablegroup.h create mode 100644 plugins/scripting/api/scriptingmodule.cpp create mode 100644 plugins/scripting/api/scriptingmodule.h create mode 100644 plugins/scripting/ktorrent_scripting.desktop create mode 100644 plugins/scripting/ktorrent_scriptingui.rc create mode 100644 plugins/scripting/script.cpp create mode 100644 plugins/scripting/script.h create mode 100644 plugins/scripting/scriptdelegate.cpp create mode 100644 plugins/scripting/scriptdelegate.h create mode 100644 plugins/scripting/scriptingplugin.cpp create mode 100644 plugins/scripting/scriptingplugin.h create mode 100644 plugins/scripting/scriptmanager.cpp create mode 100644 plugins/scripting/scriptmanager.h create mode 100644 plugins/scripting/scriptmodel.cpp create mode 100644 plugins/scripting/scriptmodel.h create mode 100644 plugins/scripting/scriptproperties.ui create mode 100644 plugins/scripting/scripts/CMakeLists.txt create mode 100644 plugins/scripting/scripts/auto_remove/CMakeLists.txt create mode 100644 plugins/scripting/scripts/auto_remove/auto_remove.desktop create mode 100644 plugins/scripting/scripts/auto_remove/auto_remove.py create mode 100644 plugins/scripting/scripts/auto_remove/auto_remove.ui create mode 100644 plugins/scripting/scripts/auto_resume/CMakeLists.txt create mode 100644 plugins/scripting/scripts/auto_resume/auto_resume.desktop create mode 100644 plugins/scripting/scripts/auto_resume/auto_resume.py create mode 100644 plugins/scripting/scripts/auto_resume/auto_resume.ui create mode 100644 plugins/scripting/scripts/email_notifications/CMakeLists.txt create mode 100644 plugins/scripting/scripts/email_notifications/email_notifications.desktop create mode 100644 plugins/scripting/scripts/email_notifications/email_notifications.py create mode 100644 plugins/scripting/scripts/email_notifications/emailconfig.ui create mode 100644 plugins/scripting/scripts/test.py create mode 100644 plugins/scripting/scripts/tracker_groups/CMakeLists.txt create mode 100644 plugins/scripting/scripts/tracker_groups/tracker_groups.desktop create mode 100644 plugins/scripting/scripts/tracker_groups/tracker_groups.py create mode 100644 plugins/search/CMakeLists.txt create mode 100644 plugins/search/home/CMakeLists.txt create mode 100644 plugins/search/home/body-background.jpg create mode 100644 plugins/search/home/box-bottom-left.png create mode 100644 plugins/search/home/box-bottom-middle.png create mode 100644 plugins/search/home/box-bottom-right.png create mode 100644 plugins/search/home/box-center.png create mode 100644 plugins/search/home/box-middle-left.png create mode 100644 plugins/search/home/box-middle-right.png create mode 100644 plugins/search/home/box-top-left.png create mode 100644 plugins/search/home/box-top-middle.png create mode 100644 plugins/search/home/box-top-right.png create mode 100644 plugins/search/home/home.css create mode 100644 plugins/search/home/home.html create mode 100644 plugins/search/home/ktorrent-icon.png create mode 100644 plugins/search/home/ktorrent_infopage.css create mode 100644 plugins/search/ktorrent_search.desktop create mode 100644 plugins/search/ktorrent_searchui.rc create mode 100644 plugins/search/ktsearchplugin.kcfg create mode 100644 plugins/search/magneturlschemehandler.cpp create mode 100644 plugins/search/magneturlschemehandler.h create mode 100644 plugins/search/opensearch/CMakeLists.txt create mode 100644 plugins/search/opensearch/btdb.in/favicon.ico create mode 100644 plugins/search/opensearch/btdb.in/opensearch.xml create mode 100644 plugins/search/opensearch/btdig.com/favicon.ico create mode 100644 plugins/search/opensearch/btdig.com/opensearch.xml create mode 100644 plugins/search/opensearch/duckduckgo.com/favicon.ico create mode 100644 plugins/search/opensearch/duckduckgo.com/opensearch.xml create mode 100644 plugins/search/opensearch/torrentproject.se/favicon.ico create mode 100644 plugins/search/opensearch/torrentproject.se/opensearch.xml create mode 100644 plugins/search/opensearchdownloadjob.cpp create mode 100644 plugins/search/opensearchdownloadjob.h create mode 100644 plugins/search/proxy_helper.cpp create mode 100644 plugins/search/proxy_helper.h create mode 100644 plugins/search/searchactivity.cpp create mode 100644 plugins/search/searchactivity.h create mode 100644 plugins/search/searchengine.cpp create mode 100644 plugins/search/searchengine.h create mode 100644 plugins/search/searchenginelist.cpp create mode 100644 plugins/search/searchenginelist.h create mode 100644 plugins/search/searchplugin.cpp create mode 100644 plugins/search/searchplugin.h create mode 100644 plugins/search/searchpluginsettings.kcfgc create mode 100644 plugins/search/searchpref.ui create mode 100644 plugins/search/searchprefpage.cpp create mode 100644 plugins/search/searchprefpage.h create mode 100644 plugins/search/searchtoolbar.cpp create mode 100644 plugins/search/searchtoolbar.h create mode 100644 plugins/search/searchwidget.cpp create mode 100644 plugins/search/searchwidget.h create mode 100644 plugins/search/webview.cpp create mode 100644 plugins/search/webview.h create mode 100644 plugins/shutdown/CMakeLists.txt create mode 100644 plugins/shutdown/ktorrent_shutdown.desktop create mode 100644 plugins/shutdown/ktorrent_shutdownui.rc create mode 100644 plugins/shutdown/shutdowndlg.cpp create mode 100644 plugins/shutdown/shutdowndlg.h create mode 100644 plugins/shutdown/shutdowndlg.ui create mode 100644 plugins/shutdown/shutdownplugin.cpp create mode 100644 plugins/shutdown/shutdownplugin.h create mode 100644 plugins/shutdown/shutdownruleset.cpp create mode 100644 plugins/shutdown/shutdownruleset.h create mode 100644 plugins/shutdown/shutdowntorrentmodel.cpp create mode 100644 plugins/shutdown/shutdowntorrentmodel.h create mode 100644 plugins/stats/CMakeLists.txt create mode 100644 plugins/stats/Conns.ui create mode 100644 plugins/stats/ConnsTabPage.cc create mode 100644 plugins/stats/ConnsTabPage.h create mode 100644 plugins/stats/DartTestfile.txt create mode 100644 plugins/stats/DisplaySettings.ui create mode 100644 plugins/stats/DisplaySettingsPage.cc create mode 100644 plugins/stats/DisplaySettingsPage.h create mode 100644 plugins/stats/PluginPage.cc create mode 100644 plugins/stats/PluginPage.h create mode 100644 plugins/stats/Settings.ui create mode 100644 plugins/stats/SettingsPage.cc create mode 100644 plugins/stats/SettingsPage.h create mode 100644 plugins/stats/Spd.ui create mode 100644 plugins/stats/SpdTabPage.cc create mode 100644 plugins/stats/SpdTabPage.h create mode 100644 plugins/stats/StatsPlugin.cc create mode 100644 plugins/stats/StatsPlugin.h create mode 100644 plugins/stats/drawer/ChartDrawer.cc create mode 100644 plugins/stats/drawer/ChartDrawer.h create mode 100644 plugins/stats/drawer/ChartDrawerData.cc create mode 100644 plugins/stats/drawer/ChartDrawerData.h create mode 100644 plugins/stats/drawer/KPlotWgtDrawer.cc create mode 100644 plugins/stats/drawer/KPlotWgtDrawer.h create mode 100644 plugins/stats/drawer/PlainChartDrawer.cc create mode 100644 plugins/stats/drawer/PlainChartDrawer.h create mode 100644 plugins/stats/ktorrent_stats.desktop create mode 100644 plugins/stats/ktstatsplugin.kcfg create mode 100644 plugins/stats/statspluginsettings.kcfgc create mode 100644 plugins/syndication/CMakeLists.txt create mode 100644 plugins/syndication/feedlist.cpp create mode 100644 plugins/syndication/feedlist.h create mode 100644 plugins/syndication/feedlistdelegate.cpp create mode 100644 plugins/syndication/feedlistdelegate.h create mode 100644 plugins/syndication/feedlistview.cpp create mode 100644 plugins/syndication/feedlistview.h create mode 100644 plugins/syndication/feedretriever.cpp create mode 100644 plugins/syndication/feedretriever.h create mode 100644 plugins/syndication/feedwidget.cpp create mode 100644 plugins/syndication/feedwidget.h create mode 100644 plugins/syndication/feedwidget.ui create mode 100644 plugins/syndication/feedwidgetmodel.cpp create mode 100644 plugins/syndication/feedwidgetmodel.h create mode 100644 plugins/syndication/filter.cpp create mode 100644 plugins/syndication/filter.h create mode 100644 plugins/syndication/filtereditor.cpp create mode 100644 plugins/syndication/filtereditor.h create mode 100644 plugins/syndication/filtereditor.ui create mode 100644 plugins/syndication/filterlist.cpp create mode 100644 plugins/syndication/filterlist.h create mode 100644 plugins/syndication/filterlistmodel.cpp create mode 100644 plugins/syndication/filterlistmodel.h create mode 100644 plugins/syndication/filterlistview.cpp create mode 100644 plugins/syndication/filterlistview.h create mode 100644 plugins/syndication/icons/16-actions-kt-add-feeds.png create mode 100644 plugins/syndication/icons/16-actions-kt-add-filters.png create mode 100644 plugins/syndication/icons/16-actions-kt-remove-feeds.png create mode 100644 plugins/syndication/icons/16-actions-kt-remove-filters.png create mode 100644 plugins/syndication/icons/22-actions-kt-add-feeds.png create mode 100644 plugins/syndication/icons/22-actions-kt-add-filters.png create mode 100644 plugins/syndication/icons/22-actions-kt-remove-feeds.png create mode 100644 plugins/syndication/icons/22-actions-kt-remove-filters.png create mode 100644 plugins/syndication/icons/32-actions-kt-add-feeds.png create mode 100644 plugins/syndication/icons/32-actions-kt-add-filters.png create mode 100644 plugins/syndication/icons/32-actions-kt-remove-feeds.png create mode 100644 plugins/syndication/icons/32-actions-kt-remove-filters.png create mode 100644 plugins/syndication/icons/CMakeLists.txt create mode 100644 plugins/syndication/icons/hisc-action-kt-add-feeds.svgz create mode 100644 plugins/syndication/icons/hisc-action-kt-add-filters.svgz create mode 100644 plugins/syndication/icons/hisc-action-kt-remove-feeds.svgz create mode 100644 plugins/syndication/icons/hisc-action-kt-remove-filters.svgz create mode 100644 plugins/syndication/ktfeed.cpp create mode 100644 plugins/syndication/ktfeed.h create mode 100644 plugins/syndication/ktorrent_syndication.desktop create mode 100644 plugins/syndication/ktorrent_syndicationui.rc create mode 100644 plugins/syndication/linkdownloader.cpp create mode 100644 plugins/syndication/linkdownloader.h create mode 100644 plugins/syndication/managefiltersdlg.cpp create mode 100644 plugins/syndication/managefiltersdlg.h create mode 100644 plugins/syndication/managefiltersdlg.ui create mode 100644 plugins/syndication/syndicationactivity.cpp create mode 100644 plugins/syndication/syndicationactivity.h create mode 100644 plugins/syndication/syndicationplugin.cpp create mode 100644 plugins/syndication/syndicationplugin.h create mode 100644 plugins/syndication/syndicationtab.cpp create mode 100644 plugins/syndication/syndicationtab.h create mode 100644 plugins/upnp/CMakeLists.txt create mode 100644 plugins/upnp/ktorrent_upnp.desktop create mode 100644 plugins/upnp/ktupnpplugin.kcfg create mode 100644 plugins/upnp/routermodel.cpp create mode 100644 plugins/upnp/routermodel.h create mode 100644 plugins/upnp/upnpplugin.cpp create mode 100644 plugins/upnp/upnpplugin.h create mode 100644 plugins/upnp/upnppluginsettings.kcfgc create mode 100644 plugins/upnp/upnpwidget.cpp create mode 100644 plugins/upnp/upnpwidget.h create mode 100644 plugins/upnp/upnpwidget.ui create mode 100644 plugins/zeroconf/CMakeLists.txt create mode 100644 plugins/zeroconf/ktorrent_zeroconf.desktop create mode 100644 plugins/zeroconf/torrentservice.cpp create mode 100644 plugins/zeroconf/torrentservice.h create mode 100644 plugins/zeroconf/zeroconfplugin.cpp create mode 100644 plugins/zeroconf/zeroconfplugin.h create mode 100644 po/ar/ktorrent.po create mode 100644 po/ast/ktorrent.po create mode 100644 po/be/ktorrent.po create mode 100644 po/bg/ktorrent.po create mode 100644 po/bs/ktorrent.po create mode 100644 po/ca/docs/ktorrent/index.docbook create mode 100644 po/ca/ktorrent.po create mode 100644 po/ca@valencia/ktorrent.po create mode 100644 po/cs/ktorrent.po create mode 100644 po/da/ktorrent.po create mode 100644 po/de/docs/ktorrent/index.docbook create mode 100644 po/de/ktorrent.po create mode 100644 po/el/ktorrent.po create mode 100644 po/en_GB/ktorrent.po create mode 100644 po/eo/ktorrent.po create mode 100644 po/es/docs/ktorrent/index.docbook create mode 100644 po/es/ktorrent.po create mode 100644 po/et/docs/ktorrent/index.docbook create mode 100644 po/et/ktorrent.po create mode 100644 po/eu/ktorrent.po create mode 100644 po/fi/ktorrent.po create mode 100644 po/fr/docs/ktorrent/index.docbook create mode 100644 po/fr/ktorrent.po create mode 100644 po/ga/ktorrent.po create mode 100644 po/gl/ktorrent.po create mode 100644 po/he/ktorrent.po create mode 100644 po/hi/ktorrent.po create mode 100644 po/hr/ktorrent.po create mode 100644 po/hu/ktorrent.po create mode 100644 po/ia/ktorrent.po create mode 100644 po/is/ktorrent.po create mode 100644 po/it/docs/ktorrent/index.docbook create mode 100644 po/it/ktorrent.po create mode 100644 po/ja/ktorrent.po create mode 100644 po/kk/ktorrent.po create mode 100644 po/km/ktorrent.po create mode 100644 po/ko/ktorrent.po create mode 100644 po/lt/ktorrent.po create mode 100644 po/lv/ktorrent.po create mode 100644 po/mr/ktorrent.po create mode 100644 po/nb/ktorrent.po create mode 100644 po/nds/ktorrent.po create mode 100644 po/nl/docs/ktorrent/index.docbook create mode 100644 po/nl/ktorrent.po create mode 100644 po/nn/ktorrent.po create mode 100644 po/pl/ktorrent.po create mode 100644 po/pt/docs/ktorrent/index.docbook create mode 100644 po/pt/ktorrent.po create mode 100644 po/pt_BR/docs/ktorrent/index.docbook create mode 100644 po/pt_BR/ktorrent.po create mode 100644 po/ro/ktorrent.po create mode 100644 po/ru/docs/ktorrent/index.docbook create mode 100644 po/ru/ktorrent.po create mode 100644 po/se/ktorrent.po create mode 100644 po/sk/ktorrent.po create mode 100644 po/sl/ktorrent.po create mode 100644 po/sq/ktorrent.po create mode 100644 po/sr/ktorrent.po create mode 100644 po/sv/docs/ktorrent/index.docbook create mode 100644 po/sv/ktorrent.po create mode 100644 po/tr/ktorrent.po create mode 100644 po/ug/ktorrent.po create mode 100644 po/uk/docs/ktorrent/index.docbook create mode 100644 po/uk/ktorrent.po create mode 100644 po/zh_CN/docs/ktorrent/index.docbook create mode 100644 po/zh_CN/ktorrent.po create mode 100644 po/zh_TW/ktorrent.po create mode 100755 scripts/GetExtragearSite.sh create mode 100755 scripts/kcfg_qobject_gen.py diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 0000000..c3add8f --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,4 @@ +# astyle +3ab3ce686bb749132d3f4b6f00346e942ea697b4 +# clang-format +008ee3176e5e256f003f324683c54bc7908e3b4f diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..46d7d6d --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +build +.kdev4/ +ktorrent.kdev4 +CMakeLists.txt.user +/.idea +cmake-build-* +/.clang-format diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..ec4fbaf --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,240 @@ +cmake_minimum_required(VERSION 3.16 FATAL_ERROR) + +# KDE Application Version, managed by release script +set (RELEASE_SERVICE_VERSION_MAJOR "21") +set (RELEASE_SERVICE_VERSION_MINOR "08") +set (RELEASE_SERVICE_VERSION_MICRO "0") +set (RELEASE_SERVICE_VERSION "${RELEASE_SERVICE_VERSION_MAJOR}.${RELEASE_SERVICE_VERSION_MINOR}.${RELEASE_SERVICE_VERSION_MICRO}") +project(KTorrent VERSION ${RELEASE_SERVICE_VERSION}) + +# drop leading 0 from minor version to avoid bogus octal value with x.08 releases +string(REGEX REPLACE "^0+" "" RELEASE_SERVICE_VERSION_MINOR_STRIPPED "${RELEASE_SERVICE_VERSION_MINOR}") +add_definitions(-D'VERSION="${RELEASE_SERVICE_VERSION}"' + -D'VERSION_MAJOR=${RELEASE_SERVICE_VERSION_MAJOR}' + -D'VERSION_MINOR=${RELEASE_SERVICE_VERSION_MINOR_STRIPPED}' + -D'VERSION_MICRO=${RELEASE_SERVICE_VERSION_MICRO}') + +set (QT_MIN_VERSION "5.15.0") +set (KF5_MIN_VERSION "5.82") +set (LIBKTORRENT_MIN_VERSION "20.11.70") +set (Boost_MIN_VERSION "1.71.0") + +find_package(ECM ${KF5_MIN_VERSION} REQUIRED NO_MODULE) +set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH} ${CMAKE_MODULE_PATH}) + +include(KDEInstallDirs) +include(KDECMakeSettings) +include(KDECompilerSettings NO_POLICY_SCOPE) +include(FeatureSummary) +include(GenerateExportHeader) +include(ECMInstallIcons) +include(ECMAddAppIcon) +include(ECMMarkAsTest) +include(ECMMarkNonGuiExecutable) +include(KDEClangFormat) +include(KDEGitCommitHooks) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +find_package(Qt5 ${QT_MIN_VERSION} CONFIG REQUIRED + Core + DBus + Network + Widgets +) + +find_package(KF5 ${KF5_MIN_VERSION} REQUIRED + COMPONENTS + Config + ConfigWidgets + CoreAddons + Crash + DBusAddons + I18n + IconThemes + KIO + Notifications + NotifyConfig + KCMUtils + Parts + Solid + WidgetsAddons + WindowSystem + XmlGui + OPTIONAL_COMPONENTS + DocTools +) + +find_package(KF5Torrent ${LIBKTORRENT_MIN_VERSION} CONFIG REQUIRED) +find_package(Boost ${Boost_MIN_VERSION} REQUIRED) + +find_package(KF5TextWidgets ${KF5_MIN_VERSION}) +set_package_properties(KF5TextWidgets + PROPERTIES + TYPE OPTIONAL + PURPOSE "Required for KTorrent's IP Filter plugin" +) +if(KF5TextWidgets_FOUND) + set(HAVE_KF5TextWidgets 1) +endif() + +find_package(KF5Archive ${KF5_MIN_VERSION}) +set_package_properties(KF5Archive + PROPERTIES + TYPE OPTIONAL + PURPOSE "Required for KTorrent's scripting plugin" +) +if(KF5Archive_FOUND) + set(HAVE_KF5Archive 1) +endif() + +find_package(KF5ItemViews ${KF5_MIN_VERSION}) +set_package_properties(KF5ItemViews + PROPERTIES + TYPE OPTIONAL + PURPOSE "Required for KTorrent's scripting plugin" +) +if(KF5ItemViews_FOUND) + set(HAVE_KF5ItemViews 1) +endif() + +find_package(KF5Kross ${KF5_MIN_VERSION}) +set_package_properties(KF5Kross + PROPERTIES + TYPE OPTIONAL + PURPOSE "Required for KTorrent's scripting plugin" +) +if(KF5Kross_FOUND) + set(HAVE_KF5Kross 1) +endif() + +find_package(KF5Plotting ${KF5_MIN_VERSION}) +set_package_properties(KF5Plotting + PROPERTIES + TYPE OPTIONAL + PURPOSE "Required for KTorrent's statistics plugin" +) +if(KF5Plotting_FOUND) + set(HAVE_KF5Plotting 1) +endif() + +find_package(KF5Syndication ${KF5_MIN_VERSION}) +set_package_properties(KF5Syndication + PROPERTIES + TYPE OPTIONAL + PURPOSE "Required for KTorrent's syndication plugin" +) +if(KF5Syndication_FOUND) + set(HAVE_KF5Syndication 1) +endif() + +find_package(Qt5WebEngineWidgets ${QT_MIN_VERSION}) +set_package_properties(Qt5WebEngineWidgets + PROPERTIES + TYPE OPTIONAL + PURPOSE "Required for KTorrent's search and syndication plugins" +) +if(Qt5WebEngineWidgets_FOUND) + set(HAVE_Qt5WebEngineWidgets 1) +endif() + +find_package(KF5DNSSD ${KF5_MIN_VERSION}) +set_package_properties(KF5DNSSD + PROPERTIES + TYPE OPTIONAL + PURPOSE "Required for KTorrent's Zeroconf plugin" +) +if(KF5DNSSD_FOUND) + set(HAVE_KF5DNSSD 1) +endif() + +find_package(KF5Completion ${KF5_MIN_VERSION}) +set_package_properties(KF5Completion + PROPERTIES + TYPE OPTIONAL + PURPOSE "Required for KTorrent's UPnP plugin" +) +if(KF5Completion_FOUND) + set(HAVE_KF5Completion 1) +endif() + +find_package(LibKWorkspace CONFIG) +set_package_properties(LibKWorkspace + PROPERTIES + TYPE OPTIONAL + PURPOSE "Required for KTorrent's shutdown plugin" +) +if(LibKWorkspace_FOUND) + set(HAVE_LibKWorkspace 1) +endif() + +#find_package(Qt5 ${QT_MIN_VERSION} OPTIONAL_COMPONENTS Multimedia MultimediaWidgets) + +find_package(Taglib) +set_package_properties(Taglib + PROPERTIES + TYPE OPTIONAL + PURPOSE "Required for KTorrent's multimedia plugin" +) + +if(TAGLIB_FOUND) + set(HAVE_Taglib 1) +endif() + +find_package(Phonon4Qt5) +set_package_properties(Phonon4Qt5 + PROPERTIES + TYPE OPTIONAL + PURPOSE "Required for KTorrent's multimedia plugin" +) + + +# +# checks for functions and stuff +# +include(CheckIncludeFiles) +include(CheckFunctionExists) +include(CheckTypeSize) #XFS + + +# according to http://www.cmake.org/pipermail/cmake/2008-June/022381.html +kde_enable_exceptions() + +add_definitions( + -DQT_USE_QSTRINGBUILDER + -DQT_NO_CAST_TO_ASCII + -DQT_NO_CAST_FROM_ASCII + -DQT_STRICT_ITERATORS + -DQT_NO_URL_CAST_FROM_STRING + -DQT_NO_CAST_FROM_BYTEARRAY + -DQT_NO_CAST_TO_BYTEARRAY + -DQT_NO_KEYWORDS + -DQT_USE_FAST_OPERATOR_PLUS +) + +set (KTORRENT_DBUS_XML_DIR ${CMAKE_SOURCE_DIR}/dbus_xml) +set (KTORRENT_PLUGIN_INSTALL_DIR ${PLUGIN_INSTALL_DIR}/ktorrent) + +include_directories(Boost::boost) + +add_subdirectory(libktcore) +add_subdirectory(plugins) +add_subdirectory(ktorrent) +add_subdirectory(ktupnptest) +add_subdirectory(ktmagnetdownloader) +if (KF5DocTools_FOUND) + add_subdirectory(doc) +endif() +# add clang-format target for all our real source files +file(GLOB_RECURSE ALL_CLANG_FORMAT_SOURCE_FILES *.cpp *.h) +kde_clang_format(${ALL_CLANG_FORMAT_SOURCE_FILES}) +kde_configure_git_pre_commit_hook(CHECKS CLANG_FORMAT) +kde_configure_git_pre_commit_hook(CHECKS CLANG_FORMAT) + + +feature_summary(WHAT ALL INCLUDE_QUIET_PACKAGES FATAL_ON_MISSING_REQUIRED_PACKAGES) +ki18n_install(po) +if (KF5DocTools_FOUND) + kdoctools_install(po) +endif() diff --git a/COPYING b/COPYING new file mode 100644 index 0000000..5185fd3 --- /dev/null +++ b/COPYING @@ -0,0 +1,346 @@ +NOTE! The GPL below is copyrighted by the Free Software Foundation, but +the instance of code that it refers to (the kde programs) are copyrighted +by the authors who actually wrote it. + +--------------------------------------------------------------------------- + + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc. + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Library General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) 19yy + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) 19yy name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Library General +Public License instead of this License. diff --git a/ChangeLog b/ChangeLog new file mode 100644 index 0000000..357a2cd --- /dev/null +++ b/ChangeLog @@ -0,0 +1,975 @@ +Changes in 5.0.1: +- Store interface name as a string in cfg +- Port stats plugin + +Changes in 5.0: +- Port to KF5/Qt5: QHttp-based tracker communication was dropped in favour of KIO-based one +- Fix issues found by Coverity static analyzer +- Improve naming of few UI items +- Initiate sequential download of media content automatically when it is opened from filelist view + +Changes in 4.4: +- Fix typo in generalpref.ui (305383) +- Remove unnecessary volume action from MediaPlayerActivity (305378) +- Use translatable unit names in connections stats graph (305395) +- Make sure search engine selected entry is not cleared when defaults are added (306913) +- Reset filter when the filterbar is closed (305379) +- Show notification when torrents are opened silently (260329) +- Remove kio-magnet, nobody really uses it and it only causes confusion for people who only want to use magnet links with ktorrent (309091) +- Revamp syndication plugin interface and add support for torrent elements from namespace (http://xmlns.ezrss.it/0.1/) within the rss item element (311224) +- Fix crash in torrent loading code under some circumstances (309332) +- Add option to enable or disable torrent highlighting (Review: 107810) +- Fix queue order not reflecting reality in some circumstances (308579) +- Use enclosure urls ending with .torrent in the syndication plugin +- Fix bug causing tab counts not to be initialized when QM is disabled (313737) +- Use file contents to figure out which type of blocklist we are dealing with in IP filter (315239) +- Fix bug causing torrent percentage to be slightly off in the files tab (315549) +- Remove estimation algorithm option +- Fix double click on subdirectory not working in fileview (337457) +- Drop support for importing KDE3 torrents +- Support x-scheme-handler/magnet mimetype +- Fix some magnet links not working in webinterface plugin (339584) + +Changes in 4.3.2: +- Backport ipfilter extraction fixes from master branch (315239) + +Changes in 4.3.1: +- Fix new custom groups not appearing in add to group submenu (307230) +- Fix crash when right clicking on some locations in the GroupView due to 0 pointer (307160) +- Force KDirWatch to use polling when watching NFS filesystems (301381) +- Fix crash due to bus error in ip filter plugin (307854) +- First check for missing files then for unmounted storage (308638) +- Use extended selection mode in syndication plugin so ctrl key works as expected (308672) +- Fix crash in GroupViewModel when dragging torrents over groups (308733) +- Sort by total leechers and seeders, if connected seeders and leechers is equal (311470) +- Fix gzip not working in ipfilter plugin due to wrong mimetype (315239) + +Changes in 4.3: +- Change default blocklist url to iblocklist.org (305109) + +Changes in 4.3rc1: +- Add support for magnet links in the syndication plugin +- Add search line to download order dialog +- Add move top and bottom option to download order dialog (295341) +- Make sure that move up and move down in the download order dialog work with multiple items (295341) +- Also update file priorities when normal priority file completes downloading in download order plugin (296576) +- Add automatic sorting options to download order plugin (name, seasons and episodes, album track number) (295341) +- Make float comparison factor smaller in ViewModel when checking if share ratio has changed (297931) +- Show torrent count next to group in GroupView (176173) +- Add new GroupSwitcher widget +- Add edit group policy button to GroupSwitcher +- Remove html tooltips from ui files +- Tracker grouping script now uses the domain name of the tracker (227430) +- Catch SIGINT and SIGTERM and do a clean shutdown (296835) +- Add bytes left to download column in View (152070) +- Add new add trackers dialog with url completion (269563) +- Delete key now also works in the MagnetView (295173) +- Fix sorting of time left column (298542) +- Add support for removable storage (286120) +- Fix typos in several strings (300969, 300781, 300782) +- Fix suspended overlay not shown on tray icon when ktorrent is started up suspended (301042) +- Make sure default size of main window, tracker column in tracker view and filename column in file view are sensible (300057) +- Fix crash in syndication plugin (301117) +- Fix close search tab button not getting disabled when only one is left after closing another one (302896) +- Fix crash when looking up country for IP address (303340) +- Fix size after download calculation in FileSelectDlg when file already exists (304595) +- Use default browser instead of application associated to test/html when clicking on a links in the comments field of the status tab (305005) + +Changes in 4.2.1: +- Fix IP filter widget list not getting registered at startup (281245) +- Fix Queue Manager widget getting the priorities wrong (296536) + +Changes in 4.2: +- Add new group switcher combobox in toolbar (290888) +- Make kio-magnet optional +- Improve handing magnet uris in search plugin +- Fix bug causing kio-magnet to deselect all files +- Close current scan widget when a new one is started for a torrent (292001) +- Fix syndication plugin not handling relative urls properly +- Fix magnet uri's not being handled properly in load torrent feature of webinterface + +Changes in 4.2rc1: +- Make it possible to check individual files of a torrent +- Data checks for multiple torrents can now get started at the same time (265611) +- Make links clickable in the status tab comments field (266089) +- Revamp UPnP plugin (266397) +- Fix bug making it possible to add conflicting items to the schedule (268879) +- Make it possible for schedule items to span multiple days (225939) +- Include disk usage of existing files when calculating bytes left after download in FileSelectDlg (267220) +- Disable scripts in GUI, if no interpreter could be found for the script (270226) +- Show shutdown plugin's config dialog when no rules are present and the enable button is pressed (271311) +- Make it possible to wait for several events before activating the shutdown action +- Fix wrong required diskspace calculation in some rare circumstances (271097) +- Revamp scanfolder plugin (263813, 271657) +- Revamp GUI of mediaplayer plugin +- Make properties extender a dialog (274356) +- Fix bug causing scripts not to be selectable in scripting plugin +- Show info hash in status tab +- Improve video streaming support +- Add Open With option to FileView context menu (279386) +- Fix bug causing views to not get properly restored when the application language changes (279588) +- Check if new trackers have a supported protocol +- Remove usage of several deprecated KDE3 networking classes +- Remove obsolete config options of datacheck during upload feature +- Improve error display of syndication plugin errors (280814) +- Remove multiple views support (281675) +- Relabel Change Tracker button into Switch Tracker to avoid user confusion (282951) +- Revamp IP filter widget (281245) +- Fix scanfolder moving to loaded directory causing a flurry of popups about the torrent already being loaded (283508) +- Only enable the infowidget plugin by default (278283) +- Add force start option (213504) +- Add torrent search bar for view (269279) +- Make it possible to hide uploads, downloads and not queued torrents in the queue manager (253779, 251861, 210064) +- Merge status and name column into one column and use icons to show the status (272160, 228564) +- Highlight in bold and scroll to newly added torrents (145028) +- Show torrent names in remove torrent and data confirmation dialog (284728) +- Enable select new location button when all non excluded files are missing instead of when all files are missing +- Hide chunkbar when download of stream is complete (259788) +- Suspend/resume queue when middle clicking on tray icon (210027) +- Use dbus to show ktorrent window from plasma applet (287309) +- Fix crash due to dangling pointer (281196) +- Always make already loaded message of a torrent a passive popup (288201) +- Make torrent properties dialog modal (288448) +- Allow i2p addresses as a custom ip in tracker announces +- Fix bug causing homepage of search plugin not the render properly + +Changes in 4.1.3: +- Fix statusbar hiding and showing not working (281674) +- Fix missing items in tray menu (282304) +- Fix move on completion location not being set when open all torrents silently is enabled (279582) + +Changes in 4.1.2: +- Fix crash when duplicate trackers are added (274413) +- Fix bug causing quit action not be shown in system tray when not using KDE +- Fix edit script not working in scripting plugin +- Make log output suspension work again +- Fix crash when shutdown_rules files becomes corrupted (277602) +- Fix file rename leading to file being placed in the toplevel directory (279926) + +Changes in 4.1.1: +- Make sure that libktorrent translation catalog is inserted (269515) +- Fix wrong tooltip in QM preference page +- Make sure activities list is added again to the GUI after toolbars are edited (269727) +- Prevent port conflicts from happening (268989) +- Clear labels of BasicJobProgressWidget in constructor (270035) +- Make tabbar widget icons follow KDE size changes (270277) +- Fix crash in search plugin (271516) + +Changes in 4.1: +- Check if source files are missing before moving them (265607) +- Fix scanextender showing when they are not supposed to (259483) +- Fix mediaplayer unable to stop in buffering mode when streaming (266100) +- KDE 4.4 compatibility fix in search plugin +- Make sure overwrite is possible for the torrent copy file feature +- Fix critical Qt warning when opening context menu in view + +Changes in 4.1rc1: +- Make sure that apply button of config dialog is enabled properly when the group changes in the scanfolder settings +- Add kio-magnet +- Improve performance of ktorrent in situations where there are many torrents (262571) +- Fix memory leak on exit (263802) +- Fix tab bar not matching TabBarWidget state in some circumstances (263068) +- Fix libktorrent not gettting updated when the download of a magnet link finishes and the FileSelectDlg is shown +- Update view tab when switching to it, fixes data scan widget showing in the wrong place (259483) +- Update view captions every GUI update (264215) +- Use user modified path for file column in chunks view (264814) +- Fix QM bug causing torrents to get started when it is not needed (262570) + +Changes in 4.1beta1: +- Remove libktupnp it is now part of libktorrent +- Add support for superseeding (171661) +- Change from and to fields to always ensure that from is smaller then to in the bandwidth scheduler plugin (225951) +- Show if a peer is using the µTP protocol or not +- Cleanup file dialog filters for torrents and make sure that files named torrent, are also seen by the filter (241259) +- Ensure that webinterface plugin works properly with bindv6only flag on (238688) +- Add option to not restore the previous session of the search plugin (233288) +- Save suspended state on exit and restore on restart (241675) +- Add support for cookies in syndication plugin (244967) +- Change GUI to use KParts +- Add feature to set the move on completion directory in the syndication plugin filters +- Improve display of errors in the syndication plugin when downloading a feed fails (246421) +- Make ChunkDownloadView and PeerView use a QSortFilterProxyModel (246835) +- Add download and move when completed location history buttons to FileSelectDlg (218048) +- Make file view of FileSelectDlg sortable (248090) +- Clicking item in day list of AddItemDlg of bwscheduler plugin now toggles it (225953) +- Add support for emule blocklists (194915) +- Add option to configure the number of log lines shown in the logviewer (223887) +- Add default move on completion location to group policy (248092) +- Prevent torrents from sharing the same files (228220) +- Add option to open all torrents silently +- Add exclusion patterns to syndication plugin filters (251141) +- Show current upload and download limit in trayicon submenus instead of limits from the settings (251953) +- Show notifications when automatic update of IP filter fails (243458) +- Add video streaming support (234212) +- Make ratio configurable at which the share ratio becomes green (254144) +- Don't show an error message for each duplicate tracker which is added +- Stop using hardcoded colors in system tray tooltip (255732) +- Add support for tracking jobs inside the GUI of ktorrent +- Ask if an existing file must be overwritten when exporting a torrent (256416) +- Don't reinclude files when changing priority of a directory (252555) +- Open new search tab on middle click (151429) +- Search plugin now uses webkit instead of KHTML +- Add do not ask again option to dialog shown when torrent file is downloaded in the search plugin (210701) +- Add option to rename single file torrents to the file inside (251799) +- Make sure that items in history of download locations and completed locations end with a slash (259499) +- Make search filter in MediaView of mediaplayer plugin case insensitive (259782) + +Changes in 4.0.5: +- Fix crash due to uncaught exception when creating torrent (255020) +- Modify tracker grouping script to avoid a memory leak in krosspython +- Fix bug causing extenders to be shown when the torrent is hidden (248205) +- Fix restoring hidden state of bottom tab bar in the torrent activity +- Improve performance of View (258324) +- Fix bug causing user modified filename not be shown for single file torrents (258860) +- Ensure that lastSaveDir is saved to the config (259041) +- Make sure ktorrent compiles and works with new solid powermanagement api introduced in KDE 4.6beta2 (258331) +- Fix bug causing the wrong path to be opened when opening a nested directory in a multifile torrent + +Changes in 4.0.4: +- Make sure that syndication filter save location overrides group save location (250116) +- Don't hardcode background of bandwidth schedule to white so that system colors are used (251925) +- Improve performance of ViewModel when there are many torrents (216501) +- Backport several improvements for IP filter auto update from trunk +- Fix bug causing two instances of ktorrent to be started when missing files dialog is shown at startup (252099) +- Fix bug causing KT to use invalid dbus paths in group paths +- Fix crash at exit due to a Core::update getting called during exit (254214) + +Changes in 4.0.3: +- Fix bug causing wrong encoding to be used when the default save location of a group is read (244873) +- Fix crash in QueueManagerWidget when moving torrents in the queue +- When renaming files in a torrent prevent duplicate names (244624) +- Fix crash on exit when cleaning up UPnP requests +- Fix crash due to uncaught exception when a tor directory cannot be removed (247979) + +Changes in 4.0.2: +- Fix wrong X-KDE-PluginInfo-Name in ktzeroconfplugin.desktop +- Fix crash caused by 0 pointer in webinterface plugin (242273) +- Increase auto update retry interval of ipfilter plugin to 15 minutes +- Fix bug causing groups file to grow very large over time (243182) + +Changes in 4.0.1: +- Make sure that the proper encoding is used when loading syndication filters +- Display filename if TagLib doesn't know the title of a file in the mediaplayer plugin +- Fix memory leak in plasma dataengine (238948) +- Use routers XML file URL to distinguish between UPnP routers +- Make sure that there are no uncaught exceptions (223243) + +Changes in 4.0: +- In Core::onExit shutdown TCP and UTP servers a bit later to prevent crashes + +Changes in 4.0rc1: +- Split of libbtcore as libktorrent +- Make sure that QM handles max share ratio and max seed time a bit better +- Clear selection of ActivityListWidget before selecting the current item (233289) +- When a torrent row is double clicked open the data directory (230618) +- Switch to new style tray icon (210811,233422) +- Make sure that state is set to active when rendering progress bar in ViewDelegate (232727) +- Suspend KTorrent -> Suspend Torrents to avoid confusion (233421) +- Workaround memory leak in krosspython by not passing QObject pointers as arguments to callMethod (223885, 226924) +- Make move on completion a per torrent feature +- Make compiling plasma applet dependant on Qt version +- Make sure extenders can be resized +- Make sure float is not used in scripting api, doesn't seem to work with python scripts +- Add dbus functions to set and get the maximum and current share ratio and seed time + +Changes in 4.0beta2: +- Use proper KDE function to format date in syndication plugin +- Make sure HTMLPart uses proper charset when rendering page +- Add copy URL item to popupmenu in search plugin view +- Pause becomes suspend +- Revamp torrent creation code +- Avoid unnecessary memcpy when generating hash +- Add UDP tracker scraping +- Improve handling of incomplete handshakes (218875) +- When switching back from fullscreen video make sure the tab shown is the video tab +- Replace KMultiTabBar by QToolBar, because it handles not enough space better (214524) +- Make tab moving possible in TorrentActivity and SearchActivity (215053) +- Show arrows in qm widget to indicate if a torrent is a seed or a download +- Fix crash caused by not checking for 0 pointer (220442) +- Add search line in QM widget which highlights and scrolls to found items (220428) +- Moving multiple items in QM widget is now possible (220428) +- Prevent trackers from sending back to much data (220684) +- Fix typo in in log message of QM (223888) +- Added patch witch adds an additional column to a View showing the date and time a torrent was added +- Fix compilation not working on solaris due to missing NAME_MAX define (222598) +- Fix check for existing files for single file torrents in FileSelectDlg (225900) +- Add pathOnDisk dbus call on a torrent to retrieve the output path +- Fix crash in GroupView (226134) +- Cleanup notifier before closing socket in webinterface plugin, fixes a crash (225956) +- Make sure that weekdays deselects weekend, and weekend deselects weekdays in scheduler plugin (225952) +- Don't show 0 KB/s when there is no limit in scheduler plugin (225943) +- Make warning icon when no trackers can be reached, work for non private torrents (227670) +- Reset bandwidth schedule when network comes up again (227423) +- Make sure TOS is set when setting up a connection +- Fix crash in AuthenticationMonitor, caused by dangling pointer (228395) +- Fix SHA1 hash generation crashes by using shared pointers to PieceData (227400) +- Fix crash in PeerConnector cleanup (228955) +- Make it possible to change the ChunkSelector at runtime +- Make torrents reannounce and kill all stale peers, when network comes back up after some downtime (184766) +- Before mapping a piece, use posix_fallocate to ensure that we can't get a SIGBUS when the disk is full (229081) +- Revamp stats file implementation so the QM can no longer block things when there are many torrents (228974) +- Use pause functionality instead of stopping and restarting a torrent when doing jobs (179456) +- Fix handling of keep-alive in webinterface plugin (225167) +- Don't open files to determine disk usage when files are not open +- Change listen backlog to SOMAXCONN +- Do preallocation properly (or not at all depending on settings) when file is recreated +- Add support for the µTP protocol (197749) +- Fix crash in HTTPConnection due to 0 pointer (231859) +- Add patch to disable authentication in webinterface (226291) +- Add plugin to generate magnet URI's +- Fix crash in webseed downloading code (232971) + +Changes in 4.0beta1: +- Make adding multiple trackers at once possible (213194) +- Bandwidth scheduler can now be deactivated temporarely +- Show information message instead of error when trackerlists are merged (213772) +- Add patch from Leo Trubach which adds support for IP rangs to the IP filter dialog +- Add support for metadata extension +- Remove own flags, now using exclusively those from KDE +- Add support for reqq parameter in extended handshake +- Ignore diskspace check when it cannot be determined on a gvfs mounted filesystem (187141) +- DHT can no longer be disabled at compile time +- Add support for magnet URL's (214375) +- Fix crash due to uncaught warning +- Expand open url dialog, it is now possible to open silently and to select the group +- Fix crash when loading magnets file (218227) + +Changes in 3.3.4: +- Fix SHA1 hash generation crashes once and for all (222753) +- Check for invalid addresses in reverse resolver, fixes a crash +- Fix sorting bug in choking algorithm +- Fix crash caused by handling exceptions badly (224097) +- Fix bug causing global max share ratio and seed time to override group policy (223745) + +Changes in 3.3.3: +- Fix crash caused by 0 pointer in Downloader::saveDownloads (219019) +- Don't remove torrent when jobs are running, wait until they are done, this fixes a crash (218853) +- Disable editing of items in ActivityListModel (219355) +- Make sure checks for ftello and fseeko are done, fixes a problem with large file sizes on BSD (217523) +- Make sure reannounce timer in UDPTracker is always stopped in the stop call (219663) +- Fix broken FilterListModel::removeFilter fixes a crash (219760) +- Revamp adding trackers to TrackerListModel, should fix a crash (218738) +- Don't allow nested event loops when iterating over the incomplete url list in scanfolder plugin, fixes a crash (219885) +- Fix crash due to uncaught exception in TorrentFileTreeModel::loadExpandedState +- Fix bug causing error message that the QM limits are reached to be shown instead of a torrent to be enqueued (220171) +- Fix crash caused by not checking for 0 pointer (221333) +- Fix crashes when DHT socket could not be bound (221872) +- Fix bug causing files to be created which are to big in some rare circumstances (222036) +- Fix bug causing wrong location hint to be used when no default save location is selected (222783) + +Changes in 3.3.2: +- Fix crash when loading Feed in syndication plugin at startup (216207) +- Replace newlines with spaces when displaying multiline comments in statustab (216683) + +Changes in 3.3.1: +- Make sure exceptions in DHT code are caught, fixes a crash (213819) +- Fix several memory leaks at exit +- Fix bug in edge detection of items on the bandwidth schedule +- Fix crash when webinterface plugin is removed and there are active connections to the webserver (214187) +- Fix bugs causing context menus to be shown in wrong location (214657) +- Don't store the color for normal file priority in FileView, use system default (214748) +- Fix bug causing current tab in TorrentActivity not to be restored (214959) +- Fix IP filter download going wrong in some cases, by specifying mimetype in KFilterDev::deviceForFile +- Fix crash when not able to bind for DHT socket (215079) +- Make it optional to intercept Qt debug messages in InitLog call +- Fix crash when trying to unzip broken zip file in ipfilter download (215353) +- Add some limits so DHT tasks don't run for to long and eat up to much memory + +Changes in 3.3: +- Fix deadlock in PacketReader +- Fix bug causing torrent not to get stopped properly when an IO error occurs +- Stop or start trackers when they get disabled or enabled +- Fix CPU consumption bug in DHT NodeLookup and AnnounceTask +- Fix crashes caused by reference count error in piece handling (204227, 186621) +- Make sure powermanagement is inhibited when mediaplayer plugin plays a video +- Fix crash caused by 0 pointer (jobuidelegate not set) (212580) +- Fix deadlock caused by missing endl in JobQueue +- Stopped reannounce timer in HTTPTracker when tracker is stopped after start request failed +- Restore normal limits when unloading bwscheduler plugin + +Changes in 3.3rc1: +- Make sure Qt's warnings, critical and fatal messages are printed to stderr in QtMessageOutput (203423) +- Fix crash by handling missing webinterface skins propperly (203430) +- Make TreeFilterModel use case insensitive filtering (203886) +- Draw progressbar in percentage column of view (190875) +- Make sure running torrents at exit are restarted when KT starts up when the QM is disabled (204988) +- Fix bug causing disabled trackers to be announced in a manual announce +- Add action to menu and toolbar to show or hide the GroupView +- Improved DHT's ability to find peers (205346) +- Make sure file locations are updated before torrent is restarted after data file move +- FileSelectDlg now displays if existing files are found +- Only do automatic update in webinterface of visible sections +- Make it possible to disable automatic update in webinterface +- Make playlist sortable in media player plugin +- Add URL drop support to view (208739) +- DHT packet receiving and parsing moved to separate thread (208801) +- Handle redirection in QHttp based announce job properly +- Make sure saving torrent in search plugin to existing file works +- Add home page for search plugin like the one in konqueror +- Clarify message when checking for files in completed downloads directory (208992) +- Use KUrl::toLocalFile instead of KUrl::path, path prepends / on windows, which breaks things +- Improve editing of feed name +- Fix crash in ChunkDownload (196417) +- Remove quit button from MissingFilesDlg (207388) +- Fix timeout handling of UDPTracker (211191) + +Changes in 3.3beta1: +- Cleanup plugin list (180402) +- If QM enabled, torrents are always managed by the QM, unless user stops them +- QM can now be disabled to allow full manual control of torrents +- Check completed dir for torrent files when opening a torrent (164165) +- Add i18n to scripts, so that they get translated to +- Trackers folder becomes Trackers in tracker_groups.py (183416) +- WebSeeds can now be disabled and enabled individually and globally +- Redesigned GUI with kontact like sidebar to switch between activities +- Added playlist to mediaplayer +- Update size when activities are added or removed (185759) +- Destroy VideoWidget completely when video is closed, to prevent ktorrent from hogging XVideo port (185846) +- Status queued can now result in status strings "Queued for downloading" or "Queued for seeding" (181096) +- Make logviewer widget position configurable +- Added shutdown plugin (182050) +- Add comment to applet desktop file and rename the applet to just KTorrent (183825) +- Added automatic torrent removal script +- The scripting plugin now supports script packages +- Use relative URL's in webinterface (188134) +- Don't send body of login page when redirecting to login page (188209) +- VideoWidget now inhibits screensaver +- Add feature to support a different speed limit when the screensaver is activated (172660) +- Use bytes to calculate availability percentage of a peer (188575) +- Add support for using multiple trackers at the same time for non private torrents (175255) +- Remove config option to select between basic and filesystem specific preallocation method (189784) +- Add patch from amichai which adds easier navigation between torrents in plasma applet (183826) +- GeoIP database will now get downloaded automatically (and updated every 30 days), so it is no longer distributed with ktorrent +- Added support for gzip and bzip2 compressed ipfilter files, and improve zip file support +- Added support for auto completion in tracker, webseed and nodes QLineEdit's of torrent creation dialog (194972) +- Added option to export the torrent file of a torrent +- Choice between file tree or list has been moved from settings to individual views (177401) +- Add filter box in FileSelectDlg (179980) +- Add filter box in FileView (173887) +- Make names of syndication feeds editable (191878) +- Make refresh interval of syndication feeds configurable (193978) +- Add hostname lookup for peers (145760) +- Add filter box to speed limits dialog +- Add jobsystem to each torrent +- Data check progress is now shown inside the View +- Make settings accessible to dbus and scripting engine +- Allow to add tracker to private torrents (198444) +- Make it possible to suspend output of logviewer (200730) +- Add paused overlay on tray icon when queue is paused (174542) +- Add global shortcut to pause the QM +- Add legit torrents search engine (174873) + +Changes in 3.2.5 : +- Fix crash caused by dangling pointer to already destroyed view (209338) +- Backport tracker timeout fix from trunk (208443) + +Changes in 3.2.4 : +- Improve flexibility of IP blocklist parsing to support more formats +- Fix bug in TorrentFileListModel causing wrong checkstate to be shown +- Fix very rare crash caused by 0 pointer (206689) +- Fix bug causing scheduler to not restart the schedule timer +- Unset Qt message handler when Log is destroyed (197944) +- Fix crash caused by dangling pointer to TorrentControl when removing torrent and torrent list is changing a lot (208385) + +Changes in 3.2.3 : +- Make sure View gets focus back when you are finished editing the name of a torrent +- Fix bug causing sizes > 4GB not being displayed properly in plasma applet +- Fix bug in sleep suppression feature causing it to not work when torrents get started at startup (195991) +- Also announce to DHT node when we get back a GetPeers with nodes (194366) +- Use deleteLater when removing torrents this should prevent a crash (197421) +- Do not keep pointer to view menu and groupview menu, this causes crashes when toolbars are reconfigured (198963) +- Make global shortcut to show and hide ktorrent work again (174541) + +Changes in 3.2.2 : +- Fix crash when changing speed limits using system tray menu (188447) +- Fix bug with displaying directory trees in infowidget FileView +- Fix crash caused by calling front on empty QList +- Make sure paths in webseed http requests are URL encoded (189477) +- Prevent torrent from being loaded twice when loading torrents via commandline or external program (190434) +- Fix crash caused by uncaught exception (190317) +- Fix crash caused by sorting list of torrent in QM when the list is being cleared (190759) +- Added patch from amichai which makes the plasma applet a popup applet and which fixes some other things (190822) +- Fix bug causing drag and drop of torrent on to plasma desktop not to work +- Increase numwant to 200 and the maximum number of potential peers to 500 +- Cancel all scan dialogs before exiting, this fixes a crash (191487) +- Config dialog now embeds pages in scroll areas, so that the dialog also is useable on small screens +- Make sure dbus names are valid, this fixes an assert (192007) +- Fix crash when parsing DHT packets (190107) +- Remove newlines from path names of files when present in torrent file (192652) +- Properly check for GeoIP system library (193117) +- Fix crash at exit caused by stopping torrent in it's destructor (193585) +- No longer check if a torrent has to have an announce or a nodes key +- Added experimental option which replaces KIO to announce with a QHttp based solution, this fixes connection to host broken errors +- Make sure http post works properly in search plugin (194037) + +Changes in 3.2.1 : +- Resort torrents if display name is changed +- Fix bug causing torrents to get stuck when data checking and move to completed dir are done when a torrent has finished +- Fix sorting of time left column (185284) +- Make sure selection in view is updated when the view is sorted again +- Prevent torrent from start when torrent is opened user controlled and not started and ktorrent is restarted +- Use IPv6 :: (all interfaces) address to bind to when interface is not specified +- Make sure QM can be edited when queue is paused +- Fix bug causing network interface not to be set when server was initialized +- Backport fix for ktorrent hogging XVideo port +- Backport fix for displaying song information not working properly +- Fix bug causing url requester in fileselect dialog not to allow you to select directories on windows (185739) +- Don't throw away model of FileView, when switching torrents (186031) +- Fix bug causing infinite emission of the stoppedByError signal when corrupted chunks are found in a not started completed torrent +- Port switching no longer requires a restart +- Fix bug in time estimation algorithm causing imported bytes not to be included in average speed calculation +- Fix bug causing torrents to move around when being sorted on floating point numbers and number is equal (186770) +- Improve keyboard navigation of GroupView by activating the current item when pressing enter +- In webinterface clear torrent details before updating the table to prevent an old file list being shown +- Fix webseeding crash (184986) +- Fix several webseeding bugs +- Check for invalid characters (for windows that is) in torrent name on windows (187373) +- Fix webseeding crash caused by wrong chunk range (187882) +- Expanded scripting API with scriptDir function +- Use QFile::rename instead of KIO jobs to do log rotation (188225) +- Make sure that selection gets updated when the peer view and chunkdownload view are sorted (185825) + +Changes in 3.2 : +- Use QDir to create directories, avoids nested event loops, which seems to be buggy in some situations (182327) +- Make sure strings tab is shown first in filter editor dialog +- Fix bug causing torrent to get stalled at startup in some rare situations +- Fix crash at exit +- Fix bug causing problems when multiple fileselect dialogs were shown at the same time (183078) +- Fix bug causing not all tracker groups to get removed when tracker_groups.py is unloaded +- Fix crash when removing the only torrent of a tracker based group (183413) +- Make sure that when a chunk is downloaded via a webseed the chunkmanager is updated +- Don't throw away data when webseed connection closes +- Make sure plasma dataengine interprets strings passed via the stats dbus call, properly (183695) +- Limit group names in tabs to 35 characters, if longer use ... (183689) +- Fix bug causing trackers to be retried continuously when hostname can not be resolved because the network isn't up yet (183697, 183699) +- Fix bug with downloading the same episode twice or more in the syndication plugin + +Changes in 3.2rc1 : +- Fix bug in webgui causing wrong content-length to be returned in some cases +- Add cmake check for libtaskmanager which is needed by the plasma applet +- Make sure ratio limit and seed time limit spinboxes in statustab only emit the valueChanged signal when the user is done editing the spinbox (175625) +- Don't show error message when torrent is loaded silently and the files can't be created (ported from stable) +- Fix crash in plasma applet +- Added select all action, manual announce now gets SHIFT + A as shortcut to not conflict with select all +- Make sure average download speed calculation doesn't wrap around (175747) +- Update buttons when torrent changes status (174815) +- Fix pause button getting disabled when current view doesn't have any torrents (176442) +- Fix bug causing webseeds to prevent chunks from getting downloaded +- Make sure plasma applet restores it's geometry properly and remembers the current torrent +- Make sure initial log rotation doesn't show any progress dialogs +- Fix open in new tab action in search plugin +- Change meaning of active to transferring data +- Inactive groups become passive groups +- Fix bug causing queueingNotPossible message to be shown multiple times (176732) +- Make sure group policy is applied when drag and drop is used to add torrents to a group (176723) +- Make sure preview priority is not set on excluded or only seed files (176487) +- Make sure to long filenames are handled properly (175793) +- Fix bug causing files to be created in diskspace check +- Fix problem with restoring file encoding (177179) +- Make sure window size is restored properly +- Queued -> Queue Manager Controlled in group tree (176726) +- Webgui revamped, PHP is ditched in favor of a HTML + AJAX approach +- Revamp enabling and disabling of actions (175951) +- Make sure speed limits action is always enabled (177695) +- Pause -> Pause KTorrent, Resume -> Resume KTorrent (177402) +- Make sure that moving files when the torrents is complete, happens after the data check when the torrent is complete (177963) +- Fix crash when removing torrent (178175) +- Fix saving and restoring of expanded state of FileView (178022) +- Remove new from system tray menu (177404) +- Ditch QSortFilterProxyModel when possible +- Add option to not download duplicate season and episode matches +- Add feature to import old feeds from RSS plugin of KT 2.2 +- Make sure ViewModel::allTorrents only returns torrents which are shown in the view (179180) +- Improved usability of QM gui by decoupling user controlled property from priority +- UPnP code now sends User-Agent property in HTTP header +- Make sure UPnP does not take exclusive control of UDP port 1900 (179570) +- Ask to create data files if new location selected in missing files dialog does not contain files (178948) +- Fix several memory leaks +- Use idle priority for data checker thread +- Fix bug with moving files when source and destination are the same, but this is not clear due to symlinks +- Make sure paste is disabled when editing the name of a torrent +- Fix bug with wrong byteorder of PEX messages +- Use KLocale::formatBytes so that KTorrent behaves the same as the rest of KDE when displaying sizes and speeds in bytes +- Improve performance of downlod thread by using a socket to wake up the poll call, instead of a regular poll every second +- Fix bug in bandwidth scheduler causing bandwidth schedule to be set continously +- Make sure that setPausedState doesn't do anyting if the paused state doesn't change (180230) +- Disable PEX and DHT actions when multiple torrents are selected +- Reorder queue when additional files are selected and the torrent is queued as a seed. +- Fix bug causing data to be reset every GUI update when editing the name of a torrent +- Move scrape from view menu to tracker tab (179415) +- Fix typos in advanced preferences tooltips (180911) +- Made several performance optimizations +- Fixed bug causing download speed to get stuck at 128 KB/s with small chunks, because chunks didn't get assigned fast enough to peers +- Switch to silly units : KiB, MiB, KiB/s, MiB/s ... +- Make sure Qt log messages end up in log +- Make sure Core gets cleaned up at exit +- Use KSqueezedTextLabel in FeedWidget to handle long url's properly (182127) + +Changes in 3.2beta1 : +- Ask to create directory in fileselectdlg, if it does not exist (163965) +- Added scripting plugin +- Use paths to indicate where a group should go in the GroupView tree +- Expand dbus interface significantly (154483) +- Added feature to disable PEX globally +- Make sure that DHT and PEX checkboxes in view menu are disabled if the feature is disabled globally +- Added plasma applet and dataengine +- Make custom IP to sent to tracker configurable again (166314) +- Add column in QM to show the order of a torrent in the queue +- IP filter revamped, and auto update added (155648) +- Improve performance iby removing unnecessary dataChanged signals +- Improved networking code, which should lead to more stable download speeds +- Fix bug causing closed connections not to be detected +- Make it possible to rename files and folders in fileselectdlg (152813 and 134098) +- Deleting data files is now done in a background job +- Added patch from Adam Forsyth which keeps track of whether we are interested in a peer or not (167938) +- Added patch from Adam Forsyth with some modifications, which allows you to rename torrents in the main torrent list.(168459 and 157544) +- Revamped webinterface +- Patch from Adam Forsyth which adds a second kfiledialog:// keyword openTorrentData (168693) +- Added modified patch from Adam Forsyth showing the directory percentages in the file tree (168062) +- Added patch from Adam Forsyth, which kills stalled peers (168397) +- Plugins with a separate logging id, now need to register and unregister their id with LogSystemManager +- Revamped settings page of logviewer plugin now using a QTreeView and the new LogSystemManager +- Take only seed chunks into account for file percentages (168062) +- Make sure IP filter auto update timer doesn't fire again when auto update is in progress +- Fix bug causing logging output not to be colored properly +- Handle HTTP redirects properly when webseeding (165740) +- Added download order plugin +- Added patch from Aaron Seigo which adds drag and drop support to plasma applet (will only work on KDE 4.2) +- Fix crash at exit in UPnPRouter (170073) +- Make sure order queue cannot be called when all torrents are being stopped +- Revamped chunk memory management completely, reducing memory usage considerably +- Fix crash in ChunkDownload::updateHash +- Remove warmup mode +- Show warning icon in status column when there is something wrong with the tracker, also show tooltip with error message from tracker +- Added patch which adds collapse all and expand all actions to the fileselection dialog and the fileview +- Added category P2P as seen in other p2p applications (e.g. transmission) +- Search plugin is now based upon Opensearch XML descriptions +- Make sure that stuff at exit is done only once +- Make sure cancels get sent when a PeerDownloader is released, so that the wait queue does not fill up +- Don't show all columns when a new upload or download view is created (171770) +- Fix bug causing all columns to be hidden when new views are created which show both uploads and downloads +- Added syndication plugin to handle RSS and Atom torrent feeds +- Added e-mail notification script +- Added script to resume paused torrents after a configurable amount of time (158386) +- Some GUI fixups (tooltips being more uniform, more tooltips, window titles for a lot of dialogs ...) +- By default now use multiple tiers when creating a torrent with multiple torrents (174207) +- Optimize handling of torrents with a lot of files +- Keep track of URL torrent was loaded from and add action to copy it to clipboard (169540 and 173085) +- Optimize chunk position calculation significantly +- Added shortcuts for a lot of actions +- Get rid of duplicate actions (174860) +- Make sure check data and queue actions are enabled when remove is enabled (174869) +- Add script to create automatic tracker based groups script (174871) + +Changes in 3.1.6 : +- Don't show error message when torrent is loaded silently and the files can't be created +- Make sure buttons get updated when paused state is toggled +- Fix deadlock in Downloader when webseeds are active +- Backport pause button fix from trunk +- Backport webseed fix from trunk +- Backport log rotate fix from trunk +- Backport fix for queueingNotPossible multiple message error +- Backport fix for group policy not being applied when using drag and drop +- Backport initial logrotate showing progress info fix +- Backport files get created in diskspace check fix +- Backport file encoding fix +- Backport fix for 177963 (move files after data check when torrent is complete fix) +- Backport UPnP port binding fix +- Backport byteorder bugfix +- Backport bugfix for 180230 +- Backport fix for not optimum speed for small chunk sizes +- Backport some optimizations from trunk + +Changes in 3.1.5 : +- Make sure symlinks work when we create multifile torrents +- Fix redirect loop when torrent is loaded via url in webgui (173513) +- Fix infinite recursive loop in scanfolder plugin +- Fix crash in treeviews caused by Qt bug (172198) +- Prevent error messages when KT is started up via desktop file +- Use multiple tiers when creating torrents with multiple trackers (174207) +- Make sure stalled time is updated properly before resuming queue (174373) +- Prevent QM from starting torrents which are being removed (175081) +- Fix bug preventing max seed time to be set on newly opened torrents +- Fix bug causing torrent to be started when user chooses not to start the +torrent in the fileselectdlg + +Changes in 3.1.4 : +- Make sure user is properly logged in when handling a torrent post in the webgui +- Prevent PHP injection attacks in webgui +- Update file size in CacheFile::growFile, this fixes a SIGBUS error (172814) +- Fix bug causing infinite DNS lookups in UDP tracker when lookup fails +- Remove default label text KSqueezedTextLabel in trackerview (173065) + +Changes in 3.1.3 : +- Make login into webinterface secure +- Check if menus are created properly before showing them +- Stop KT from exiting when scanfolder loads a plugin silently and KT is hidden in systray. +- Make sure duplicate URL's are not shown in trackerview +- Fix problem causing file priorities not to be read at startup +- Make sure added.f is filled in with the proper flags for each peer in the added field of a PEX message (169014) +- Make sure it is not required to put http:// for the proxy in the config dialog (169133) +- Fix typo in advanced pref +- Fix bug causing webseed not to download last chunk of a request +- Fix bug which resulted in speeds of webseed not getting calculated +- Fix rename of groups when edit is not initialized via context menu +- Make toolbars hideable +- Make sure that files can not be moved to the location they are already in +- Don't start torrents at startup when a torrent is user controlled and over limit +- Fix crash in logging code due to 0 pointer +- Remove bitoogle from default search engine list + + +Changes in 3.1.2 : +- Improved performance of GUI updates +- Make sure app icon is set (mostly for windows) +- Fix crash when clicking on clear search history button twice (167580) +- Backported some improvements to download thread +- Make sure cookies are not sent in http announces +- Fix crash at startup which started happening since KDE 4.1 +- Fix problem causing massive memory usage (148385) +- Fix issue causing Timer class to stop working when day changes +- Make sure toolbar settings are restored properly +- Fix compile error on arm architecture +- Fixed some bugs in file deselection code + +Changes in 3.1.1 : +- Fix wrong icon name in PeerView for encrypted peers +- If speed is very low, don't show speed in PeerView +- Fix crash when double clicking on directories in fileview (164434) +- Added GroupFilterModel to filter out torrents, this avoids a bug in Qt which +caused hidden torrents to be shown when a view was sorted by a column. +(164113) +- Make sure port is 80 if it is not specified in device url in upnp library +- Fix Preview in right click menu of View (164503) +- Fix bug in network preferences causing the chosen network interface not to +be selected +- Fix border chunk priority being set wrongly in some rare cases (165587) +- Don't stop connecting with webseed after 3 failed attempts but keep going on +with a longer interval (2 minutes) + +Changes in 3.1 : +- Use KIO::storedGet to download XML description of UPnP router +- Use KIO::mkdir in MakeDir function +- Make sure that path creation on windows works when creating the necessary paths for a torrents' files +- Fix bug in mediaplayer causing wrong file to be played in some case + +Changes in 3.1rc1 : +- Make sure that when keep seeding is on and a torrent is stopped because it has finished downloading, the torrent is not restarted the next time you startup KT +- Clean up stuff is move of data files is canceled (157657) +- Fix recursive infinite loop in QM +- Items on the bandwidth schedule can now be moved around using the mouse +- Items on the bandwidth schedule can now be resized using the mouse +- Show guidance lines when items on schedule are moved or resized +- Colors of the bandwidth scheduler can now be configured +- Added new dialog to add items to the schedule, you can now add the same item to multiple days. +- Make sure that CompressFileJob cannot get stuck (162099) +- Make sure interfaces are not added multiple times to interface combobox in network preferences +- Make sure shortcuts work for back and reload in search plugin +- Add Open in New Tab entry to right click menu in search plugin +- Revamped the UPnP plugin : the widget displaying the routers has been moved from the preferences dialog to the bottom tab bar +- Use QTcpSocket in HttpRequest of UPnP library +- Make sure undo forward and forward buttons on UPnP widget are disabled and enabled properly +- Improved error handling and display in UPnP plugin +- Added recommended settings dialog + +Changes in 3.1beta2 : +- Ported changes from windows branch, so windows is now officially supported +- Added new icons created by Lee Olson +- Fix infinite loop when adding torrent to group in view menu +- Manually save and restore window size and position to fix bug with window size when tray icon is enabled +- Fix crash when trying to show context menu of a dir item in the fileview +- Added toggle action to show or hide the video widget +- Make sure that video widget is shown again when video playing is started and the video widget is hidden +- Ported sparc SIGBUS crash from KDE3 version +- Fix typos in settings page of QM (161664) +- Updated geoip database to most recent one +- Added option to choose the name of the toplevel directory of multifile torrents in the fileselect dialog +- Fix bug when loading torrent via webgui +- Make sure reuse address option is set before bind call in http server +- Open silently no longer shows error messages, instead it uses a passive notification +- Fix tooltip in network pref page +- Use fstat64 if possible to calculate disk usage of CacheFile, should fix diskspace check for files larger then 4 GB (161804) +- Fix bug in media model when torrents with media files get added +- Added import dialog to import torrents from the KDE3 version into the KDE4 one +- Use KDialog instead of QDialog in several dialogs +- Fix bug causing connection stats to be wrong +- Added patch from athantor showing the interval in ms between charts updates on plugin's settings page + +Changes in 3.1beta1 : +- Added Location column (157463) +- Added group for ungrouped torrents (156921) +- Pasting urls to ktorrent will now result in KT opening them (154317) +- Use KNotify system for notifications instead of own hardcoded one with KPassivePopup. Also make this configurable using the standard configure notifications dialog (157513) +- Expanded torrentcreator dialog to make it possible to create torrents with webseeds +- Make sure dslforum urn's in UPNP servicetypes are also supported +- Added support for http webseeding +- Remove 1.02 multiplication factor in allowance calculation of speed limits +- When multiple torrents are started, avoid asking the same questions for multiple torrents. (136381) +- Use CTRL + L as shortcut for speedlimits dialog (144854) +- Expanded bandwidth scheduler with option to set the connection limits (146432) +- Fix bug in generating peer ID based upon version information +- Move proxy settings to a separate page in the settings dialog +- Added KIO::Job to gzip files, and use this job in the log rotation code, thereby removing the not so portable system() calls. +- Use QToolButton instead of a KPushButton as corner widgets for the main tab widget +- Added header menu to configure the columns of a view, to the right click menu of the view. +- Show more detailed info in the scan dialog (160054) +- Added color coded file priorities in the infowidget plugin (158280) +- Make it possible to add newly created torrents to a group (159391) +- Change default values of maxSeeds (5) and maxDownloads(3) (144754) +- Added group policy feature. Each custom group can now have several default settings which will be applied to the torrents of that group. +- The scanfolder plugin now has an option to add torrents to a group, when it loads a torrent. +- Added column to the QueueManager showing the time a torrent is stalled. +- Add feature to decrease torrent priority when the torrent is stalled for longer then a user specified amount of time.(156103) +- Preview size for audio and video files is now configurable. +- Fix some bugs in the queuemanager with the stalled torrent feature +- Added assured speed feature which allows you to set a minimum download speed for each torrent. (151903) +- Use XML GUI stuff for groupview menu +- Menu of views now uses XML GUI stuff (Resulting in the removal of the ViewMenu class) +- Add to group menu now has an entry to add torrents to a new group +- Added PeerID of KGet and BitsOnWheels +- Determining if a file has a preview available, now uses the sizes configured in the settings +- Check for duplicate trackers when users adds a new one. (160678) +- Added media player plugin to play audio and video files +- Fix bug with empty proxy field (160918) +- Expanded logviewer plugin to support media player plugin +- Add sizeHint function to SpinBoxDelegate, so it has a proper size when you edit it +- Make speed limits dialog remember it's size +- Added feature to suppress sleep when torrents are running +- Added feature to skip datacheck in fileselect dialog (and mark the files as fully downloaded) +- Make it possible to disable or enable trackers using a checkbox in front of each tracker in the trackers tab +- Fix saving of current tab in sidebar, we were using the icon name instead of the tab text. +- Show total times a torrent was downloaded in the trackers tab +- Show the number of seeders and leechers in the trackers tab +- Redesigned trackers tab layout (for the above changes) +- Fix editing of speedlimits model +- Find -> Search in search tab to be more consistent +- Added option to open multiple torrents silently or not (159811) +- Paste torrent URL -> Open URL + + +Changes in 3.0.2 : +- Make sure seed time cannot wrap around (159756) +- Fix broken convert of blocklists +- Ported lock file fix for NFS home directories from KDE3 version +- When recreating files, make sure directory they are in, exists +- Fix remove from group not working (160499) +- Fix crash in torrent creator when there are no trackers +- Fix sorting in files tab +- Fix crash when unloading plugins with Qt4.4 (160565) +- Fix update search engines from internet +- Remove torrentspy and update piratebay URL +- Fix 4 GB limit in transfer of statusbar (160711) +- Fix crash when removing 2 torrents at the same time +- Show real IP address of peers when using socks proxies +- Make sure that the current torrent changes when the group is changed of a view +- Sort and merge block list before writing to level1.dat file +- Clear time to next tracker update in tracker tab, when torrent is not +running (160972) +- Fix occasional crash at startup (160935) + + +Changes in 3.0.1 : +- Fix sessionTTL not being able to be bigger then 99 +- Ported stop all and start all in system tray menu fix from KDE3 version +- Fix bug which caused KT not to check preexisting single file torrents (158167) +- Added PeerViewModel for PeerView, also fixes crash (158243) +- Fix hidden_on_exit (158273) +- Add pause resume option to system tray icon (158278) +- Added XFS cmake checks for XFS delayed allocation +- Fix zeroing average speeds on BOTH charts after choosing to reset ONLY one +in stats plugin +- Remove minimum vertical size of URL requester in torrent creation dialog, +this makes sure that it is sized properly when you open the dialog +- Fix changing text codec for multifile torrents (158775) +- Fix crash in handling of KResolverResults (158940) +- Fix bug causing KT to stop seeding after data was moved to the completed dir +(158813) +- Fix keep seeding (159040) +- Make sure settings of logviewer plugin are applied at startup +- Make sure that views stay sorted (158975) +- Make sure speed settings are kept in sync between all the places where you +can change them (159039) +- Make sure that scheduler plugin does not set stuff in paused state twice (152445) +- Fix crash in ConvertDialog of ipfilter plugin +- Fix adding torrent to group in fileselectdlg (159684) + + + +Changes in 3.O : +- Fix sorting of upload and download rate (157939) +- Fix infinite loop in DHT code +- Fix socket descriptor leak (thanks to Richard Narstrom) (156163) +- Patch from Richard Narstrom to fix a problem with character encodings causing KT not to find files anymore after a restart (156838) +- Fix issue with encoding of group names when saving and loading them +- Fix DHT ping storm bug +- Ported open silently from commandline patch from KDE3 version +- Reenabled log rotation +- Added option to use system geoip +- Fix pastedialog (proper usage of KUrl) +- Fix wrong display of percentages in file view +- Fix broken start torrent checkbox in fileselectdlg +- Fix broken import of multifile torrent (157582) +- Make sure that KT is not closed when another window is closed and the main window is hidden (157656) +- Ported behavioral change from KDE3 version: torrents with no selected files are downloads +- Added sorting in speedlimits dialog +- When the speed limits dialog is opened via the context menu of a torrent, select that torrent in the list and scroll to it. (157711) +- Fix broken open data dir and open tor dir in viewmenu (wrong usage of KRun) +- Fix enabling actions bug in view +- Made file priorities more strict (153105) + +Changes in 3.0rc1 : +- Fix typo in tooltip of network sleep interval (154481) +- Fix KT closing when window is hidden and a torrent is opened via commandline (154488) +- Added open silently action to file menu (154484) +- Fix crash in ChunkDownloadView by using a model instead of an item based +approach +- Added action to pause and resume all torrents +- Fix some problems with the update timer not getting started when the QM ran +- Estimate time left in seeding mode when there is a max share ratio +- Disable system tray icon tooltips when show popups is disabled (BUG: 151019) +- Added option to change text encoding of a torrent when opening it +- Fix sorting in main view using a SortFilterProxyModel +- Use XMLGUI stuff for SearchToolBar (BUG: 154838) +- Make it possible to disable plugins at compile time (BUG: 154906) +- Fix sorting in ChunkDownloadView +- Added action to pause and resume all torrents +- Added time estimation algorithm selection +- Fixed bug in KTorrent time estimation algorithm (150866) +- Estimate time left when seeding when there is a max share ratio set (BUG: 142990) +- Ported don't preallocate fix for readonly files +- Make sure toplevel CMakeLists.txt check for KDE4Internal package +- Fix bug in PeerView which displayed the content of choked and snubbed in the wrong column +- Made DHT IPv6 ready +- Add support for peers6 field in tracker announce responses (so we are IPv6 ready there to) +- Fix bug displaying the wrong number of leechers +- Fix bdecoder bug causing an assert in Qt (BUG: 155712) +- Properly fix add to group stuff (remove of ampersand is very hackish) +- Drag drop of torrents upon custom groups now works perfectly +- Fix problem with connections staying in close_wait state (BUG: 156163) +- Save own DHT key in a file and reuse that +- Fix DHT problem causing a DHT ping storm +- Fix opening of directories + + +Changes in 3.0beta1 : +- Added option to select the network interface to use +- Added option to disable data checking during uploading +- Added possibility to open multiple torrents in one go (they will be opened +silently) +- Added option to do a data check when a torrent is finished +- Added new missing files dialog with quit button +- Added support for hostnames in PeerManager::addPotentialPeer +- Added IPv6 support +- Remember current searches in searchplugin and restore them on restart +- Make tracker url and status selectable in tracker tab +- Switch to QCA2 for SHA1 hash generation (old code is still there in case QCA::isSupported("sha1") fails) +- Shutdown update timer when not necessary +- Stop using symlinks and use a file which contains the location of each file of a torrent +- Use a toolbar in the search plugin instead of a widget +- Added global shortcut to show or hide ktorrent +- Show files which a chunk belongs to in chunks tab +- Add alternative fileview mode (flat list) +- After tree succesive mmap failures, switch to buffered mode permanently +- Added SOCKSv4 and v5 support +- Added feature to move inidividual files of a torrent +- When files are missing allow user to select another location diff --git a/LICENSES/GPL-2.0-only.txt b/LICENSES/GPL-2.0-only.txt new file mode 100644 index 0000000..3b6070f --- /dev/null +++ b/LICENSES/GPL-2.0-only.txt @@ -0,0 +1,311 @@ +GNU GENERAL PUBLIC LICENSE +Version 2, June 1991 + +Copyright (C) 1989, 1991 Free Software Foundation, Inc. +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + +Everyone is permitted to copy and distribute verbatim copies of this license +document, but changing it is not allowed. + +Preamble + +The licenses for most software are designed to take away your freedom to share +and change it. By contrast, the GNU General Public License is intended to +guarantee your freedom to share and change free software--to make sure the +software is free for all its users. This General Public License applies to +most of the Free Software Foundation's software and to any other program whose +authors commit to using it. (Some other Free Software Foundation software +is covered by the GNU Lesser General Public License instead.) You can apply +it to your programs, too. + +When we speak of free software, we are referring to freedom, not price. Our +General Public Licenses are designed to make sure that you have the freedom +to distribute copies of free software (and charge for this service if you +wish), that you receive source code or can get it if you want it, that you +can change the software or use pieces of it in new free programs; and that +you know you can do these things. + +To protect your rights, we need to make restrictions that forbid anyone to +deny you these rights or to ask you to surrender the rights. These restrictions +translate to certain responsibilities for you if you distribute copies of +the software, or if you modify it. + +For example, if you distribute copies of such a program, whether gratis or +for a fee, you must give the recipients all the rights that you have. You +must make sure that they, too, receive or can get the source code. And you +must show them these terms so they know their rights. + +We protect your rights with two steps: (1) copyright the software, and (2) +offer you this license which gives you legal permission to copy, distribute +and/or modify the software. + +Also, for each author's protection and ours, we want to make certain that +everyone understands that there is no warranty for this free software. If +the software is modified by someone else and passed on, we want its recipients +to know that what they have is not the original, so that any problems introduced +by others will not reflect on the original authors' reputations. + +Finally, any free program is threatened constantly by software patents. We +wish to avoid the danger that redistributors of a free program will individually +obtain patent licenses, in effect making the program proprietary. To prevent +this, we have made it clear that any patent must be licensed for everyone's +free use or not licensed at all. + +The precise terms and conditions for copying, distribution and modification +follow. + +TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + +0. This License applies to any program or other work which contains a notice +placed by the copyright holder saying it may be distributed under the terms +of this General Public License. The "Program", below, refers to any such program +or work, and a "work based on the Program" means either the Program or any +derivative work under copyright law: that is to say, a work containing the +Program or a portion of it, either verbatim or with modifications and/or translated +into another language. (Hereinafter, translation is included without limitation +in the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not covered +by this License; they are outside its scope. The act of running the Program +is not restricted, and the output from the Program is covered only if its +contents constitute a work based on the Program (independent of having been +made by running the Program). Whether that is true depends on what the Program +does. + +1. You may copy and distribute verbatim copies of the Program's source code +as you receive it, in any medium, provided that you conspicuously and appropriately +publish on each copy an appropriate copyright notice and disclaimer of warranty; +keep intact all the notices that refer to this License and to the absence +of any warranty; and give any other recipients of the Program a copy of this +License along with the Program. + +You may charge a fee for the physical act of transferring a copy, and you +may at your option offer warranty protection in exchange for a fee. + +2. You may modify your copy or copies of the Program or any portion of it, +thus forming a work based on the Program, and copy and distribute such modifications +or work under the terms of Section 1 above, provided that you also meet all +of these conditions: + +a) You must cause the modified files to carry prominent notices stating that +you changed the files and the date of any change. + +b) You must cause any work that you distribute or publish, that in whole or +in part contains or is derived from the Program or any part thereof, to be +licensed as a whole at no charge to all third parties under the terms of this +License. + +c) If the modified program normally reads commands interactively when run, +you must cause it, when started running for such interactive use in the most +ordinary way, to print or display an announcement including an appropriate +copyright notice and a notice that there is no warranty (or else, saying that +you provide a warranty) and that users may redistribute the program under +these conditions, and telling the user how to view a copy of this License. +(Exception: if the Program itself is interactive but does not normally print +such an announcement, your work based on the Program is not required to print +an announcement.) + +These requirements apply to the modified work as a whole. If identifiable +sections of that work are not derived from the Program, and can be reasonably +considered independent and separate works in themselves, then this License, +and its terms, do not apply to those sections when you distribute them as +separate works. But when you distribute the same sections as part of a whole +which is a work based on the Program, the distribution of the whole must be +on the terms of this License, whose permissions for other licensees extend +to the entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest your +rights to work written entirely by you; rather, the intent is to exercise +the right to control the distribution of derivative or collective works based +on the Program. + +In addition, mere aggregation of another work not based on the Program with +the Program (or with a work based on the Program) on a volume of a storage +or distribution medium does not bring the other work under the scope of this +License. + +3. You may copy and distribute the Program (or a work based on it, under Section +2) in object code or executable form under the terms of Sections 1 and 2 above +provided that you also do one of the following: + +a) Accompany it with the complete corresponding machine-readable source code, +which must be distributed under the terms of Sections 1 and 2 above on a medium +customarily used for software interchange; or, + +b) Accompany it with a written offer, valid for at least three years, to give +any third party, for a charge no more than your cost of physically performing +source distribution, a complete machine-readable copy of the corresponding +source code, to be distributed under the terms of Sections 1 and 2 above on +a medium customarily used for software interchange; or, + +c) Accompany it with the information you received as to the offer to distribute +corresponding source code. (This alternative is allowed only for noncommercial +distribution and only if you received the program in object code or executable +form with such an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for making +modifications to it. For an executable work, complete source code means all +the source code for all modules it contains, plus any associated interface +definition files, plus the scripts used to control compilation and installation +of the executable. However, as a special exception, the source code distributed +need not include anything that is normally distributed (in either source or +binary form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component itself +accompanies the executable. + +If distribution of executable or object code is made by offering access to +copy from a designated place, then offering equivalent access to copy the +source code from the same place counts as distribution of the source code, +even though third parties are not compelled to copy the source along with +the object code. + +4. You may not copy, modify, sublicense, or distribute the Program except +as expressly provided under this License. Any attempt otherwise to copy, modify, +sublicense or distribute the Program is void, and will automatically terminate +your rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses terminated +so long as such parties remain in full compliance. + +5. You are not required to accept this License, since you have not signed +it. However, nothing else grants you permission to modify or distribute the +Program or its derivative works. These actions are prohibited by law if you +do not accept this License. Therefore, by modifying or distributing the Program +(or any work based on the Program), you indicate your acceptance of this License +to do so, and all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + +6. Each time you redistribute the Program (or any work based on the Program), +the recipient automatically receives a license from the original licensor +to copy, distribute or modify the Program subject to these terms and conditions. +You may not impose any further restrictions on the recipients' exercise of +the rights granted herein. You are not responsible for enforcing compliance +by third parties to this License. + +7. If, as a consequence of a court judgment or allegation of patent infringement +or for any other reason (not limited to patent issues), conditions are imposed +on you (whether by court order, agreement or otherwise) that contradict the +conditions of this License, they do not excuse you from the conditions of +this License. If you cannot distribute so as to satisfy simultaneously your +obligations under this License and any other pertinent obligations, then as +a consequence you may not distribute the Program at all. For example, if a +patent license would not permit royalty-free redistribution of the Program +by all those who receive copies directly or indirectly through you, then the +only way you could satisfy both it and this License would be to refrain entirely +from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply and +the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any patents +or other property right claims or to contest validity of any such claims; +this section has the sole purpose of protecting the integrity of the free +software distribution system, which is implemented by public license practices. +Many people have made generous contributions to the wide range of software +distributed through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing to +distribute software through any other system and a licensee cannot impose +that choice. + +This section is intended to make thoroughly clear what is believed to be a +consequence of the rest of this License. + +8. If the distribution and/or use of the Program is restricted in certain +countries either by patents or by copyrighted interfaces, the original copyright +holder who places the Program under this License may add an explicit geographical +distribution limitation excluding those countries, so that distribution is +permitted only in or among countries not thus excluded. In such case, this +License incorporates the limitation as if written in the body of this License. + +9. The Free Software Foundation may publish revised and/or new versions of +the General Public License from time to time. Such new versions will be similar +in spirit to the present version, but may differ in detail to address new +problems or concerns. + +Each version is given a distinguishing version number. If the Program specifies +a version number of this License which applies to it and "any later version", +you have the option of following the terms and conditions either of that version +or of any later version published by the Free Software Foundation. If the +Program does not specify a version number of this License, you may choose +any version ever published by the Free Software Foundation. + +10. If you wish to incorporate parts of the Program into other free programs +whose distribution conditions are different, write to the author to ask for +permission. For software which is copyrighted by the Free Software Foundation, +write to the Free Software Foundation; we sometimes make exceptions for this. +Our decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing and reuse +of software generally. + +NO WARRANTY + +11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR +THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE +STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM +"AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, +BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE +OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE +OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA +OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES +OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH +HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +END OF TERMS AND CONDITIONS + +How to Apply These Terms to Your New Programs + +If you develop a new program, and you want it to be of the greatest possible +use to the public, the best way to achieve this is to make it free software +which everyone can redistribute and change under these terms. + +To do so, attach the following notices to the program. It is safest to attach +them to the start of each source file to most effectively convey the exclusion +of warranty; and each file should have at least the "copyright" line and a +pointer to where the full notice is found. + +one line to give the program's name and an idea of what it does. Copyright +(C) yyyy name of author + +This program is free software; you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation; either version 2 of the License, or (at your option) any later +version. + +This program is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + +You should have received a copy of the GNU General Public License along with +this program; if not, write to the Free Software Foundation, Inc., 51 Franklin +Street, Fifth Floor, Boston, MA 02110-1301, USA. Also add information on how +to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this when +it starts in an interactive mode: + +Gnomovision version 69, Copyright (C) year name of author Gnomovision comes +with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, +and you are welcome to redistribute it under certain conditions; type `show +c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may be +called something other than `show w' and `show c'; they could even be mouse-clicks +or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your school, +if any, to sign a "copyright disclaimer" for the program, if necessary. Here +is a sample; alter the names: + +Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' +(which makes passes at compilers) written by James Hacker. + +signature of Ty Coon, 1 April 1989 Ty Coon, President of Vice diff --git a/LICENSES/GPL-2.0-or-later.txt b/LICENSES/GPL-2.0-or-later.txt new file mode 100644 index 0000000..3b6070f --- /dev/null +++ b/LICENSES/GPL-2.0-or-later.txt @@ -0,0 +1,311 @@ +GNU GENERAL PUBLIC LICENSE +Version 2, June 1991 + +Copyright (C) 1989, 1991 Free Software Foundation, Inc. +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + +Everyone is permitted to copy and distribute verbatim copies of this license +document, but changing it is not allowed. + +Preamble + +The licenses for most software are designed to take away your freedom to share +and change it. By contrast, the GNU General Public License is intended to +guarantee your freedom to share and change free software--to make sure the +software is free for all its users. This General Public License applies to +most of the Free Software Foundation's software and to any other program whose +authors commit to using it. (Some other Free Software Foundation software +is covered by the GNU Lesser General Public License instead.) You can apply +it to your programs, too. + +When we speak of free software, we are referring to freedom, not price. Our +General Public Licenses are designed to make sure that you have the freedom +to distribute copies of free software (and charge for this service if you +wish), that you receive source code or can get it if you want it, that you +can change the software or use pieces of it in new free programs; and that +you know you can do these things. + +To protect your rights, we need to make restrictions that forbid anyone to +deny you these rights or to ask you to surrender the rights. These restrictions +translate to certain responsibilities for you if you distribute copies of +the software, or if you modify it. + +For example, if you distribute copies of such a program, whether gratis or +for a fee, you must give the recipients all the rights that you have. You +must make sure that they, too, receive or can get the source code. And you +must show them these terms so they know their rights. + +We protect your rights with two steps: (1) copyright the software, and (2) +offer you this license which gives you legal permission to copy, distribute +and/or modify the software. + +Also, for each author's protection and ours, we want to make certain that +everyone understands that there is no warranty for this free software. If +the software is modified by someone else and passed on, we want its recipients +to know that what they have is not the original, so that any problems introduced +by others will not reflect on the original authors' reputations. + +Finally, any free program is threatened constantly by software patents. We +wish to avoid the danger that redistributors of a free program will individually +obtain patent licenses, in effect making the program proprietary. To prevent +this, we have made it clear that any patent must be licensed for everyone's +free use or not licensed at all. + +The precise terms and conditions for copying, distribution and modification +follow. + +TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + +0. This License applies to any program or other work which contains a notice +placed by the copyright holder saying it may be distributed under the terms +of this General Public License. The "Program", below, refers to any such program +or work, and a "work based on the Program" means either the Program or any +derivative work under copyright law: that is to say, a work containing the +Program or a portion of it, either verbatim or with modifications and/or translated +into another language. (Hereinafter, translation is included without limitation +in the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not covered +by this License; they are outside its scope. The act of running the Program +is not restricted, and the output from the Program is covered only if its +contents constitute a work based on the Program (independent of having been +made by running the Program). Whether that is true depends on what the Program +does. + +1. You may copy and distribute verbatim copies of the Program's source code +as you receive it, in any medium, provided that you conspicuously and appropriately +publish on each copy an appropriate copyright notice and disclaimer of warranty; +keep intact all the notices that refer to this License and to the absence +of any warranty; and give any other recipients of the Program a copy of this +License along with the Program. + +You may charge a fee for the physical act of transferring a copy, and you +may at your option offer warranty protection in exchange for a fee. + +2. You may modify your copy or copies of the Program or any portion of it, +thus forming a work based on the Program, and copy and distribute such modifications +or work under the terms of Section 1 above, provided that you also meet all +of these conditions: + +a) You must cause the modified files to carry prominent notices stating that +you changed the files and the date of any change. + +b) You must cause any work that you distribute or publish, that in whole or +in part contains or is derived from the Program or any part thereof, to be +licensed as a whole at no charge to all third parties under the terms of this +License. + +c) If the modified program normally reads commands interactively when run, +you must cause it, when started running for such interactive use in the most +ordinary way, to print or display an announcement including an appropriate +copyright notice and a notice that there is no warranty (or else, saying that +you provide a warranty) and that users may redistribute the program under +these conditions, and telling the user how to view a copy of this License. +(Exception: if the Program itself is interactive but does not normally print +such an announcement, your work based on the Program is not required to print +an announcement.) + +These requirements apply to the modified work as a whole. If identifiable +sections of that work are not derived from the Program, and can be reasonably +considered independent and separate works in themselves, then this License, +and its terms, do not apply to those sections when you distribute them as +separate works. But when you distribute the same sections as part of a whole +which is a work based on the Program, the distribution of the whole must be +on the terms of this License, whose permissions for other licensees extend +to the entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest your +rights to work written entirely by you; rather, the intent is to exercise +the right to control the distribution of derivative or collective works based +on the Program. + +In addition, mere aggregation of another work not based on the Program with +the Program (or with a work based on the Program) on a volume of a storage +or distribution medium does not bring the other work under the scope of this +License. + +3. You may copy and distribute the Program (or a work based on it, under Section +2) in object code or executable form under the terms of Sections 1 and 2 above +provided that you also do one of the following: + +a) Accompany it with the complete corresponding machine-readable source code, +which must be distributed under the terms of Sections 1 and 2 above on a medium +customarily used for software interchange; or, + +b) Accompany it with a written offer, valid for at least three years, to give +any third party, for a charge no more than your cost of physically performing +source distribution, a complete machine-readable copy of the corresponding +source code, to be distributed under the terms of Sections 1 and 2 above on +a medium customarily used for software interchange; or, + +c) Accompany it with the information you received as to the offer to distribute +corresponding source code. (This alternative is allowed only for noncommercial +distribution and only if you received the program in object code or executable +form with such an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for making +modifications to it. For an executable work, complete source code means all +the source code for all modules it contains, plus any associated interface +definition files, plus the scripts used to control compilation and installation +of the executable. However, as a special exception, the source code distributed +need not include anything that is normally distributed (in either source or +binary form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component itself +accompanies the executable. + +If distribution of executable or object code is made by offering access to +copy from a designated place, then offering equivalent access to copy the +source code from the same place counts as distribution of the source code, +even though third parties are not compelled to copy the source along with +the object code. + +4. You may not copy, modify, sublicense, or distribute the Program except +as expressly provided under this License. Any attempt otherwise to copy, modify, +sublicense or distribute the Program is void, and will automatically terminate +your rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses terminated +so long as such parties remain in full compliance. + +5. You are not required to accept this License, since you have not signed +it. However, nothing else grants you permission to modify or distribute the +Program or its derivative works. These actions are prohibited by law if you +do not accept this License. Therefore, by modifying or distributing the Program +(or any work based on the Program), you indicate your acceptance of this License +to do so, and all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + +6. Each time you redistribute the Program (or any work based on the Program), +the recipient automatically receives a license from the original licensor +to copy, distribute or modify the Program subject to these terms and conditions. +You may not impose any further restrictions on the recipients' exercise of +the rights granted herein. You are not responsible for enforcing compliance +by third parties to this License. + +7. If, as a consequence of a court judgment or allegation of patent infringement +or for any other reason (not limited to patent issues), conditions are imposed +on you (whether by court order, agreement or otherwise) that contradict the +conditions of this License, they do not excuse you from the conditions of +this License. If you cannot distribute so as to satisfy simultaneously your +obligations under this License and any other pertinent obligations, then as +a consequence you may not distribute the Program at all. For example, if a +patent license would not permit royalty-free redistribution of the Program +by all those who receive copies directly or indirectly through you, then the +only way you could satisfy both it and this License would be to refrain entirely +from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply and +the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any patents +or other property right claims or to contest validity of any such claims; +this section has the sole purpose of protecting the integrity of the free +software distribution system, which is implemented by public license practices. +Many people have made generous contributions to the wide range of software +distributed through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing to +distribute software through any other system and a licensee cannot impose +that choice. + +This section is intended to make thoroughly clear what is believed to be a +consequence of the rest of this License. + +8. If the distribution and/or use of the Program is restricted in certain +countries either by patents or by copyrighted interfaces, the original copyright +holder who places the Program under this License may add an explicit geographical +distribution limitation excluding those countries, so that distribution is +permitted only in or among countries not thus excluded. In such case, this +License incorporates the limitation as if written in the body of this License. + +9. The Free Software Foundation may publish revised and/or new versions of +the General Public License from time to time. Such new versions will be similar +in spirit to the present version, but may differ in detail to address new +problems or concerns. + +Each version is given a distinguishing version number. If the Program specifies +a version number of this License which applies to it and "any later version", +you have the option of following the terms and conditions either of that version +or of any later version published by the Free Software Foundation. If the +Program does not specify a version number of this License, you may choose +any version ever published by the Free Software Foundation. + +10. If you wish to incorporate parts of the Program into other free programs +whose distribution conditions are different, write to the author to ask for +permission. For software which is copyrighted by the Free Software Foundation, +write to the Free Software Foundation; we sometimes make exceptions for this. +Our decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing and reuse +of software generally. + +NO WARRANTY + +11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR +THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE +STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM +"AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, +BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE +OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE +OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA +OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES +OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH +HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +END OF TERMS AND CONDITIONS + +How to Apply These Terms to Your New Programs + +If you develop a new program, and you want it to be of the greatest possible +use to the public, the best way to achieve this is to make it free software +which everyone can redistribute and change under these terms. + +To do so, attach the following notices to the program. It is safest to attach +them to the start of each source file to most effectively convey the exclusion +of warranty; and each file should have at least the "copyright" line and a +pointer to where the full notice is found. + +one line to give the program's name and an idea of what it does. Copyright +(C) yyyy name of author + +This program is free software; you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation; either version 2 of the License, or (at your option) any later +version. + +This program is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + +You should have received a copy of the GNU General Public License along with +this program; if not, write to the Free Software Foundation, Inc., 51 Franklin +Street, Fifth Floor, Boston, MA 02110-1301, USA. Also add information on how +to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this when +it starts in an interactive mode: + +Gnomovision version 69, Copyright (C) year name of author Gnomovision comes +with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, +and you are welcome to redistribute it under certain conditions; type `show +c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may be +called something other than `show w' and `show c'; they could even be mouse-clicks +or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your school, +if any, to sign a "copyright disclaimer" for the program, if necessary. Here +is a sample; alter the names: + +Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' +(which makes passes at compilers) written by James Hacker. + +signature of Ty Coon, 1 April 1989 Ty Coon, President of Vice diff --git a/LICENSES/GPL-3.0-only.txt b/LICENSES/GPL-3.0-only.txt new file mode 100644 index 0000000..5990771 --- /dev/null +++ b/LICENSES/GPL-3.0-only.txt @@ -0,0 +1,604 @@ +GNU GENERAL PUBLIC LICENSE +Version 3, 29 June 2007 + +Copyright © 2007 Free Software Foundation, Inc. + +Everyone is permitted to copy and distribute verbatim copies of this license +document, but changing it is not allowed. + +Preamble + +The GNU General Public License is a free, copyleft license for software and +other kinds of works. + +The licenses for most software and other practical works are designed to take +away your freedom to share and change the works. By contrast, the GNU General +Public License is intended to guarantee your freedom to share and change all +versions of a program--to make sure it remains free software for all its users. +We, the Free Software Foundation, use the GNU General Public License for most +of our software; it applies also to any other work released this way by its +authors. You can apply it to your programs, too. + +When we speak of free software, we are referring to freedom, not price. Our +General Public Licenses are designed to make sure that you have the freedom +to distribute copies of free software (and charge for them if you wish), that +you receive source code or can get it if you want it, that you can change +the software or use pieces of it in new free programs, and that you know you +can do these things. + +To protect your rights, we need to prevent others from denying you these rights +or asking you to surrender the rights. Therefore, you have certain responsibilities +if you distribute copies of the software, or if you modify it: responsibilities +to respect the freedom of others. + +For example, if you distribute copies of such a program, whether gratis or +for a fee, you must pass on to the recipients the same freedoms that you received. +You must make sure that they, too, receive or can get the source code. And +you must show them these terms so they know their rights. + +Developers that use the GNU GPL protect your rights with two steps: (1) assert +copyright on the software, and (2) offer you this License giving you legal +permission to copy, distribute and/or modify it. + +For the developers' and authors' protection, the GPL clearly explains that +there is no warranty for this free software. For both users' and authors' +sake, the GPL requires that modified versions be marked as changed, so that +their problems will not be attributed erroneously to authors of previous versions. + +Some devices are designed to deny users access to install or run modified +versions of the software inside them, although the manufacturer can do so. +This is fundamentally incompatible with the aim of protecting users' freedom +to change the software. The systematic pattern of such abuse occurs in the +area of products for individuals to use, which is precisely where it is most +unacceptable. Therefore, we have designed this version of the GPL to prohibit +the practice for those products. If such problems arise substantially in other +domains, we stand ready to extend this provision to those domains in future +versions of the GPL, as needed to protect the freedom of users. + +Finally, every program is threatened constantly by software patents. States +should not allow patents to restrict development and use of software on general-purpose +computers, but in those that do, we wish to avoid the special danger that +patents applied to a free program could make it effectively proprietary. To +prevent this, the GPL assures that patents cannot be used to render the program +non-free. + +The precise terms and conditions for copying, distribution and modification +follow. + +TERMS AND CONDITIONS + +0. Definitions. + +“This License” refers to version 3 of the GNU General Public License. + +“Copyright” also means copyright-like laws that apply to other kinds of works, +such as semiconductor masks. + +“The Program” refers to any copyrightable work licensed under this License. +Each licensee is addressed as “you”. “Licensees” and “recipients” may be individuals +or organizations. + +To “modify” a work means to copy from or adapt all or part of the work in +a fashion requiring copyright permission, other than the making of an exact +copy. The resulting work is called a “modified version” of the earlier work +or a work “based on” the earlier work. + +A “covered work” means either the unmodified Program or a work based on the +Program. + +To “propagate” a work means to do anything with it that, without permission, +would make you directly or secondarily liable for infringement under applicable +copyright law, except executing it on a computer or modifying a private copy. +Propagation includes copying, distribution (with or without modification), +making available to the public, and in some countries other activities as +well. + +To “convey” a work means any kind of propagation that enables other parties +to make or receive copies. Mere interaction with a user through a computer +network, with no transfer of a copy, is not conveying. + +An interactive user interface displays “Appropriate Legal Notices” to the +extent that it includes a convenient and prominently visible feature that +(1) displays an appropriate copyright notice, and (2) tells the user that +there is no warranty for the work (except to the extent that warranties are +provided), that licensees may convey the work under this License, and how +to view a copy of this License. If the interface presents a list of user commands +or options, such as a menu, a prominent item in the list meets this criterion. + +1. Source Code. +The “source code” for a work means the preferred form of the work for making +modifications to it. “Object code” means any non-source form of a work. + +A “Standard Interface” means an interface that either is an official standard +defined by a recognized standards body, or, in the case of interfaces specified +for a particular programming language, one that is widely used among developers +working in that language. + +The “System Libraries” of an executable work include anything, other than +the work as a whole, that (a) is included in the normal form of packaging +a Major Component, but which is not part of that Major Component, and (b) +serves only to enable use of the work with that Major Component, or to implement +a Standard Interface for which an implementation is available to the public +in source code form. A “Major Component”, in this context, means a major essential +component (kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to produce +the work, or an object code interpreter used to run it. + +The “Corresponding Source” for a work in object code form means all the source +code needed to generate, install, and (for an executable work) run the object +code and to modify the work, including scripts to control those activities. +However, it does not include the work's System Libraries, or general-purpose +tools or generally available free programs which are used unmodified in performing +those activities but which are not part of the work. For example, Corresponding +Source includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically linked +subprograms that the work is specifically designed to require, such as by +intimate data communication or control flow between those subprograms and +other parts of the work. + +The Corresponding Source need not include anything that users can regenerate +automatically from other parts of the Corresponding Source. + +The Corresponding Source for a work in source code form is that same work. + +2. Basic Permissions. +All rights granted under this License are granted for the term of copyright +on the Program, and are irrevocable provided the stated conditions are met. +This License explicitly affirms your unlimited permission to run the unmodified +Program. The output from running a covered work is covered by this License +only if the output, given its content, constitutes a covered work. This License +acknowledges your rights of fair use or other equivalent, as provided by copyright +law. + +You may make, run and propagate covered works that you do not convey, without +conditions so long as your license otherwise remains in force. You may convey +covered works to others for the sole purpose of having them make modifications +exclusively for you, or provide you with facilities for running those works, +provided that you comply with the terms of this License in conveying all material +for which you do not control copyright. Those thus making or running the covered +works for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of your copyrighted +material outside their relationship with you. + +Conveying under any other circumstances is permitted solely under the conditions +stated below. Sublicensing is not allowed; section 10 makes it unnecessary. + +3. Protecting Users' Legal Rights From Anti-Circumvention Law. +No covered work shall be deemed part of an effective technological measure +under any applicable law fulfilling obligations under article 11 of the WIPO +copyright treaty adopted on 20 December 1996, or similar laws prohibiting +or restricting circumvention of such measures. + +When you convey a covered work, you waive any legal power to forbid circumvention +of technological measures to the extent such circumvention is effected by +exercising rights under this License with respect to the covered work, and +you disclaim any intention to limit operation or modification of the work +as a means of enforcing, against the work's users, your or third parties' +legal rights to forbid circumvention of technological measures. + +4. Conveying Verbatim Copies. +You may convey verbatim copies of the Program's source code as you receive +it, in any medium, provided that you conspicuously and appropriately publish +on each copy an appropriate copyright notice; keep intact all notices stating +that this License and any non-permissive terms added in accord with section +7 apply to the code; keep intact all notices of the absence of any warranty; +and give all recipients a copy of this License along with the Program. + +You may charge any price or no price for each copy that you convey, and you +may offer support or warranty protection for a fee. + +5. Conveying Modified Source Versions. +You may convey a work based on the Program, or the modifications to produce +it from the Program, in the form of source code under the terms of section +4, provided that you also meet all of these conditions: + +a) The work must carry prominent notices stating that you modified it, and +giving a relevant date. + +b) The work must carry prominent notices stating that it is released under +this License and any conditions added under section 7. This requirement modifies +the requirement in section 4 to “keep intact all notices”. + +c) You must license the entire work, as a whole, under this License to anyone +who comes into possession of a copy. This License will therefore apply, along +with any applicable section 7 additional terms, to the whole of the work, +and all its parts, regardless of how they are packaged. This License gives +no permission to license the work in any other way, but it does not invalidate +such permission if you have separately received it. + +d) If the work has interactive user interfaces, each must display Appropriate +Legal Notices; however, if the Program has interactive interfaces that do +not display Appropriate Legal Notices, your work need not make them do so. + +A compilation of a covered work with other separate and independent works, +which are not by their nature extensions of the covered work, and which are +not combined with it such as to form a larger program, in or on a volume of +a storage or distribution medium, is called an “aggregate” if the compilation +and its resulting copyright are not used to limit the access or legal rights +of the compilation's users beyond what the individual works permit. Inclusion +of a covered work in an aggregate does not cause this License to apply to +the other parts of the aggregate. + +6. Conveying Non-Source Forms. +You may convey a covered work in object code form under the terms of sections +4 and 5, provided that you also convey the machine-readable Corresponding +Source under the terms of this License, in one of these ways: + +a) Convey the object code in, or embodied in, a physical product (including +a physical distribution medium), accompanied by the Corresponding Source fixed +on a durable physical medium customarily used for software interchange. + +b) Convey the object code in, or embodied in, a physical product (including +a physical distribution medium), accompanied by a written offer, valid for +at least three years and valid for as long as you offer spare parts or customer +support for that product model, to give anyone who possesses the object code +either (1) a copy of the Corresponding Source for all the software in the +product that is covered by this License, on a durable physical medium customarily +used for software interchange, for a price no more than your reasonable cost +of physically performing this conveying of source, or (2) access to copy the +Corresponding Source from a network server at no charge. + +c) Convey individual copies of the object code with a copy of the written +offer to provide the Corresponding Source. This alternative is allowed only +occasionally and noncommercially, and only if you received the object code +with such an offer, in accord with subsection 6b. + +d) Convey the object code by offering access from a designated place (gratis +or for a charge), and offer equivalent access to the Corresponding Source +in the same way through the same place at no further charge. You need not +require recipients to copy the Corresponding Source along with the object +code. If the place to copy the object code is a network server, the Corresponding +Source may be on a different server (operated by you or a third party) that +supports equivalent copying facilities, provided you maintain clear directions +next to the object code saying where to find the Corresponding Source. Regardless +of what server hosts the Corresponding Source, you remain obligated to ensure +that it is available for as long as needed to satisfy these requirements. + +e) Convey the object code using peer-to-peer transmission, provided you inform +other peers where the object code and Corresponding Source of the work are +being offered to the general public at no charge under subsection 6d. + +A separable portion of the object code, whose source code is excluded from +the Corresponding Source as a System Library, need not be included in conveying +the object code work. + +A “User Product” is either (1) a “consumer product”, which means any tangible +personal property which is normally used for personal, family, or household +purposes, or (2) anything designed or sold for incorporation into a dwelling. +In determining whether a product is a consumer product, doubtful cases shall +be resolved in favor of coverage. For a particular product received by a particular +user, “normally used” refers to a typical or common use of that class of product, +regardless of the status of the particular user or of the way in which the +particular user actually uses, or expects or is expected to use, the product. +A product is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent the +only significant mode of use of the product. + +“Installation Information” for a User Product means any methods, procedures, +authorization keys, or other information required to install and execute modified +versions of a covered work in that User Product from a modified version of +its Corresponding Source. The information must suffice to ensure that the +continued functioning of the modified object code is in no case prevented +or interfered with solely because modification has been made. + +If you convey an object code work under this section in, or with, or specifically +for use in, a User Product, and the conveying occurs as part of a transaction +in which the right of possession and use of the User Product is transferred +to the recipient in perpetuity or for a fixed term (regardless of how the +transaction is characterized), the Corresponding Source conveyed under this +section must be accompanied by the Installation Information. But this requirement +does not apply if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has been installed +in ROM). + +The requirement to provide Installation Information does not include a requirement +to continue to provide support service, warranty, or updates for a work that +has been modified or installed by the recipient, or for the User Product in +which it has been modified or installed. Access to a network may be denied +when the modification itself materially and adversely affects the operation +of the network or violates the rules and protocols for communication across +the network. + +Corresponding Source conveyed, and Installation Information provided, in accord +with this section must be in a format that is publicly documented (and with +an implementation available to the public in source code form), and must require +no special password or key for unpacking, reading or copying. + +7. Additional Terms. +“Additional permissions” are terms that supplement the terms of this License +by making exceptions from one or more of its conditions. Additional permissions +that are applicable to the entire Program shall be treated as though they +were included in this License, to the extent that they are valid under applicable +law. If additional permissions apply only to part of the Program, that part +may be used separately under those permissions, but the entire Program remains +governed by this License without regard to the additional permissions. + +When you convey a copy of a covered work, you may at your option remove any +additional permissions from that copy, or from any part of it. (Additional +permissions may be written to require their own removal in certain cases when +you modify the work.) You may place additional permissions on material, added +by you to a covered work, for which you have or can give appropriate copyright +permission. + +Notwithstanding any other provision of this License, for material you add +to a covered work, you may (if authorized by the copyright holders of that +material) supplement the terms of this License with terms: + +a) Disclaiming warranty or limiting liability differently from the terms of +sections 15 and 16 of this License; or + +b) Requiring preservation of specified reasonable legal notices or author +attributions in that material or in the Appropriate Legal Notices displayed +by works containing it; or + +c) Prohibiting misrepresentation of the origin of that material, or requiring +that modified versions of such material be marked in reasonable ways as different +from the original version; or + +d) Limiting the use for publicity purposes of names of licensors or authors +of the material; or + +e) Declining to grant rights under trademark law for use of some trade names, +trademarks, or service marks; or + +f) Requiring indemnification of licensors and authors of that material by +anyone who conveys the material (or modified versions of it) with contractual +assumptions of liability to the recipient, for any liability that these contractual +assumptions directly impose on those licensors and authors. + +All other non-permissive additional terms are considered “further restrictions” +within the meaning of section 10. If the Program as you received it, or any +part of it, contains a notice stating that it is governed by this License +along with a term that is a further restriction, you may remove that term. +If a license document contains a further restriction but permits relicensing +or conveying under this License, you may add to a covered work material governed +by the terms of that license document, provided that the further restriction +does not survive such relicensing or conveying. + +If you add terms to a covered work in accord with this section, you must place, +in the relevant source files, a statement of the additional terms that apply +to those files, or a notice indicating where to find the applicable terms. + +Additional terms, permissive or non-permissive, may be stated in the form +of a separately written license, or stated as exceptions; the above requirements +apply either way. + +8. Termination. +You may not propagate or modify a covered work except as expressly provided +under this License. Any attempt otherwise to propagate or modify it is void, +and will automatically terminate your rights under this License (including +any patent licenses granted under the third paragraph of section 11). + +However, if you cease all violation of this License, then your license from +a particular copyright holder is reinstated (a) provisionally, unless and +until the copyright holder explicitly and finally terminates your license, +and (b) permanently, if the copyright holder fails to notify you of the violation +by some reasonable means prior to 60 days after the cessation. + +Moreover, your license from a particular copyright holder is reinstated permanently +if the copyright holder notifies you of the violation by some reasonable means, +this is the first time you have received notice of violation of this License +(for any work) from that copyright holder, and you cure the violation prior +to 30 days after your receipt of the notice. + +Termination of your rights under this section does not terminate the licenses +of parties who have received copies or rights from you under this License. +If your rights have been terminated and not permanently reinstated, you do +not qualify to receive new licenses for the same material under section 10. + +9. Acceptance Not Required for Having Copies. +You are not required to accept this License in order to receive or run a copy +of the Program. Ancillary propagation of a covered work occurring solely as +a consequence of using peer-to-peer transmission to receive a copy likewise +does not require acceptance. However, nothing other than this License grants +you permission to propagate or modify any covered work. These actions infringe +copyright if you do not accept this License. Therefore, by modifying or propagating +a covered work, you indicate your acceptance of this License to do so. + +10. Automatic Licensing of Downstream Recipients. +Each time you convey a covered work, the recipient automatically receives +a license from the original licensors, to run, modify and propagate that work, +subject to this License. You are not responsible for enforcing compliance +by third parties with this License. + +An “entity transaction” is a transaction transferring control of an organization, +or substantially all assets of one, or subdividing an organization, or merging +organizations. If propagation of a covered work results from an entity transaction, +each party to that transaction who receives a copy of the work also receives +whatever licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the Corresponding +Source of the work from the predecessor in interest, if the predecessor has +it or can get it with reasonable efforts. + +You may not impose any further restrictions on the exercise of the rights +granted or affirmed under this License. For example, you may not impose a +license fee, royalty, or other charge for exercise of rights granted under +this License, and you may not initiate litigation (including a cross-claim +or counterclaim in a lawsuit) alleging that any patent claim is infringed +by making, using, selling, offering for sale, or importing the Program or +any portion of it. + +11. Patents. +A “contributor” is a copyright holder who authorizes use under this License +of the Program or a work on which the Program is based. The work thus licensed +is called the contributor's “contributor version”. + +A contributor's “essential patent claims” are all patent claims owned or controlled +by the contributor, whether already acquired or hereafter acquired, that would +be infringed by some manner, permitted by this License, of making, using, +or selling its contributor version, but do not include claims that would be +infringed only as a consequence of further modification of the contributor +version. For purposes of this definition, “control” includes the right to +grant patent sublicenses in a manner consistent with the requirements of this +License. + +Each contributor grants you a non-exclusive, worldwide, royalty-free patent +license under the contributor's essential patent claims, to make, use, sell, +offer for sale, import and otherwise run, modify and propagate the contents +of its contributor version. + +In the following three paragraphs, a “patent license” is any express agreement +or commitment, however denominated, not to enforce a patent (such as an express +permission to practice a patent or covenant not to sue for patent infringement). +To “grant” such a patent license to a party means to make such an agreement +or commitment not to enforce a patent against the party. + +If you convey a covered work, knowingly relying on a patent license, and the +Corresponding Source of the work is not available for anyone to copy, free +of charge and under the terms of this License, through a publicly available +network server or other readily accessible means, then you must either (1) +cause the Corresponding Source to be so available, or (2) arrange to deprive +yourself of the benefit of the patent license for this particular work, or +(3) arrange, in a manner consistent with the requirements of this License, +to extend the patent license to downstream recipients. “Knowingly relying” +means you have actual knowledge that, but for the patent license, your conveying +the covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that country +that you have reason to believe are valid. + +If, pursuant to or in connection with a single transaction or arrangement, +you convey, or propagate by procuring conveyance of, a covered work, and grant +a patent license to some of the parties receiving the covered work authorizing +them to use, propagate, modify or convey a specific copy of the covered work, +then the patent license you grant is automatically extended to all recipients +of the covered work and works based on it. + +A patent license is “discriminatory” if it does not include within the scope +of its coverage, prohibits the exercise of, or is conditioned on the non-exercise +of one or more of the rights that are specifically granted under this License. +You may not convey a covered work if you are a party to an arrangement with +a third party that is in the business of distributing software, under which +you make payment to the third party based on the extent of your activity of +conveying the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory patent +license (a) in connection with copies of the covered work conveyed by you +(or copies made from those copies), or (b) primarily for and in connection +with specific products or compilations that contain the covered work, unless +you entered into that arrangement, or that patent license was granted, prior +to 28 March 2007. + +Nothing in this License shall be construed as excluding or limiting any implied +license or other defenses to infringement that may otherwise be available +to you under applicable patent law. + +12. No Surrender of Others' Freedom. +If conditions are imposed on you (whether by court order, agreement or otherwise) +that contradict the conditions of this License, they do not excuse you from +the conditions of this License. If you cannot convey a covered work so as +to satisfy simultaneously your obligations under this License and any other +pertinent obligations, then as a consequence you may not convey it at all. +For example, if you agree to terms that obligate you to collect a royalty +for further conveying from those to whom you convey the Program, the only +way you could satisfy both those terms and this License would be to refrain +entirely from conveying the Program. + +13. Use with the GNU Affero General Public License. +Notwithstanding any other provision of this License, you have permission to +link or combine any covered work with a work licensed under version 3 of the +GNU Affero General Public License into a single combined work, and to convey +the resulting work. The terms of this License will continue to apply to the +part which is the covered work, but the special requirements of the GNU Affero +General Public License, section 13, concerning interaction through a network +will apply to the combination as such. + +14. Revised Versions of this License. +The Free Software Foundation may publish revised and/or new versions of the +GNU General Public License from time to time. Such new versions will be similar +in spirit to the present version, but may differ in detail to address new +problems or concerns. + +Each version is given a distinguishing version number. If the Program specifies +that a certain numbered version of the GNU General Public License “or any +later version” applies to it, you have the option of following the terms and +conditions either of that numbered version or of any later version published +by the Free Software Foundation. If the Program does not specify a version +number of the GNU General Public License, you may choose any version ever +published by the Free Software Foundation. + +If the Program specifies that a proxy can decide which future versions of +the GNU General Public License can be used, that proxy's public statement +of acceptance of a version permanently authorizes you to choose that version +for the Program. + +Later license versions may give you additional or different permissions. However, +no additional obligations are imposed on any author or copyright holder as +a result of your choosing to follow a later version. + +15. Disclaimer of Warranty. +THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE +LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE PROGRAM “AS IS” WITHOUT WARRANTY OF ANY KIND, EITHER +EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM +PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR +CORRECTION. + +16. Limitation of Liability. +IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL +ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM +AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, +INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO +USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED +INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE +PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER +PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +17. Interpretation of Sections 15 and 16. +If the disclaimer of warranty and limitation of liability provided above cannot +be given local legal effect according to their terms, reviewing courts shall +apply local law that most closely approximates an absolute waiver of all civil +liability in connection with the Program, unless a warranty or assumption +of liability accompanies a copy of the Program in return for a fee. + +END OF TERMS AND CONDITIONS + +How to Apply These Terms to Your New Programs + +If you develop a new program, and you want it to be of the greatest possible +use to the public, the best way to achieve this is to make it free software +which everyone can redistribute and change under these terms. + +To do so, attach the following notices to the program. It is safest to attach +them to the start of each source file to most effectively state the exclusion +of warranty; and each file should have at least the “copyright” line and a +pointer to where the full notice is found. + + + Copyright (C) + +This program is free software: you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation, either version 3 of the License, or (at your option) any later +version. + +This program is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + +You should have received a copy of the GNU General Public License along with +this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + +If the program does terminal interaction, make it output a short notice like +this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. +This is free software, and you are welcome to redistribute it under certain +conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands might +be different; for a GUI interface, you would use an “about box”. + +You should also get your employer (if you work as a programmer) or school, +if any, to sign a “copyright disclaimer” for the program, if necessary. For +more information on this, and how to apply and follow the GNU GPL, see . + +The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General Public +License instead of this License. But first, please read . diff --git a/LICENSES/LGPL-2.0-only.txt b/LICENSES/LGPL-2.0-only.txt new file mode 100644 index 0000000..ec9eedc --- /dev/null +++ b/LICENSES/LGPL-2.0-only.txt @@ -0,0 +1,444 @@ +GNU LIBRARY GENERAL PUBLIC LICENSE + +Version 2, June 1991 + +Copyright (C) 1991 Free Software Foundation, Inc. +51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA + +Everyone is permitted to copy and distribute verbatim copies of this license +document, but changing it is not allowed. + +[This is the first released version of the library GPL. It is numbered 2 +because it goes with version 2 of the ordinary GPL.] + +Preamble + +The licenses for most software are designed to take away your freedom to share +and change it. By contrast, the GNU General Public Licenses are intended to +guarantee your freedom to share and change free software--to make sure the +software is free for all its users. + +This license, the Library General Public License, applies to some specially +designated Free Software Foundation software, and to any other libraries whose +authors decide to use it. You can use it for your libraries, too. + +When we speak of free software, we are referring to freedom, not price. Our +General Public Licenses are designed to make sure that you have the freedom +to distribute copies of free software (and charge for this service if you +wish), that you receive source code or can get it if you want it, that you +can change the software or use pieces of it in new free programs; and that +you know you can do these things. + +To protect your rights, we need to make restrictions that forbid anyone to +deny you these rights or to ask you to surrender the rights. These restrictions +translate to certain responsibilities for you if you distribute copies of +the library, or if you modify it. + +For example, if you distribute copies of the library, whether gratis or for +a fee, you must give the recipients all the rights that we gave you. You must +make sure that they, too, receive or can get the source code. If you link +a program with the library, you must provide complete object files to the +recipients so that they can relink them with the library, after making changes +to the library and recompiling it. And you must show them these terms so they +know their rights. + +Our method of protecting your rights has two steps: (1) copyright the library, +and (2) offer you this license which gives you legal permission to copy, distribute +and/or modify the library. + +Also, for each distributor's protection, we want to make certain that everyone +understands that there is no warranty for this free library. If the library +is modified by someone else and passed on, we want its recipients to know +that what they have is not the original version, so that any problems introduced +by others will not reflect on the original authors' reputations. + +Finally, any free program is threatened constantly by software patents. We +wish to avoid the danger that companies distributing free software will individually +obtain patent licenses, thus in effect transforming the program into proprietary +software. To prevent this, we have made it clear that any patent must be licensed +for everyone's free use or not licensed at all. + +Most GNU software, including some libraries, is covered by the ordinary GNU +General Public License, which was designed for utility programs. This license, +the GNU Library General Public License, applies to certain designated libraries. +This license is quite different from the ordinary one; be sure to read it +in full, and don't assume that anything in it is the same as in the ordinary +license. + +The reason we have a separate public license for some libraries is that they +blur the distinction we usually make between modifying or adding to a program +and simply using it. Linking a program with a library, without changing the +library, is in some sense simply using the library, and is analogous to running +a utility program or application program. However, in a textual and legal +sense, the linked executable is a combined work, a derivative of the original +library, and the ordinary General Public License treats it as such. + +Because of this blurred distinction, using the ordinary General Public License +for libraries did not effectively promote software sharing, because most developers +did not use the libraries. We concluded that weaker conditions might promote +sharing better. + +However, unrestricted linking of non-free programs would deprive the users +of those programs of all benefit from the free status of the libraries themselves. +This Library General Public License is intended to permit developers of non-free +programs to use free libraries, while preserving your freedom as a user of +such programs to change the free libraries that are incorporated in them. +(We have not seen how to achieve this as regards changes in header files, +but we have achieved it as regards changes in the actual functions of the +Library.) The hope is that this will lead to faster development of free libraries. + +The precise terms and conditions for copying, distribution and modification +follow. Pay close attention to the difference between a "work based on the +library" and a "work that uses the library". The former contains code derived +from the library, while the latter only works together with the library. + +Note that it is possible for a library to be covered by the ordinary General +Public License rather than by this special one. + +TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + +0. This License Agreement applies to any software library which contains a +notice placed by the copyright holder or other authorized party saying it +may be distributed under the terms of this Library General Public License +(also called "this License"). Each licensee is addressed as "you". + +A "library" means a collection of software functions and/or data prepared +so as to be conveniently linked with application programs (which use some +of those functions and data) to form executables. + +The "Library", below, refers to any such software library or work which has +been distributed under these terms. A "work based on the Library" means either +the Library or any derivative work under copyright law: that is to say, a +work containing the Library or a portion of it, either verbatim or with modifications +and/or translated straightforwardly into another language. (Hereinafter, translation +is included without limitation in the term "modification".) + +"Source code" for a work means the preferred form of the work for making modifications +to it. For a library, complete source code means all the source code for all +modules it contains, plus any associated interface definition files, plus +the scripts used to control compilation and installation of the library. + +Activities other than copying, distribution and modification are not covered +by this License; they are outside its scope. The act of running a program +using the Library is not restricted, and output from such a program is covered +only if its contents constitute a work based on the Library (independent of +the use of the Library in a tool for writing it). Whether that is true depends +on what the Library does and what the program that uses the Library does. + +1. You may copy and distribute verbatim copies of the Library's complete source +code as you receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice and disclaimer +of warranty; keep intact all the notices that refer to this License and to +the absence of any warranty; and distribute a copy of this License along with +the Library. + +You may charge a fee for the physical act of transferring a copy, and you +may at your option offer warranty protection in exchange for a fee. + +2. You may modify your copy or copies of the Library or any portion of it, +thus forming a work based on the Library, and copy and distribute such modifications +or work under the terms of Section 1 above, provided that you also meet all +of these conditions: + + a) The modified work must itself be a software library. + +b) You must cause the files modified to carry prominent notices stating that +you changed the files and the date of any change. + +c) You must cause the whole of the work to be licensed at no charge to all +third parties under the terms of this License. + +d) If a facility in the modified Library refers to a function or a table of +data to be supplied by an application program that uses the facility, other +than as an argument passed when the facility is invoked, then you must make +a good faith effort to ensure that, in the event an application does not supply +such function or table, the facility still operates, and performs whatever +part of its purpose remains meaningful. + +(For example, a function in a library to compute square roots has a purpose +that is entirely well-defined independent of the application. Therefore, Subsection +2d requires that any application-supplied function or table used by this function +must be optional: if the application does not supply it, the square root function +must still compute square roots.) + +These requirements apply to the modified work as a whole. If identifiable +sections of that work are not derived from the Library, and can be reasonably +considered independent and separate works in themselves, then this License, +and its terms, do not apply to those sections when you distribute them as +separate works. But when you distribute the same sections as part of a whole +which is a work based on the Library, the distribution of the whole must be +on the terms of this License, whose permissions for other licensees extend +to the entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest your +rights to work written entirely by you; rather, the intent is to exercise +the right to control the distribution of derivative or collective works based +on the Library. + +In addition, mere aggregation of another work not based on the Library with +the Library (or with a work based on the Library) on a volume of a storage +or distribution medium does not bring the other work under the scope of this +License. + +3. You may opt to apply the terms of the ordinary GNU General Public License +instead of this License to a given copy of the Library. To do this, you must +alter all the notices that refer to this License, so that they refer to the +ordinary GNU General Public License, version 2, instead of to this License. +(If a newer version than version 2 of the ordinary GNU General Public License +has appeared, then you can specify that version instead if you wish.) Do not +make any other change in these notices. + +Once this change is made in a given copy, it is irreversible for that copy, +so the ordinary GNU General Public License applies to all subsequent copies +and derivative works made from that copy. + +This option is useful when you wish to copy part of the code of the Library +into a program that is not a library. + +4. You may copy and distribute the Library (or a portion or derivative of +it, under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you accompany it with the complete corresponding +machine-readable source code, which must be distributed under the terms of +Sections 1 and 2 above on a medium customarily used for software interchange. + +If distribution of object code is made by offering access to copy from a designated +place, then offering equivalent access to copy the source code from the same +place satisfies the requirement to distribute the source code, even though +third parties are not compelled to copy the source along with the object code. + +5. A program that contains no derivative of any portion of the Library, but +is designed to work with the Library by being compiled or linked with it, +is called a "work that uses the Library". Such a work, in isolation, is not +a derivative work of the Library, and therefore falls outside the scope of +this License. + +However, linking a "work that uses the Library" with the Library creates an +executable that is a derivative of the Library (because it contains portions +of the Library), rather than a "work that uses the library". The executable +is therefore covered by this License. Section 6 states terms for distribution +of such executables. + +When a "work that uses the Library" uses material from a header file that +is part of the Library, the object code for the work may be a derivative work +of the Library even though the source code is not. Whether this is true is +especially significant if the work can be linked without the Library, or if +the work is itself a library. The threshold for this to be true is not precisely +defined by law. + +If such an object file uses only numerical parameters, data structure layouts +and accessors, and small macros and small inline functions (ten lines or less +in length), then the use of the object file is unrestricted, regardless of +whether it is legally a derivative work. (Executables containing this object +code plus portions of the Library will still fall under Section 6.) + +Otherwise, if the work is a derivative of the Library, you may distribute +the object code for the work under the terms of Section 6. Any executables +containing that work also fall under Section 6, whether or not they are linked +directly with the Library itself. + +6. As an exception to the Sections above, you may also compile or link a "work +that uses the Library" with the Library to produce a work containing portions +of the Library, and distribute that work under terms of your choice, provided +that the terms permit modification of the work for the customer's own use +and reverse engineering for debugging such modifications. + +You must give prominent notice with each copy of the work that the Library +is used in it and that the Library and its use are covered by this License. +You must supply a copy of this License. If the work during execution displays +copyright notices, you must include the copyright notice for the Library among +them, as well as a reference directing the user to the copy of this License. +Also, you must do one of these things: + +a) Accompany the work with the complete corresponding machine-readable source +code for the Library including whatever changes were used in the work (which +must be distributed under Sections 1 and 2 above); and, if the work is an +executable linked with the Library, with the complete machine-readable "work +that uses the Library", as object code and/or source code, so that the user +can modify the Library and then relink to produce a modified executable containing +the modified Library. (It is understood that the user who changes the contents +of definitions files in the Library will not necessarily be able to recompile +the application to use the modified definitions.) + +b) Accompany the work with a written offer, valid for at least three years, +to give the same user the materials specified in Subsection 6a, above, for +a charge no more than the cost of performing this distribution. + +c) If distribution of the work is made by offering access to copy from a designated +place, offer equivalent access to copy the above specified materials from +the same place. + +d) Verify that the user has already received a copy of these materials or +that you have already sent this user a copy. + +For an executable, the required form of the "work that uses the Library" must +include any data and utility programs needed for reproducing the executable +from it. However, as a special exception, the source code distributed need +not include anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the operating +system on which the executable runs, unless that component itself accompanies +the executable. + +It may happen that this requirement contradicts the license restrictions of +other proprietary libraries that do not normally accompany the operating system. +Such a contradiction means you cannot use both them and the Library together +in an executable that you distribute. + +7. You may place library facilities that are a work based on the Library side-by-side +in a single library together with other library facilities not covered by +this License, and distribute such a combined library, provided that the separate +distribution of the work based on the Library and of the other library facilities +is otherwise permitted, and provided that you do these two things: + +a) Accompany the combined library with a copy of the same work based on the +Library, uncombined with any other library facilities. This must be distributed +under the terms of the Sections above. + +b) Give prominent notice with the combined library of the fact that part of +it is a work based on the Library, and explaining where to find the accompanying +uncombined form of the same work. + +8. You may not copy, modify, sublicense, link with, or distribute the Library +except as expressly provided under this License. Any attempt otherwise to +copy, modify, sublicense, link with, or distribute the Library is void, and +will automatically terminate your rights under this License. However, parties +who have received copies, or rights, from you under this License will not +have their licenses terminated so long as such parties remain in full compliance. + +9. You are not required to accept this License, since you have not signed +it. However, nothing else grants you permission to modify or distribute the +Library or its derivative works. These actions are prohibited by law if you +do not accept this License. Therefore, by modifying or distributing the Library +(or any work based on the Library), you indicate your acceptance of this License +to do so, and all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + +10. Each time you redistribute the Library (or any work based on the Library), +the recipient automatically receives a license from the original licensor +to copy, distribute, link with or modify the Library subject to these terms +and conditions. You may not impose any further restrictions on the recipients' +exercise of the rights granted herein. You are not responsible for enforcing +compliance by third parties to this License. + +11. If, as a consequence of a court judgment or allegation of patent infringement +or for any other reason (not limited to patent issues), conditions are imposed +on you (whether by court order, agreement or otherwise) that contradict the +conditions of this License, they do not excuse you from the conditions of +this License. If you cannot distribute so as to satisfy simultaneously your +obligations under this License and any other pertinent obligations, then as +a consequence you may not distribute the Library at all. For example, if a +patent license would not permit royalty-free redistribution of the Library +by all those who receive copies directly or indirectly through you, then the +only way you could satisfy both it and this License would be to refrain entirely +from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any patents +or other property right claims or to contest validity of any such claims; +this section has the sole purpose of protecting the integrity of the free +software distribution system which is implemented by public license practices. +Many people have made generous contributions to the wide range of software +distributed through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing to +distribute software through any other system and a licensee cannot impose +that choice. + +This section is intended to make thoroughly clear what is believed to be a +consequence of the rest of this License. + +12. If the distribution and/or use of the Library is restricted in certain +countries either by patents or by copyrighted interfaces, the original copyright +holder who places the Library under this License may add an explicit geographical +distribution limitation excluding those countries, so that distribution is +permitted only in or among countries not thus excluded. In such case, this +License incorporates the limitation as if written in the body of this License. + +13. The Free Software Foundation may publish revised and/or new versions of +the Library General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to address +new problems or concerns. + +Each version is given a distinguishing version number. If the Library specifies +a version number of this License which applies to it and "any later version", +you have the option of following the terms and conditions either of that version +or of any later version published by the Free Software Foundation. If the +Library does not specify a license version number, you may choose any version +ever published by the Free Software Foundation. + +14. If you wish to incorporate parts of the Library into other free programs +whose distribution conditions are incompatible with these, write to the author +to ask for permission. For software which is copyrighted by the Free Software +Foundation, write to the Free Software Foundation; we sometimes make exceptions +for this. Our decision will be guided by the two goals of preserving the free +status of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + +NO WARRANTY + +15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR +THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE +STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE LIBRARY +"AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, +BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE +OF THE LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE +THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE +OR INABILITY TO USE THE LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA +OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES +OR A FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH +HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +END OF TERMS AND CONDITIONS + +How to Apply These Terms to Your New Libraries + +If you develop a new library, and you want it to be of the greatest possible +use to the public, we recommend making it free software that everyone can +redistribute and change. You can do so by permitting redistribution under +these terms (or, alternatively, under the terms of the ordinary General Public +License). + +To apply these terms, attach the following notices to the library. It is safest +to attach them to the start of each source file to most effectively convey +the exclusion of warranty; and each file should have at least the "copyright" +line and a pointer to where the full notice is found. + + one line to give the library's name and an idea of what it does. + Copyright (C) year name of author + +This library is free software; you can redistribute it and/or modify it under +the terms of the GNU Library General Public License as published by the Free +Software Foundation; either version 2 of the License, or (at your option) +any later version. + +This library is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for +more details. + +You should have received a copy of the GNU Library General Public License +along with this library; if not, write to the Free Software Foundation, Inc., +51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. + +Also add information on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your school, +if any, to sign a "copyright disclaimer" for the library, if necessary. Here +is a sample; alter the names: + +Yoyodyne, Inc., hereby disclaims all copyright interest in +the library `Frob' (a library for tweaking knobs) written +by James Random Hacker. + +signature of Ty Coon, 1 April 1990 +Ty Coon, President of Vice + +That's all there is to it! diff --git a/LICENSES/LicenseRef-KDE-Accepted-GPL.txt b/LICENSES/LicenseRef-KDE-Accepted-GPL.txt new file mode 100644 index 0000000..60a2dff --- /dev/null +++ b/LICENSES/LicenseRef-KDE-Accepted-GPL.txt @@ -0,0 +1,12 @@ +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License as +published by the Free Software Foundation; either version 3 of +the license or (at your option) at any later version that is +accepted by the membership of KDE e.V. (or its successor +approved by the membership of KDE e.V.), which shall act as a +proxy as defined in Section 14 of version 3 of the license. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. diff --git a/LICENSES/Qt-Commercial-exception-1.0.txt b/LICENSES/Qt-Commercial-exception-1.0.txt new file mode 100644 index 0000000..00b93ea --- /dev/null +++ b/LICENSES/Qt-Commercial-exception-1.0.txt @@ -0,0 +1,4 @@ +As a special exception, the copyright holder(s) give permission to link +this program with the Qt Library (commercial or non-commercial edition), +and distribute the resulting executable, without including the source +code for the Qt library in the source distribution. diff --git a/Messages.sh b/Messages.sh new file mode 100755 index 0000000..5a50c47 --- /dev/null +++ b/Messages.sh @@ -0,0 +1,5 @@ +#! /usr/bin/env bash +$EXTRACTRC `find . -name \*.ui -o -name \*.rc -o -name \*.kcfg` >> rc.cpp +$XGETTEXT `find . -name \*.cpp -o -name \*.cc -o -name \*.h ` rc.cpp -o $podir/ktorrent.pot +$XGETTEXT -L Python `find . -name \*.py` -j -o $podir/ktorrent.pot +rm -f rc.cpp diff --git a/RoadMap b/RoadMap new file mode 100644 index 0000000..38290a1 --- /dev/null +++ b/RoadMap @@ -0,0 +1,12 @@ +4.2: +- Scanfolder: configurable loaded directory +- Scanfolder: group per directory +- Improved streaming support to minimize + the impact of streaming on swarm health +- Removeable storage management +- Properties dialog +- Expand scripting +- Time based expiration of items in the loaded list of a feed in the syndication plugin +- Hidden files in torrent creation +- Make filter from item in syndication plugin +- Search torrents in View diff --git a/dbus_xml/org.freedesktop.PowerManagement.Inhibit.xml b/dbus_xml/org.freedesktop.PowerManagement.Inhibit.xml new file mode 100644 index 0000000..d5be190 --- /dev/null +++ b/dbus_xml/org.freedesktop.PowerManagement.Inhibit.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/dbus_xml/org.freedesktop.PowerManagement.xml b/dbus_xml/org.freedesktop.PowerManagement.xml new file mode 100644 index 0000000..35b45c1 --- /dev/null +++ b/dbus_xml/org.freedesktop.PowerManagement.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dbus_xml/org.freedesktop.ScreenSaver.xml b/dbus_xml/org.freedesktop.ScreenSaver.xml new file mode 100644 index 0000000..5efd943 --- /dev/null +++ b/dbus_xml/org.freedesktop.ScreenSaver.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/doc/CMakeLists.txt b/doc/CMakeLists.txt new file mode 100644 index 0000000..ceaac6e --- /dev/null +++ b/doc/CMakeLists.txt @@ -0,0 +1,3 @@ +########### install files ################ +kdoctools_create_handbook(index.docbook INSTALL_DESTINATION ${HTML_INSTALL_DIR}/en SUBDIR ktorrent) + diff --git a/doc/index.docbook b/doc/index.docbook new file mode 100644 index 0000000..5e4b436 --- /dev/null +++ b/doc/index.docbook @@ -0,0 +1,71 @@ + +KTorrent"> + + +]> +
+KTorrent + + +Luca +Beltrame + +
einar@heavensinferno.net
+
+
+ + + + +
+ + +2010-10-27 + +4.0 &kde; 4.5 + +
+ +&ktorrent; is a bittorrent application by KDE which allows you to download files using the BitTorrent protocol. It enables you to run multiple torrents at the same time and comes with extended features to make it a full-featured client for BitTorrent. + + +Features +Queuing of torrents +Global and per torrent speed limits +Previewing of certain file types, build in (video and audio) +Importing of partially or fully downloaded files +File prioritization for multi-file torrents +Selective downloading for multi-file torrents +Kick/ban peers with an additional IP Filter dialog for list/edit purposes +UDP tracker support +Support for private trackers and torrents +Support for µTorrent's peer exchange +Support for protocol encryption (compatible with Azureus) +Support for creating trackerless torrents +Support for distributed hash tables (DHT, the Mainline version) +Support for UPnP to automatically forward ports on a LAN with dynamic assigned hosts +Support for webseeds +Scripting support via Kross and interprocess control via DBus interface +System tray integration +Tracker authentication support +Connection though a proxy +Scripting support via Kross and interprocess control via DBus interface + + + +Online Help Resources +KTorrent on UserBase +KDE Community Forums + +
diff --git a/ktmagnetdownloader/CMakeLists.txt b/ktmagnetdownloader/CMakeLists.txt new file mode 100644 index 0000000..cadd8fc --- /dev/null +++ b/ktmagnetdownloader/CMakeLists.txt @@ -0,0 +1,9 @@ +add_executable(ktmagnetdownloader) +ecm_mark_nongui_executable(ktmagnetdownloader) + +target_sources(ktmagnetdownloader PRIVATE magnetdownloader.cpp magnettest.cpp) + +target_link_libraries(ktmagnetdownloader ktcore Qt5::Widgets Qt5::Network) + +install(TARGETS ktmagnetdownloader ${INSTALL_TARGETS_DEFAULT_ARGS}) + diff --git a/ktmagnetdownloader/magnetdownloader.cpp b/ktmagnetdownloader/magnetdownloader.cpp new file mode 100644 index 0000000..b8565cf --- /dev/null +++ b/ktmagnetdownloader/magnetdownloader.cpp @@ -0,0 +1,46 @@ +#include +#include + +#include + +#include +#include +#include + +#include "magnettest.h" + +using namespace bt; + +int main(int argc, char **argv) +{ + if (argc != 2) { + fprintf(stderr, "Usage: ktmagnetdownloader \n"); + return 0; + } + + QApplication app(argc, argv); + app.setApplicationName(QStringLiteral("KTMagnetDownloader")); + app.setQuitOnLastWindowClosed(false); + + if (!bt::InitLibKTorrent()) { + fprintf(stderr, "Failed to initialize libktorrent\n"); + return -1; + } + + bt::MagnetLink mlink(QString::fromUtf8(argv[1])); + if (!mlink.isValid()) { + fprintf(stderr, "Invalid magnet link %s\n\n", argv[1]); + fprintf(stderr, "Usage: ktmagnetdownloader \n"); + return 0; + } + + bt::SetClientInfo(QStringLiteral("ktmagnetdownloader"), bt::MAJOR, bt::MINOR, bt::RELEASE, bt::NORMAL, QStringLiteral("KT")); + bt::InitLog(QStringLiteral("ktmagnetdownload.log"), false, true); + bt::Log &log = Out(); + log.setOutputToConsole(true); + log << "Downloading " << mlink.toString() << bt::endl; + + MagnetTest mtest(mlink); + + return app.exec(); +} diff --git a/ktmagnetdownloader/magnettest.cpp b/ktmagnetdownloader/magnettest.cpp new file mode 100644 index 0000000..adb2f04 --- /dev/null +++ b/ktmagnetdownloader/magnettest.cpp @@ -0,0 +1,131 @@ +/* + SPDX-FileCopyrightText: 2009 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "magnettest.h" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace kt; +using namespace bt; + +MagnetTest::MagnetTest(const bt::MagnetLink &mlink, QObject *parent) + : QObject(parent) + , mlink(mlink) +{ + upnp = new bt::UPnPMCastSocket(); + connect(upnp, &bt::UPnPMCastSocket::discovered, this, &MagnetTest::routerDiscovered); + + mdownloader = new MagnetDownloader(mlink, this); + connect(mdownloader, &MagnetDownloader::foundMetadata, this, &MagnetTest::foundMetaData); + + QTimer::singleShot(0, this, &MagnetTest::start); + connect(&timer, &QTimer::timeout, this, &MagnetTest::update); +} + +MagnetTest::~MagnetTest() +{ + delete upnp; +} + +void MagnetTest::routerDiscovered(bt::UPnPRouter *router) +{ + net::Port port; + port.number = Settings::dhtPort(); + port.proto = net::UDP; + port.forward = true; + router->forward(port); +} + +void MagnetTest::start() +{ + Uint16 port = Settings::port(); + if (port == 0) { + port = 6881; + Settings::setPort(6881); + } + + // Make sure network interface is set properly before server is initialized + if (!Settings::networkInterface().isEmpty()) { + // QList iface_list = QNetworkInterface::allInterfaces(); + QString iface = Settings::networkInterface(); + SetNetworkInterface(iface); + } + + Uint16 i = 0; + while (!Globals::instance().initTCPServer(port + i) && i < 10) + i++; + + if (i != 10) { + Out(SYS_GEN | LOG_NOTICE) << "Bound to port " << (port + i - 1) << endl; + } else { + Out(SYS_GEN | LOG_IMPORTANT) << "Cannot find free port" << endl; + } + + // start DHT + bt::Globals::instance().getDHT().start(kt::DataDir() + QStringLiteral("dht_table"), kt::DataDir() + QStringLiteral("dht_key"), Settings::dhtPort()); + + // Start UPnP router discovery + upnp->loadRouters(kt::DataDir() + QStringLiteral("routers")); + upnp->discover(); + + mdownloader->start(); + timer.start(500); +} + +void MagnetTest::update() +{ + try { + bt::AuthenticationMonitor::instance().update(); + mdownloader->update(); + } catch (bt::Error &err) { + Out(SYS_GEN | LOG_IMPORTANT) << "Caught bt::Error: " << err.toString() << endl; + } +} + +void MagnetTest::foundMetaData(MagnetDownloader *md, const QByteArray &data) +{ + Q_UNUSED(md); + Out(SYS_GEN | LOG_IMPORTANT) << "Saving to output.torrent" << endl; + bt::File fptr; + if (fptr.open(QStringLiteral("output.torrent"), QStringLiteral("wb"))) { + BEncoder enc(&fptr); + enc.beginDict(); + QList trs = mlink.trackers(); + if (trs.count()) { + enc.write(QByteArrayLiteral("announce")); + enc.write(trs.first().toEncoded()); + if (trs.count() > 1) { + enc.write(QByteArrayLiteral("announce-list")); + enc.beginList(); + for (const QUrl &u : qAsConst(trs)) { + enc.write(u.toEncoded()); + } + enc.end(); + } + } + enc.write(QByteArrayLiteral("info")); + fptr.write(data.data(), data.size()); + enc.end(); + QTimer::singleShot(0, qApp, &QCoreApplication::quit); + } else { + Out(SYS_GEN | LOG_IMPORTANT) << "Failed to open output.torrent: " << fptr.errorString() << endl; + } +} diff --git a/ktmagnetdownloader/magnettest.h b/ktmagnetdownloader/magnettest.h new file mode 100644 index 0000000..130a390 --- /dev/null +++ b/ktmagnetdownloader/magnettest.h @@ -0,0 +1,38 @@ +/* + SPDX-FileCopyrightText: 2009 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef MAGNETTEST_H +#define MAGNETTEST_H + +#include + +#include +#include + +namespace bt +{ +class UPnPRouter; +class UPnPMCastSocket; +} + +class MagnetTest : public QObject +{ +public: + MagnetTest(const bt::MagnetLink &mlink, QObject *parent = nullptr); + ~MagnetTest() override; + + void routerDiscovered(bt::UPnPRouter *router); + void start(); + void update(); + void foundMetaData(bt::MagnetDownloader *md, const QByteArray &data); + +private: + bt::MagnetLink mlink; + bt::UPnPMCastSocket *upnp; + bt::MagnetDownloader *mdownloader; + QTimer timer; +}; + +#endif // MAGNETTEST_H diff --git a/ktorrent/CMakeLists.txt b/ktorrent/CMakeLists.txt new file mode 100644 index 0000000..4e5619a --- /dev/null +++ b/ktorrent/CMakeLists.txt @@ -0,0 +1,131 @@ +add_executable(ktorrent_app) +set_target_properties(ktorrent_app PROPERTIES + OUTPUT_NAME ktorrent + CXX_STANDARD 14 +) + +set(ktorrent_dbus_SRC) +set(powermanagementinhibit_xml ${KTORRENT_DBUS_XML_DIR}/org.freedesktop.PowerManagement.Inhibit.xml) +qt5_add_dbus_interface(ktorrent_dbus_SRC ${powermanagementinhibit_xml} powermanagementinhibit_interface) + +target_sources(ktorrent_app PRIVATE + ${ktorrent_dbus_SRC} + + main.cpp + core.cpp + gui.cpp + torrentactivity.cpp + statusbar.cpp + trayicon.cpp + ipfilterlist.cpp + ipfilterwidget.cpp + statusbarofflineindicator.cpp + + tools/queuemanagerwidget.cpp + tools/queuemanagermodel.cpp + tools/magnetmodel.cpp + tools/magnetview.cpp + + groups/grouppolicydlg.cpp + groups/groupfiltermodel.cpp + groups/groupview.cpp + groups/groupviewmodel.cpp + groups/groupswitcher.cpp + + dialogs/pastedialog.cpp + dialogs/speedlimitsdlg.cpp + dialogs/speedlimitsmodel.cpp + dialogs/spinboxdelegate.cpp + dialogs/torrentcreatordlg.cpp + dialogs/missingfilesdlg.cpp + dialogs/importdialog.cpp + dialogs/addpeersdlg.cpp + dialogs/fileselectdlg.cpp + + pref/prefdialog.cpp + pref/colorpref.cpp + pref/advancedpref.cpp + pref/networkpref.cpp + pref/proxypref.cpp + pref/qmpref.cpp + pref/generalpref.cpp + pref/recommendedsettingsdlg.cpp + pref/btpref.cpp + + view/view.cpp + view/viewmodel.cpp + view/viewdelegate.cpp + view/viewselectionmodel.cpp + view/viewjobtracker.cpp + view/scanextender.cpp + view/propertiesdlg.cpp + view/torrentsearchbar.cpp +) + +ki18n_wrap_ui(ktorrent_app + ipfilterwidget.ui + + dialogs/speedlimitsdlg.ui + dialogs/pastedlgbase.ui + dialogs/torrentcreatordlg.ui + dialogs/missingfilesdlg.ui + dialogs/importdialog.ui + dialogs/addpeersdlg.ui + dialogs/fileselectdlg.ui + + groups/grouppolicydlg.ui + + pref/qmpref.ui + pref/btpref.ui + pref/generalpref.ui + pref/colorpref.ui + pref/advancedpref.ui + pref/networkpref.ui + pref/proxypref.ui + pref/recommendedsettingsdlg.ui + + view/scanextender.ui + view/propertiesdlg.ui +) + +# collect icons +set(KTORRENT_ICONS_PNG + icons/16-apps-ktorrent.png + icons/22-apps-ktorrent.png + icons/32-apps-ktorrent.png + icons/48-apps-ktorrent.png + icons/64-apps-ktorrent.png + icons/128-apps-ktorrent.png +) + +ecm_add_app_icon(ktorrent_app ICONS ${KTORRENT_ICONS_PNG}) + +target_link_libraries(ktorrent_app + ktcore + KF5::Torrent + KF5::Crash + KF5::ConfigCore + KF5::ConfigGui + KF5::DBusAddons + KF5::I18n + KF5::IconThemes + KF5::KIOCore + KF5::KIOFileWidgets + KF5::Notifications + KF5::NotifyConfig + KF5::Parts + KF5::Solid + KF5::WidgetsAddons + KF5::WindowSystem + KF5::XmlGui + KF5::Crash +) + +install(TARGETS ktorrent_app ${INSTALL_TARGETS_DEFAULT_ARGS}) +install(PROGRAMS org.kde.ktorrent.desktop DESTINATION ${XDG_APPS_INSTALL_DIR} ) +install(FILES ktorrentui.rc DESTINATION ${KXMLGUI_INSTALL_DIR}/ktorrent ) +install(FILES kttorrentactivityui.rc DESTINATION ${KXMLGUI_INSTALL_DIR}/ktorrent ) +install(FILES ktorrent.notifyrc DESTINATION ${KNOTIFYRC_INSTALL_DIR} ) +install(FILES org.kde.ktorrent.appdata.xml DESTINATION ${KDE_INSTALL_METAINFODIR} ) + +add_subdirectory(icons) diff --git a/ktorrent/core.cpp b/ktorrent/core.cpp new file mode 100644 index 0000000..c66638a --- /dev/null +++ b/ktorrent/core.cpp @@ -0,0 +1,1262 @@ +/* + SPDX-FileCopyrightText: 2005 Joris Guisson + SPDX-FileCopyrightText: 2005 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "core.h" + +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "dialogs/fileselectdlg.h" +#include "dialogs/missingfilesdlg.h" +#include "gui.h" +#include "powermanagementinhibit_interface.h" +#include "settings.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace bt; + +namespace kt +{ +const Uint32 CORE_UPDATE_INTERVAL = 250; + +Core::Core(kt::GUI *gui) + : gui(gui) + , keep_seeding(true) + , sleep_suppression_cookie(0) + , exiting(false) + , reordering_queue(false) +{ + UpdateCurrentTime(); + qman = new QueueManager(); + connect(qman, &kt::QueueManager::lowDiskSpace, this, &Core::onLowDiskSpace); + connect(qman, &kt::QueueManager::queuingNotPossible, this, &Core::enqueueTorrentOverMaxRatio); + connect(qman, &kt::QueueManager::lowDiskSpace, this, &Core::onLowDiskSpace); + connect(qman, &kt::QueueManager::orderingQueue, this, &Core::beforeQueueReorder); + connect(qman, &kt::QueueManager::queueOrdered, this, &Core::afterQueueReorder); + + data_dir = Settings::tempDir(); + bool dd_not_exist = !bt::Exists(data_dir); + if (data_dir.isEmpty() || dd_not_exist) { + data_dir = kt::DataDir(); + if (dd_not_exist) { + Settings::setTempDir(data_dir); + Settings::self()->save(); + } + } + + removed_bytes_up = removed_bytes_down = 0; + + if (!data_dir.endsWith(bt::DirSeparator())) + data_dir += bt::DirSeparator(); + + connect(&update_timer, &QTimer::timeout, this, &Core::update); + + // Make sure network interface is set properly before server is initialized + if (!Settings::networkInterface().isEmpty()) { + // QList iface_list = QNetworkInterface::allInterfaces(); + QString iface = Settings::networkInterface(); + SetNetworkInterface(iface); + } + + startServers(); + + mman = new kt::MagnetManager(this); + pman = new kt::PluginManager(this, gui); + gman = new kt::GroupManager(); + applySettings(); + gman->loadGroups(); + connect(gman, &kt::GroupManager::customGroupChanged, this, &Core::customGroupChanged); + + qRegisterMetaType("bt::MagnetLink"); + qRegisterMetaType("kt::MagnetLinkLoadOptions"); + connect(mman, &kt::MagnetManager::metadataDownloaded, this, &Core::onMetadataDownloaded, Qt::QueuedConnection); + + mman->loadMagnets(kt::DataDir() + QLatin1String("magnets")); + + connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, this, &Core::onExit); +} + +Core::~Core() +{ + delete qman; + delete pman; + delete gman; +} + +void Core::startServers() +{ + Uint16 port = Settings::port(); + if (port == 0) { + port = 6881; + Settings::setPort(6881); + } + + if (Settings::utpEnabled()) { + startUTPServer(port); + if (!Settings::onlyUseUtp()) + startTCPServer(port); + } else { + startTCPServer(port); + } + ServerInterface::setPort(port); +} + +void Core::startTCPServer(bt::Uint16 port) +{ + if (Globals::instance().initTCPServer(port)) { + Out(SYS_GEN | LOG_NOTICE) << "Bound to TCP port " << port << endl; + } else { + gui->errorMsg(i18n("KTorrent is unable to accept connections because the TCP port %1 is already in use by another program.", port)); + Out(SYS_GEN | LOG_IMPORTANT) << "Cannot find free TCP port" << endl; + } +} + +bool Core::startUTPServer(bt::Uint16 port) +{ + if (Globals::instance().initUTPServer(port)) { + Out(SYS_GEN | LOG_NOTICE) << "Bound to UDP port " << port << endl; + } else { + gui->errorMsg(i18n("KTorrent is unable to accept connections because the UDP port %1 is already in use by another program.", port)); + Out(SYS_GEN | LOG_IMPORTANT) << "Cannot find free UDP port" << endl; + return false; + } + return true; +} + +void Core::applySettings() +{ + bt::Uint16 port = Settings::port(); + bt::Uint16 current_port = ServerInterface::getPort(); + + bool utp_enabled = Settings::utpEnabled(); + bool tcp_enabled = utp_enabled && Settings::onlyUseUtp() ? false : true; + + bt::Globals &globals = bt::Globals::instance(); + + if (globals.isTCPEnabled() && !tcp_enabled) + globals.shutdownTCPServer(); + else if (!globals.isTCPEnabled() && tcp_enabled) + startTCPServer(port); + else if (tcp_enabled && port != current_port) + globals.getTCPServer().changePort(port); + + if (globals.isUTPEnabled() && (!utp_enabled || port != current_port)) + globals.shutdownUTPServer(); + if (!globals.isUTPEnabled() && utp_enabled) + startUTPServer(port); + + if (utp_enabled) + globals.getUTPServer().setTOS(Settings::dscp() << 2); + + ServerInterface::setPort(port); + ServerInterface::setUtpEnabled(utp_enabled, Settings::onlyUseUtp()); + ServerInterface::setPrimaryTransportProtocol((bt::TransportProtocol)Settings::primaryTransportProtocol()); + ApplySettings(); + setMaxDownloads(Settings::maxDownloads()); + setMaxSeeds(Settings::maxSeeds()); + setKeepSeeding(Settings::keepSeeding()); + + QString tmp = Settings::tempDir(); + if (tmp.isEmpty()) + tmp = kt::DataDir(); + + changeDataDir(tmp); + // update QM + getQueueManager()->orderQueue(); + + mman->setUseSlotTimer(Settings::requeueMagnets()); + mman->setTimerDuration(Settings::requeueMagnetsTime()); + mman->setDownloadingSlots(Settings::numMagnetDownloadingSlots()); + + settingsChanged(); +} + +void Core::loadPlugins() +{ + pman->loadPluginList(); +} + +bool Core::init(TorrentControl *tc, const QString &group, const QString &location, bool silently) +{ + bool start_torrent = false; + bool skip_check = false; + QString selected_group = group; + + if (Settings::maxRatio() > 0) + tc->setMaxShareRatio(Settings::maxRatio()); + if (Settings::maxSeedTime() > 0) + tc->setMaxSeedTime(Settings::maxSeedTime()); + + if (Settings::useCompletedDir() && (silently || Settings::openAllTorrentsSilently())) + tc->setMoveWhenCompletedDir(Settings::completedDir()); + + if (qman->alreadyLoaded(tc->getInfoHash())) { + Out(SYS_GEN | LOG_IMPORTANT) << "Torrent " << tc->getDisplayName() << " already loaded" << endl; + return false; + } + + if (!silently && !Settings::openAllTorrentsSilently()) { + FileSelectDlg dlg(qman, gman, group, gui->getMainWindow()); + dlg.loadState(KSharedConfig::openConfig()); + bool ret = dlg.execute(tc, &start_torrent, &skip_check, location) == QDialog::Accepted; + dlg.saveState(KSharedConfig::openConfig()); + + if (!ret) + return false; + else + selected_group = dlg.selectedGroup(); + } else + start_torrent = true; + + QStringList conflicting; + if (qman->checkFileConflicts(tc, conflicting)) { + Out(SYS_GEN | LOG_IMPORTANT) << "Torrent " << tc->getDisplayName() << " conflicts with the following torrents: " << endl; + Out(SYS_GEN | LOG_IMPORTANT) << conflicting.join(QStringLiteral(", ")) << endl; + if (!silently) { + QString err = i18n( + "Opening the torrent %1, would share one or more files with the following torrents. " + "Torrents are not allowed to write to the same files. ", + tc->getDisplayName()); + KMessageBox::errorList(gui, err, conflicting); + } + + return false; + } + + try { + tc->createFiles(); + } catch (bt::Error &err) { + if (!silently) + gui->errorMsg(err.toString()); + Out(SYS_GEN | LOG_IMPORTANT) << err.toString() << endl; + return false; + } + + if (tc->hasExistingFiles()) { + if (!skip_check) + doDataCheck(tc, true); + else + tc->markExistingFilesAsDownloaded(); + } + + tc->setPreallocateDiskSpace(true); + connectSignals(tc); + qman->append(tc); + qman->torrentAdded(tc, start_torrent); + + // now copy torrent file to user specified dir if needed + if (Settings::useTorrentCopyDir()) { + QString torFile = tc->getTorDir(); + if (!torFile.endsWith(bt::DirSeparator())) + torFile += bt::DirSeparator(); + + torFile += QLatin1String("torrent"); + QString destination = Settings::torrentCopyDir(); + if (!destination.endsWith(bt::DirSeparator())) + destination += bt::DirSeparator(); + + destination += tc->getStats().torrent_name + QLatin1String(".torrent"); + KIO::copy(QUrl::fromLocalFile(torFile), QUrl::fromLocalFile(destination)); + } + + // add torrent to group if necessary + Group *g = gman->find(selected_group); + if (g) { + if (!g->isMember(tc)) { + g->addTorrent(tc, true); + gman->saveGroups(); + } + } + + torrentAdded(tc); + if (silently) + Q_EMIT openedSilently(tc); + return true; +} + +bt::TorrentInterface *Core::loadFromData(const QByteArray &data, const QString &dir, const QString &group, bool silently, const QUrl &url) +{ + QString tdir = findNewTorrentDir(); + TorrentControl *tc = nullptr; + try { + tc = new TorrentControl(); + tc->setLoadUrl(url); + tc->init(qman, data, tdir, dir); + + if (init(tc, group, dir, silently)) { + startUpdateTimer(); + return tc; + } + } catch (bt::Warning &warning) { + bt::Out(SYS_GEN | LOG_NOTICE) << warning.toString() << endl; + canNotLoadSilently(warning.toString()); + } catch (bt::Error &err) { + bt::Out(SYS_GEN | LOG_IMPORTANT) << err.toString() << endl; + if (!silently) + gui->errorMsg(err.toString()); + else + canNotLoadSilently(err.toString()); + } + + delete tc; + tc = nullptr; + // delete tdir if necessary + if (bt::Exists(tdir)) + bt::Delete(tdir, true); + + return nullptr; +} + +bt::TorrentInterface *Core::loadFromFile(const QString &target, const QString &dir, const QString &group, bool silently) +{ + try { + QByteArray data = bt::LoadFile(target); + return loadFromData(data, dir, group, silently, QUrl::fromLocalFile(target)); + } catch (bt::Error &err) { + bt::Out(SYS_GEN | LOG_IMPORTANT) << err.toString() << endl; + if (!silently) + gui->errorMsg(err.toString()); + else + canNotLoadSilently(err.toString()); + return nullptr; + } +} + +void Core::downloadFinished(KJob *job) +{ + KIO::StoredTransferJob *j = (KIO::StoredTransferJob *)job; + int err = j->error(); + if (err == KIO::ERR_USER_CANCELED) + return; + + if (err) { + gui->errorMsg(j); + } else { + // load in the file (target is always local) + QString group; + QMap::iterator i = add_to_groups.find(j->url()); + if (i != add_to_groups.end()) { + group = i.value(); + add_to_groups.erase(i); + } + + QString dir = locationHint(group); + if (dir != QString()) + loadFromData(j->data(), dir, group, false, j->url()); + } +} + +void Core::load(const QUrl &url, const QString &group) +{ + if (url.scheme() == QLatin1String("magnet")) { + MagnetLinkLoadOptions options; + options.silently = false; + options.group = group; + load(bt::MagnetLink(url), options); + } else if (url.isLocalFile()) { + QString path = url.toLocalFile(); + QString dir = locationHint(group); + if (dir != QString()) + loadFromFile(path, dir, group, false); + } else { + KIO::Job *j = KIO::storedGet(url); + connect(j, &KIO::Job::result, this, &Core::downloadFinished); + if (!group.isEmpty()) + add_to_groups.insert(url, group); + } +} + +void Core::downloadFinishedSilently(KJob *job) +{ + KIO::StoredTransferJob *j = (KIO::StoredTransferJob *)job; + int err = j->error(); + if (err == KIO::ERR_USER_CANCELED) { + // do nothing + } else if (err) { + canNotLoadSilently(j->errorString()); + } else { + QString dir; + if (custom_save_locations.contains(j)) { + // we have a custom save location so save to that + dir = custom_save_locations[j].toLocalFile(); + custom_save_locations.remove(j); + } else if (!Settings::useSaveDir()) { + // in case save dir is not set, use home director + Out(SYS_GEN | LOG_NOTICE) << "Cannot load " << j->url() << " silently, default save location not set !" << endl; + Out(SYS_GEN | LOG_NOTICE) << "Using home directory instead !" << endl; + dir = QDir::homePath(); + } else { + dir = Settings::saveDir(); + } + + QString group; + QMap::iterator i = add_to_groups.find(j->url()); + if (i != add_to_groups.end()) { + group = i.value(); + add_to_groups.erase(i); + } + + if (dir != QString()) + loadFromData(j->data(), dir, group, true, j->url()); + } +} + +void Core::loadSilently(const QUrl &url, const QString &group) +{ + if (url.scheme() == QLatin1String("magnet")) { + MagnetLinkLoadOptions options; + options.silently = true; + options.group = group; + load(bt::MagnetLink(url), options); + } else if (url.isLocalFile()) { + QString path = url.toLocalFile(); + QString dir = locationHint(group); + + if (dir != QString()) + loadFromFile(path, dir, group, true); + } else { + // download to a random file in tmp + KIO::Job *j = KIO::storedGet(url); + connect(j, &KIO::Job::result, this, &Core::downloadFinishedSilently); + if (!group.isEmpty()) + add_to_groups.insert(url, group); + } +} + +bt::TorrentInterface *Core::load(const QByteArray &data, const QUrl &url, const QString &group, const QString &savedir) +{ + QString dir; + if (savedir.isEmpty() || !bt::Exists(savedir)) + dir = locationHint(group); + else + dir = savedir; + + if (dir != QString()) + return loadFromData(data, dir, group, false, url); + else + return nullptr; +} + +bt::TorrentInterface *Core::loadSilently(const QByteArray &data, const QUrl &url, const QString &group, const QString &savedir) +{ + QString dir; + if (savedir.isEmpty() || !bt::Exists(savedir)) + dir = locationHint(group); + else + dir = savedir; + + if (dir != QString()) + return loadFromData(data, dir, group, true, url); + else + return nullptr; +} + +void Core::start(bt::TorrentInterface *tc) +{ + if (tc->getStats().paused) { + tc->unpause(); + } else { + TorrentStartResponse reason = qman->start(tc); + if (reason == NOT_ENOUGH_DISKSPACE || reason == QM_LIMITS_REACHED) + canNotStart(tc, reason); + } + + startUpdateTimer(); // restart update timer +} + +void Core::start(QList &todo) +{ + if (todo.isEmpty()) + return; + + // unpause paused torrents + for (QList::iterator i = todo.begin(); i != todo.end();) { + if ((*i)->getStats().paused) { + (*i)->unpause(); + i = todo.erase(i); + } else + i++; + } + + if (todo.count() == 1) { + start(todo.front()); + } else { + qman->start(todo); + } + + startUpdateTimer(); // restart update timer +} + +void Core::stop(bt::TorrentInterface *tc) +{ + qman->stop(tc); +} + +void Core::stop(QList &todo) +{ + qman->stop(todo); +} + +void Core::pause(TorrentInterface *tc) +{ + tc->pause(); +} + +void Core::pause(QList &todo) +{ + for (bt::TorrentInterface *tc : qAsConst(todo)) { + tc->pause(); + } +} + +QString Core::findNewTorrentDir() const +{ + int i = 0; + while (true) { + QDir d; + QString dir = data_dir % QLatin1String("tor") % QString::number(i) % QLatin1Char('/'); + if (!d.exists(dir)) { + return dir; + } + i++; + } + return QString(); +} + +void Core::loadExistingTorrent(const QString &tor_dir) +{ + TorrentControl *tc = nullptr; + + QString idir = tor_dir; + if (!idir.endsWith(bt::DirSeparator())) + idir += bt::DirSeparator(); + + if (!bt::Exists(idir + QLatin1String("torrent"))) + return; + + try { + tc = new TorrentControl(); + tc->init(qman, bt::LoadFile(idir + QLatin1String("torrent")), idir, QString()); + + qman->append(tc); + connectSignals(tc); + torrentAdded(tc); + } catch (bt::Error &err) { + gui->errorMsg(err.toString()); + delete tc; + } catch (bt::Warning &warning) { + bt::Out(SYS_GEN | LOG_NOTICE) << warning.toString() << endl; + canNotLoadSilently(warning.toString()); + bt::Delete(tor_dir, true); + } +} + +void Core::loadTorrents() +{ + QDir dir(data_dir); + QStringList filters; + filters << QStringLiteral("tor*"); + const QStringList sl = dir.entryList(filters, QDir::Dirs); + for (const QString &s : sl) { + QString idir = data_dir + s; + if (!idir.endsWith(DirSeparator())) + idir.append(DirSeparator()); + + Out(SYS_GEN | LOG_NOTICE) << "Loading " << idir << endl; + loadExistingTorrent(idir); + } + + gman->torrentsLoaded(qman); + qman->loadState(KSharedConfig::openConfig()); + QTimer::singleShot(0, this, &Core::delayedStart); +} + +void Core::delayedStart() +{ + qman->orderQueue(); + if (!kt::QueueManager::enabled()) + qman->startAutoStartTorrents(); +} + +void Core::remove(bt::TorrentInterface *tc, bool data_to) +{ + try { + if (tc->getJobQueue()->runningJobs()) { + // if there are running jobs, schedule delete when they finish + delayed_removal.insert(tc, data_to); + connect(tc, &TorrentControl::runningJobsDone, this, &Core::delayedRemove); + return; + } + + const bt::TorrentStats &s = tc->getStats(); + removed_bytes_up += s.session_bytes_uploaded; + removed_bytes_down += s.session_bytes_downloaded; + stop(tc); + + QString dir = tc->getTorDir(); + + try { + if (data_to) + tc->deleteDataFiles(); + } catch (Error &e) { + gui->errorMsg(e.toString()); + } + + torrentRemoved(tc); + gman->torrentRemoved(tc); + qman->torrentRemoved(tc); + gui->updateActions(); + bt::Delete(dir, false); + delayed_removal.remove(tc); + } catch (Error &e) { + gui->errorMsg(e.toString()); + } +} + +void Core::remove(QList &todo, bool data_to) +{ + QList::iterator i = todo.begin(); + while (i != todo.end()) { + bt::TorrentInterface *tc = *i; + if (tc->getJobQueue()->runningJobs()) { + // if there are running jobs, schedule delete when they finish + delayed_removal.insert(tc, data_to); + connect(tc, &bt::TorrentInterface::runningJobsDone, this, &Core::delayedRemove); + i = todo.erase(i); + } else + i++; + } + + stop(todo); + + for (bt::TorrentInterface *tc : qAsConst(todo)) { + const bt::TorrentStats &s = tc->getStats(); + removed_bytes_up += s.session_bytes_uploaded; + removed_bytes_down += s.session_bytes_downloaded; + + QString dir = tc->getTorDir(); + + try { + if (data_to) + tc->deleteDataFiles(); + } catch (Error &e) { + gui->errorMsg(e.toString()); + } + + torrentRemoved(tc); + gman->torrentRemoved(tc); + try { + bt::Delete(dir, false); + } catch (Error &e) { + gui->errorMsg(e.toString()); + } + } + + qman->torrentsRemoved(todo); + gui->updateActions(); +} + +void Core::delayedRemove(bt::TorrentInterface *tc) +{ + if (!delayed_removal.contains(tc)) + return; + + remove(tc, delayed_removal[tc]); +} + +void Core::setMaxDownloads(int max) +{ + qman->setMaxDownloads(max); +} + +void Core::setMaxSeeds(int max) +{ + qman->setMaxSeeds(max); +} + +void Core::torrentFinished(bt::TorrentInterface *tc) +{ + if (!keep_seeding) + tc->stop(); + + finished(tc); + qman->torrentFinished(tc); +} + +void Core::setKeepSeeding(bool ks) +{ + keep_seeding = ks; + qman->setKeepSeeding(ks); +} + +void Core::onExit() +{ + Q_EMIT aboutToQuit(); + // stop timer to prevent updates during wait + exiting = true; + update_timer.stop(); + + net::SocketMonitor::instance().shutdown(); + mman->saveMagnets(kt::DataDir() + QLatin1String("magnets")); + // make sure DHT is stopped + Globals::instance().getDHT().stop(); + // stop all authentications going on + AuthenticationMonitor::instance().shutdown(); + + WaitJob *job = new WaitJob(5000); + qman->saveState(KSharedConfig::openConfig()); + + // Sync the config to be sure everything is saved + Settings::self()->save(); + + qman->onExit(job); + // wait for completion of stopped events + if (job->needToWait()) { + WaitJob::execute(job); + } else + delete job; + + // shutdown the servers + Globals::instance().shutdownTCPServer(); + Globals::instance().shutdownUTPServer(); + + pman->unloadAll(); + qman->clear(); +} + +bool Core::changeDataDir(const QString &new_dir) +{ + try { + // do nothing if new and old dir are the same + if ((QFileInfo(data_dir).absoluteFilePath().length() && QFileInfo(data_dir).absoluteFilePath() == QFileInfo(new_dir).absoluteFilePath() + && QFileInfo(data_dir).absoluteFilePath() == QFileInfo(new_dir).absoluteFilePath()) + || data_dir == new_dir || data_dir == (new_dir + bt::DirSeparator())) + return true; + + update_timer.stop(); + // safety check + if (!bt::Exists(new_dir)) + bt::MakeDir(new_dir); + + // make sure new_dir ends with a / + QString nd = new_dir; + if (!nd.endsWith(DirSeparator())) + nd += DirSeparator(); + + Out(SYS_GEN | LOG_DEBUG) << "Switching to datadir " << nd << endl; + + qman->setSuspendedState(true); + + QList succes; + + QList::iterator i = qman->begin(); + while (i != qman->end()) { + bt::TorrentInterface *tc = *i; + if (!tc->changeTorDir(nd)) { + // failure time to roll back all the successful tc's + rollback(succes); + // set back the old data_dir in Settings + Settings::setTempDir(data_dir); + Settings::self()->save(); + qman->setSuspendedState(false); + update_timer.start(CORE_UPDATE_INTERVAL); + return false; + } else { + succes.append(tc); + } + i++; + } + data_dir = nd; + qman->setSuspendedState(false); + update_timer.start(CORE_UPDATE_INTERVAL); + return true; + } catch (bt::Error &e) { + Out(SYS_GEN | LOG_IMPORTANT) << "Error : " << e.toString() << endl; + update_timer.start(CORE_UPDATE_INTERVAL); + return false; + } +} + +void Core::rollback(const QList &succes) +{ + Out(SYS_GEN | LOG_DEBUG) << "Error, rolling back" << endl; + update_timer.stop(); + QList::const_iterator i = succes.begin(); + while (i != succes.end()) { + (*i)->rollback(); + i++; + } + update_timer.start(CORE_UPDATE_INTERVAL); +} + +void Core::startAll() +{ + qman->startAll(); + startUpdateTimer(); +} + +void Core::stopAll() +{ + qman->stopAll(); +} + +void Core::startUpdateTimer() +{ + if (!update_timer.isActive()) { + Out(SYS_GEN | LOG_DEBUG) << "Started update timer" << endl; + update_timer.start(CORE_UPDATE_INTERVAL); + if (Settings::suppressSleep() && sleep_suppression_cookie == 0) { + org::freedesktop::PowerManagement::Inhibit powerManagement(QStringLiteral("org.freedesktop.PowerManagement.Inhibit"), + QStringLiteral("/org/freedesktop/PowerManagement/Inhibit"), + QDBusConnection::sessionBus()); + QDBusPendingReply pendingReply = powerManagement.Inhibit(QStringLiteral("ktorrent"), i18n("KTorrent is running one or more torrents")); + auto pendingCallWatcher = new QDBusPendingCallWatcher(pendingReply, this); + connect(pendingCallWatcher, &QDBusPendingCallWatcher::finished, this, [=](QDBusPendingCallWatcher *callWatcher) { + QDBusPendingReply reply = *callWatcher; + if (reply.isValid()) { + sleep_suppression_cookie = reply.value(); + Out(SYS_GEN | LOG_DEBUG) << "Suppressing sleep" << endl; + } else + Out(SYS_GEN | LOG_IMPORTANT) << "Failed to suppress sleeping" << endl; + }); + } + } +} + +void Core::update() +{ + if (exiting) + return; + + try { + bt::UpdateCurrentTime(); + AuthenticationMonitor::instance().update(); + + QList::iterator i = qman->begin(); + bool updated = false; + while (i != qman->end()) { + bt::TorrentInterface *tc = *i; + if (tc->getStats().running) { + tc->update(); + updated = true; + } + i++; + } + + if (!updated && mman->count() == 0) { + Out(SYS_GEN | LOG_DEBUG) << "Stopped update timer" << endl; + update_timer.stop(); // stop timer when not necessary + + if (sleep_suppression_cookie) { + org::freedesktop::PowerManagement::Inhibit powerManagement(QStringLiteral("org.freedesktop.PowerManagement.Inhibit"), + QStringLiteral("/org/freedesktop/PowerManagement/Inhibit"), + QDBusConnection::sessionBus()); + auto pendingReply = powerManagement.UnInhibit(sleep_suppression_cookie); + auto pendingCallWatcher = new QDBusPendingCallWatcher(pendingReply, this); + connect(pendingCallWatcher, &QDBusPendingCallWatcher::finished, this, [=](QDBusPendingCallWatcher *callWatcher) { + QDBusPendingReply reply = *callWatcher; + if (reply.isValid()) { + sleep_suppression_cookie = 0; + Out(SYS_GEN | LOG_DEBUG) << "Stopped suppressing sleep" << endl; + } else + Out(SYS_GEN | LOG_IMPORTANT) << "Failed to stop suppressing sleep" << endl; + }); + } + } else { + mman->update(); + // check if the priority of stalled torrents must be decreased + if (Settings::decreasePriorityOfStalledTorrents()) { + qman->checkStalledTorrents(bt::CurrentTime(), Settings::stallTimer()); + } + } + } catch (bt::Error &err) { + Out(SYS_GEN | LOG_IMPORTANT) << "Caught bt::Error: " << err.toString() << endl; + } +} + +bt::TorrentInterface *Core::createTorrent(bt::TorrentCreator *mktor, bool seed) +{ + QString tdir; + try { + tdir = findNewTorrentDir(); + bt::TorrentControl *tc = mktor->makeTC(tdir); + if (tc) { + connectSignals(tc); + qman->append(tc); + if (seed) + start(tc); + torrentAdded(tc); + return tc; + } + } catch (bt::Error &e) { + // cleanup if necessary + if (bt::Exists(tdir)) + bt::Delete(tdir, true); + + // Show error message + gui->errorMsg(i18n("Cannot create torrent: %1", e.toString())); + } + return nullptr; +} + +CurrentStats Core::getStats() +{ + CurrentStats stats; + Uint64 bytes_dl = 0, bytes_ul = 0; + Uint32 speed_dl = 0, speed_ul = 0; + + for (bt::TorrentInterface *tc : qAsConst(*qman)) { + const TorrentStats &s = tc->getStats(); + speed_dl += s.download_rate; + speed_ul += s.upload_rate; + bytes_dl += s.session_bytes_downloaded; + bytes_ul += s.session_bytes_uploaded; + } + stats.download_speed = speed_dl; + stats.upload_speed = speed_ul; + stats.bytes_downloaded = bytes_dl + removed_bytes_down; + stats.bytes_uploaded = bytes_ul + removed_bytes_up; + + return stats; +} + +bool Core::changePort(Uint16 port) +{ + bt::Globals &globals = bt::Globals::instance(); + bool ok = false; + if (Settings::utpEnabled()) { + globals.shutdownUTPServer(); + ok = startUTPServer(port); + + if (!Settings::onlyUseUtp()) { + bt::Server &srv = globals.getTCPServer(); + ok = ok && srv.changePort(port); + } + } else { + bt::Server &srv = globals.getTCPServer(); + ok = srv.changePort(port); + } + + return ok; +} + +void Core::slotStoppedByError(bt::TorrentInterface *tc, QString msg) +{ + Q_EMIT torrentStoppedByError(tc, msg); +} + +Uint32 Core::getNumTorrentsRunning() const +{ + return qman->getNumRunning(); +} + +Uint32 Core::getNumTorrentsNotRunning() const +{ + return qman->count() - qman->getNumRunning(); +} + +kt::QueueManager *Core::getQueueManager() +{ + return this->qman; +} + +void Core::torrentSeedAutoStopped(bt::TorrentInterface *tc, AutoStopReason reason) +{ + qman->startNext(); + if (reason == MAX_RATIO_REACHED) + maxShareRatioReached(tc); + else + maxSeedTimeReached(tc); + startUpdateTimer(); +} + +void Core::setSuspendedState(bool suspend) +{ + qman->setSuspendedState(suspend); + if (!suspend) + startUpdateTimer(); +} + +bool Core::getSuspendedState() +{ + return qman->getSuspendedState(); +} + +bool Core::checkMissingFiles(TorrentInterface *tc) +{ + QStringList missing; + if (!tc->hasMissingFiles(missing)) + return true; + + QStringList not_mounted; + while (!tc->isStorageMounted(not_mounted)) { + QString msg = i18n("One or more storage volumes are not mounted. In order to start this torrent, they need to be mounted."); + KGuiItem retry(i18n("Retry"), QStringLiteral("emblem-mounted")); + if (KMessageBox::warningContinueCancelList(gui, msg, not_mounted, QString(), retry) == KMessageBox::Continue) { + not_mounted.clear(); + continue; + } else { + if (not_mounted.size() == 1) + tc->handleError(i18n("Storage volume %1 is not mounted", not_mounted.first())); + else + tc->handleError(i18n("Storage volumes %1 are not mounted", not_mounted.join(QStringLiteral(", ")))); + return false; + } + } + + missing.clear(); + if (!tc->hasMissingFiles(missing)) + return true; + + if (tc->getStats().multi_file_torrent) { + QString msg = i18n( + "Several data files of the torrent \"%1\" are missing. \n" + "Do you want to recreate them, or do you want to not download them?", + tc->getStats().torrent_name); + + MissingFilesDlg dlg(msg, missing, tc, nullptr); + + switch (dlg.execute()) { + case MissingFilesDlg::CANCEL: + tc->handleError(i18n("Data files are missing")); + return false; + case MissingFilesDlg::DO_NOT_DOWNLOAD: + try { + // mark them as do not download + tc->dndMissingFiles(); + } catch (bt::Error &e) { + gui->errorMsg(i18n("Cannot deselect missing files: %1", e.toString())); + tc->handleError(i18n("Data files are missing")); + return false; + } + break; + case MissingFilesDlg::RECREATE: + try { + // recreate them + tc->recreateMissingFiles(); + } catch (bt::Error &e) { + KMessageBox::error(nullptr, i18n("Cannot recreate missing files: %1", e.toString())); + tc->handleError(i18n("Data files are missing")); + return false; + } + break; + case MissingFilesDlg::NEW_LOCATION_SELECTED: + break; + } + } else { + QString msg = i18n( + "The file where the data is saved of the torrent \"%1\" is missing.\n" + "Do you want to recreate it?", + tc->getStats().torrent_name); + MissingFilesDlg dlg(msg, missing, tc, nullptr); + + switch (dlg.execute()) { + case MissingFilesDlg::CANCEL: + tc->handleError(i18n("Data file is missing")); + return false; + case MissingFilesDlg::RECREATE: + try { + tc->recreateMissingFiles(); + } catch (bt::Error &e) { + gui->errorMsg(i18n("Cannot recreate data file: %1", e.toString())); + tc->handleError(i18n("Data file is missing")); + return false; + } + break; + case MissingFilesDlg::DO_NOT_DOWNLOAD: + return false; + case MissingFilesDlg::NEW_LOCATION_SELECTED: + break; + } + } + + return true; +} + +void Core::aboutToBeStarted(bt::TorrentInterface *tc, bool &ret) +{ + ret = checkMissingFiles(tc); +} + +void Core::emitCorruptedData(bt::TorrentInterface *tc) +{ + corruptedData(tc); +} + +void Core::connectSignals(bt::TorrentInterface *tc) +{ + connect(tc, &bt::TorrentInterface::finished, this, &Core::torrentFinished); + connect(tc, &bt::TorrentInterface::stoppedByError, this, &Core::slotStoppedByError); + connect(tc, &bt::TorrentInterface::seedingAutoStopped, this, &Core::torrentSeedAutoStopped); + connect(tc, &bt::TorrentInterface::aboutToBeStarted, this, &Core::aboutToBeStarted); + connect(tc, &bt::TorrentInterface::corruptedDataFound, this, &Core::emitCorruptedData); + connect(tc, &bt::TorrentInterface::needDataCheck, this, &Core::autoCheckData); + connect(tc, &bt::TorrentInterface::statusChanged, this, &Core::onStatusChanged); +} + +float Core::getGlobalMaxShareRatio() const +{ + return Settings::maxRatio(); +} + +void Core::enqueueTorrentOverMaxRatio(bt::TorrentInterface *tc) +{ + Q_EMIT queuingNotPossible(tc); +} + +void Core::autoCheckData(bt::TorrentInterface *tc) +{ + Out(SYS_GEN | LOG_IMPORTANT) << "Doing an automatic data check on " << tc->getStats().torrent_name << endl; + + doDataCheck(tc); +} + +void Core::doDataCheck(bt::TorrentInterface *tc, bool auto_import) +{ + tc->startDataCheck(auto_import, 0, tc->getStats().total_chunks); +} + +void Core::onLowDiskSpace(bt::TorrentInterface *tc, bool stopped) +{ + Q_EMIT lowDiskSpace(tc, stopped); +} + +void Core::updateGuiPlugins() +{ + pman->updateGuiPlugins(); +} + +DBus *Core::getExternalInterface() +{ + return gui->getDBusInterface(); +} + +void Core::onStatusChanged(bt::TorrentInterface *tc) +{ + Q_UNUSED(tc); + if (!reordering_queue) + gui->updateActions(); +} + +void Core::beforeQueueReorder() +{ + reordering_queue = true; +} + +void Core::afterQueueReorder() +{ + reordering_queue = false; + gui->updateActions(); + gman->updateCount(qman); + startUpdateTimer(); +} + +void Core::customGroupChanged() +{ + gman->updateCount(qman); +} + +void Core::load(const bt::MagnetLink &mlink, const MagnetLinkLoadOptions &options) +{ + if (!mlink.isValid()) { + gui->errorMsg(i18n("Invalid magnet bittorrent link: %1", mlink.toString())); + } else { + if (!Globals::instance().getDHT().isRunning()) + dhtNotEnabled(i18n("You are attempting to download a magnet link, and DHT is not enabled. For optimum results enable DHT.")); + mman->addMagnet(mlink, options, false); + startUpdateTimer(); + } +} + +void Core::onMetadataDownloaded(const bt::MagnetLink &mlink, const QByteArray &data, const kt::MagnetLinkLoadOptions &options) +{ + QByteArray tmp; + BEncoderBufferOutput *out = new BEncoderBufferOutput(tmp); + BEncoder enc(out); + enc.beginDict(); + QList trs = mlink.trackers(); + if (trs.count()) { + enc.write(QByteArrayLiteral("announce")); + enc.write(trs.first().toDisplayString().toUtf8()); + if (trs.count() > 1) { + enc.write(QByteArrayLiteral("announce-list")); + enc.beginList(); + for (const QUrl &tracker : qAsConst(trs)) { + enc.beginList(); + enc.write(tracker.toDisplayString().toUtf8()); + enc.end(); + } + enc.end(); + } + } + enc.write(QByteArrayLiteral("info")); + out->write(data.data(), data.size()); + enc.end(); + + QUrl url(mlink.toString()); + + bt::TorrentInterface *tc = nullptr; + if (options.silently) + tc = loadSilently(tmp, url, options.group, options.location); + else + tc = load(tmp, url, options.group, options.location); + + if (tc && !options.move_on_completion.isEmpty()) + tc->setMoveWhenCompletedDir(options.move_on_completion); +} + +QString Core::locationHint(const QString &group) const +{ + QString dir; + + // First see if we can use the group settings + Group *g = gman->find(group); + QString group_save_location = g != nullptr ? g->groupPolicy().default_save_location : QString(); + if (!group_save_location.isEmpty() && bt::Exists(group_save_location)) + dir = g->groupPolicy().default_save_location; + else if (Settings::useSaveDir()) + dir = Settings::saveDir(); + else + dir = Settings::lastSaveDir(); + + if (dir.isEmpty() || !bt::Exists(dir)) + dir = QDir::homePath(); + + return dir; +} + +} diff --git a/ktorrent/core.h b/ktorrent/core.h new file mode 100644 index 0000000..017b700 --- /dev/null +++ b/ktorrent/core.h @@ -0,0 +1,270 @@ +/* + SPDX-FileCopyrightText: 2005 Joris Guisson + SPDX-FileCopyrightText: 2005 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KTCORE_HH +#define KTCORE_HH + +#include +#include + +#include +#include + +class KJob; + +namespace bt +{ +class TorrentControl; +class TorrentInterface; +} + +namespace kt +{ +class MagnetManager; +class GUI; +class PluginManager; +class GroupManager; + +/** + * Core of ktorrent, manages every non GUI aspect of the application + * */ +class Core : public CoreInterface +{ + Q_OBJECT +public: + Core(GUI *gui); + ~Core() override; + + // implemented from CoreInterface + void setKeepSeeding(bool ks) override; + bool changeDataDir(const QString &new_dir) override; + void startAll() override; + void stopAll() override; + CurrentStats getStats() override; + bool changePort(bt::Uint16 port) override; + bt::Uint32 getNumTorrentsRunning() const override; + bt::Uint32 getNumTorrentsNotRunning() const override; + void load(const QUrl &url, const QString &group) override; + bt::TorrentInterface *load(const QByteArray &data, const QUrl &url, const QString &group, const QString &savedir) override; + void loadSilently(const QUrl &url, const QString &group) override; + bt::TorrentInterface *loadSilently(const QByteArray &data, const QUrl &url, const QString &group, const QString &savedir) override; + void load(const bt::MagnetLink &mlink, const MagnetLinkLoadOptions &options) override; + QString findNewTorrentDir() const override; + void loadExistingTorrent(const QString &tor_dir) override; + void setSuspendedState(bool suspend) override; + bool getSuspendedState() override; + float getGlobalMaxShareRatio() const; + DBus *getExternalInterface() override; + + /// Get the queue manager + kt::QueueManager *getQueueManager() override; + + /// Get the group manager + kt::GroupManager *getGroupManager() override + { + return gman; + } + + /// Get the magnet manager + kt::MagnetManager *getMagnetManager() override + { + return mman; + } + + bt::TorrentInterface *createTorrent(bt::TorrentCreator *mktor, bool seed) override; + + /** + * Set the maximum number of simultaneous downloads. + * @param max The max num (0 == no limit) + */ + void setMaxDownloads(int max); + + /** + * Set the maximum number of simultaneous seeds. + * @param max The max num (0 == no limit) + */ + void setMaxSeeds(int max); + + /** + * Load plugins. + */ + void loadPlugins(); + +public Q_SLOTS: + /** + * Start the update timer + */ + void startUpdateTimer(); + + /** + * Update all torrents. + */ + void update(); + + void start(bt::TorrentInterface *tc) override; + void start(QList &todo) override; + void stop(bt::TorrentInterface *tc) override; + void stop(QList &todo) override; + void remove(bt::TorrentInterface *tc, bool data_to) override; + void remove(QList &todo, bool data_to) override; + void pause(bt::TorrentInterface *tc) override; + void pause(QList &todo) override; + + /** + * A torrent is about to be started. We will do some file checks upon this signal. + * @param tc The TorrentInterface + */ + void aboutToBeStarted(bt::TorrentInterface *tc, bool &ret); + + /** + * Checks for missing files and shows the MissingFilesDlg if necessary. + * @param tc The torrent + * @return True if everything is OK, false otherwise + */ + bool checkMissingFiles(bt::TorrentInterface *tc); + + /** + * User tried to enqueue a torrent that has reached max share ratio. + * Emits appropriate signal. + */ + void enqueueTorrentOverMaxRatio(bt::TorrentInterface *tc); + + /** + * Do a data check on a torrent + * @param tc The torrent + * @param auto_import Is this an automatic import + */ + void doDataCheck(bt::TorrentInterface *tc, bool auto_import = false); + + /// Fires when disk space is running low + void onLowDiskSpace(bt::TorrentInterface *tc, bool stopped); + + /// Apply all the settings + void applySettings() override; + + /// Update the GUI plugins + void updateGuiPlugins(); + + /// Handle status changes + void onStatusChanged(bt::TorrentInterface *tc); + + /// Handle the download of meta data + void onMetadataDownloaded(const bt::MagnetLink &mlink, const QByteArray &data, const kt::MagnetLinkLoadOptions &options); + +Q_SIGNALS: + /** + * TorrentCore torrents have beed updated. Stats are changed. + **/ + void statsUpdated(); + + /** + * Emitted when a torrent has reached it's max share ratio. + * @param tc The torrent + */ + void maxShareRatioReached(bt::TorrentInterface *tc); + + /** + * Emitted when a torrent has reached it's max seed time + * @param tc The torrent + */ + void maxSeedTimeReached(bt::TorrentInterface *tc); + + /** + * Corrupted data has been detected. + * @param tc The torrent with the corrupted data + */ + void corruptedData(bt::TorrentInterface *tc); + + /** + * User tried to enqueue a torrent that has reached max share ratio. It's not possible. + * Signal should be connected to SysTray slot which shows appropriate KPassivePopup info. + * @param tc The torrent in question. + */ + void queuingNotPossible(bt::TorrentInterface *tc); + + /** + * Emitted when a torrent cannot be started + * @param tc The torrent + * @param reason The reason + */ + void canNotStart(bt::TorrentInterface *tc, bt::TorrentStartResponse reason); + + /** + * Diskspace is running low. + * Signal should be connected to SysTray slot which shows appropriate KPassivePopup info. + * @param tc The torrent in question. + */ + void lowDiskSpace(bt::TorrentInterface *tc, bool stopped); + + /** + * Loading silently failed. + * @param msg Error message + */ + void canNotLoadSilently(const QString &msg); + + /** + * Emitted when DHT is not enabled and a MagnetLink is being downloaded + */ + void dhtNotEnabled(const QString &msg); + void openedSilently(bt::TorrentInterface *tc); + + /** + * Emitted just before Core object is destroyed + */ + void aboutToQuit(); + +private: + void rollback(const QList &success); + void connectSignals(bt::TorrentInterface *tc); + bool init(bt::TorrentControl *tc, const QString &group, const QString &location, bool silently); + QString locationHint(const QString &group) const; + void startServers(); + void startTCPServer(bt::Uint16 port); + bool startUTPServer(bt::Uint16 port); + bt::TorrentInterface *loadFromFile(const QString &file, const QString &dir, const QString &group, bool silently); + bt::TorrentInterface *loadFromData(const QByteArray &data, const QString &dir, const QString &group, bool silently, const QUrl &url); + +public: + void loadTorrents(); + +private Q_SLOTS: + void torrentFinished(bt::TorrentInterface *tc); + void slotStoppedByError(bt::TorrentInterface *tc, QString msg); + void torrentSeedAutoStopped(bt::TorrentInterface *tc, bt::AutoStopReason reason); + void downloadFinished(KJob *job); + void downloadFinishedSilently(KJob *job); + void emitCorruptedData(bt::TorrentInterface *tc); + void autoCheckData(bt::TorrentInterface *tc); + void delayedRemove(bt::TorrentInterface *tc); + void delayedStart(); + void beforeQueueReorder(); + void afterQueueReorder(); + void customGroupChanged(); + /** + * KT is exiting, shutdown the core + */ + void onExit(); + +private: + GUI *gui; + bool keep_seeding; + QString data_dir; + QTimer update_timer; + bt::Uint64 removed_bytes_up, removed_bytes_down; + kt::PluginManager *pman; + kt::QueueManager *qman; + kt::GroupManager *gman; + kt::MagnetManager *mman; + QMap custom_save_locations; // map to store save locations + QMap add_to_groups; // Map to keep track of which group to add a torrent to + quint32 sleep_suppression_cookie; + QMap delayed_removal; + bool exiting; + bool reordering_queue; +}; +} + +#endif diff --git a/ktorrent/dialogs/addpeersdlg.cpp b/ktorrent/dialogs/addpeersdlg.cpp new file mode 100644 index 0000000..04cb87e --- /dev/null +++ b/ktorrent/dialogs/addpeersdlg.cpp @@ -0,0 +1,66 @@ +/* + SPDX-FileCopyrightText: 2007 Joris Guisson + SPDX-FileCopyrightText: 2007 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include +#include + +#include "addpeersdlg.h" +#include + +namespace kt +{ +class ManualPeerSource : public bt::PeerSource +{ +public: + ManualPeerSource() + { + } + ~ManualPeerSource() override + { + } + + void start() override + { + } + void stop(bt::WaitJob *) override + { + } + + void add(const QString &ip, bt::Uint16 port) + { + addPeer(net::Address(ip, port), false); + peersReady(this); + } +}; + +AddPeersDlg::AddPeersDlg(bt::TorrentInterface *tc, QWidget *parent) + : QDialog(parent) + , tc(tc) + , mps(nullptr) +{ + setupUi(this); + connect(m_close, &QPushButton::clicked, this, &AddPeersDlg::reject); + connect(m_add, &QPushButton::clicked, this, &AddPeersDlg::addPressed); + + KGuiItem::assign(m_close, KStandardGuiItem::close()); + KGuiItem::assign(m_add, KStandardGuiItem::add()); + + mps = new ManualPeerSource(); + tc->addPeerSource(mps); +} + +AddPeersDlg::~AddPeersDlg() +{ + tc->removePeerSource(mps); + delete mps; +} + +void AddPeersDlg::addPressed() +{ + mps->add(m_ip->text(), m_port->value()); +} + +} diff --git a/ktorrent/dialogs/addpeersdlg.h b/ktorrent/dialogs/addpeersdlg.h new file mode 100644 index 0000000..02fa8a5 --- /dev/null +++ b/ktorrent/dialogs/addpeersdlg.h @@ -0,0 +1,43 @@ +/* + SPDX-FileCopyrightText: 2007 Joris Guisson + SPDX-FileCopyrightText: 2007 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KTADDPEERSDLG_H +#define KTADDPEERSDLG_H + +#include "ui_addpeersdlg.h" +#include +#include + +namespace bt +{ +class TorrentInterface; +} + +namespace kt +{ +class ManualPeerSource; + +/** + Dialog to manually add peers to a torrent +*/ +class AddPeersDlg : public QDialog, public Ui_AddPeersDlg +{ + Q_OBJECT +public: + AddPeersDlg(bt::TorrentInterface *tc, QWidget *parent); + ~AddPeersDlg() override; + +private Q_SLOTS: + void addPressed(); + +private: + bt::TorrentInterface *tc; + ManualPeerSource *mps; +}; + +} + +#endif diff --git a/ktorrent/dialogs/addpeersdlg.ui b/ktorrent/dialogs/addpeersdlg.ui new file mode 100644 index 0000000..5d44e01 --- /dev/null +++ b/ktorrent/dialogs/addpeersdlg.ui @@ -0,0 +1,126 @@ + + + AddPeersDlg + + + + 0 + 0 + 423 + 144 + + + + Dialog + + + + + + Enter the IP address or hostname and port number of the peer you wish to add: + + + true + + + + + + + + + + + Peer: + + + + + + + <p>The IP address or hostname of the peer you want to connect to, should be put here.</p> +<p><span style=" font-weight:600;">Note:</span> We accept both IPv4 and IPv6 addresses</p> + + + + 127.0.0.1 + + + + + + + + + + + Port: + + + + + + + + 50 + 0 + + + + 1 + + + 65535 + + + 6881 + + + + + + + + + + + Qt::Horizontal + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Add + + + + + + + Close + + + + + + + + + + diff --git a/ktorrent/dialogs/fileselectdlg.cpp b/ktorrent/dialogs/fileselectdlg.cpp new file mode 100644 index 0000000..72abea5 --- /dev/null +++ b/ktorrent/dialogs/fileselectdlg.cpp @@ -0,0 +1,670 @@ +/* + SPDX-FileCopyrightText: 2005 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "fileselectdlg.h" + +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "settings.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace bt; + +namespace kt +{ +FileSelectDlg::FileSelectDlg(kt::QueueManager *qman, kt::GroupManager *gman, const QString &group_hint, QWidget *parent) + : QDialog(parent) + , tc(nullptr) + , model(nullptr) + , qman(qman) + , gman(gman) + , start(nullptr) + , skip_check(nullptr) + , initial_group(nullptr) + , show_file_tree(true) + , already_downloaded(0) +{ + setupUi(this); + connect(buttonBox, &QDialogButtonBox::accepted, this, &FileSelectDlg::accept); + connect(buttonBox, &QDialogButtonBox::rejected, this, &FileSelectDlg::reject); + + m_file_view->setAlternatingRowColors(true); + filter_model = new TreeFilterModel(this); + m_file_view->setModel(filter_model); + // root = 0; + connect(m_select_all, &QPushButton::clicked, this, &FileSelectDlg::selectAll); + connect(m_select_none, &QPushButton::clicked, this, &FileSelectDlg::selectNone); + connect(m_invert_selection, &QPushButton::clicked, this, &FileSelectDlg::invertSelection); + connect(m_collapse_all, &QPushButton::clicked, m_file_view, &QTreeView::collapseAll); + connect(m_expand_all, &QPushButton::clicked, m_file_view, &QTreeView::expandAll); + + m_downloadLocation->setMode(KFile::Directory | KFile::ExistingOnly | KFile::LocalOnly); + m_completedLocation->setMode(KFile::Directory | KFile::ExistingOnly | KFile::LocalOnly); + + m_download_location_history->setIcon(QIcon::fromTheme(QStringLiteral("view-history"))); + m_download_location_history->setPopupMode(QToolButton::MenuButtonPopup); + m_move_when_completed_history->setIcon(QIcon::fromTheme(QStringLiteral("view-history"))); + m_move_when_completed_history->setPopupMode(QToolButton::MenuButtonPopup); + + encodings = QTextCodec::availableMibs(); + for (int mib : qAsConst(encodings)) { + m_encoding->addItem(QString::fromUtf8(QTextCodec::codecForMib(mib)->name())); + } + + if (!group_hint.isNull()) + initial_group = gman->find(group_hint); + + QButtonGroup *bg = new QButtonGroup(this); + m_tree->setIcon(QIcon::fromTheme(QStringLiteral("view-list-tree"))); + m_tree->setToolTip(i18n("Show a file tree")); + connect(m_tree, &QToolButton::clicked, this, &FileSelectDlg::fileTree); + m_list->setIcon(QIcon::fromTheme(QStringLiteral("view-list-text"))); + m_list->setToolTip(i18n("Show a file list")); + connect(m_list, &QToolButton::clicked, this, &FileSelectDlg::fileList); + m_tree->setCheckable(true); + m_list->setCheckable(true); + bg->addButton(m_tree); + bg->addButton(m_list); + bg->setExclusive(true); + + m_filter->setClearButtonEnabled(true); + m_filter->setPlaceholderText(i18n("Filter")); + connect(m_filter, &QLineEdit::textChanged, this, &FileSelectDlg::setFilter); + + m_moveCompleted->setCheckState(Settings::useCompletedDir() ? Qt::Checked : Qt::Unchecked); + + m_completedLocation->setEnabled(Settings::useCompletedDir()); + connect(m_moveCompleted, &QCheckBox::toggled, this, &FileSelectDlg::moveCompletedToggled); +} + +FileSelectDlg::~FileSelectDlg() +{ +} + +int FileSelectDlg::execute(bt::TorrentInterface *tc, bool *start, bool *skip_check, const QString &location_hint) +{ + if (!tc) + return QDialog::Rejected; + + setWindowTitle(i18n("Opening %1", tc->getDisplayName())); + this->tc = tc; + this->start = start; + this->skip_check = skip_check; + + int idx = encodings.indexOf(tc->getTextCodec()->mibEnum()); + Out(SYS_GEN | LOG_DEBUG) << "Codec: " << QString::fromLatin1(tc->getTextCodec()->name()) << " " << idx << endl; + m_encoding->setCurrentIndex(idx); + connect(m_encoding, qOverload(&KComboBox::currentIndexChanged), this, &FileSelectDlg::onCodecChanged); + + for (Uint32 i = 0; i < tc->getNumFiles(); i++) { + bt::TorrentFileInterface &file = tc->getTorrentFile(i); + file.setEmitDownloadStatusChanged(false); + } + + populateFields(location_hint); + if (show_file_tree) + model = new TorrentFileTreeModel(tc, TorrentFileTreeModel::DELETE_FILES, this); + else + model = new TorrentFileListModel(tc, TorrentFileTreeModel::DELETE_FILES, this); + + model->setFileNamesEditable(true); + + connect(model, &TorrentFileModel::checkStateChanged, this, &FileSelectDlg::updateSizeLabels); + connect(m_downloadLocation, &KUrlRequester::textChanged, this, &FileSelectDlg::downloadLocationChanged); + filter_model->setSourceModel(model); + filter_model->setSortRole(Qt::UserRole); + m_file_view->setSortingEnabled(true); + m_file_view->expandAll(); + m_file_view->resizeColumnToContents(0); + + updateSizeLabels(); + + bool multi_file_torrent = tc->getStats().multi_file_torrent; + bool collapse_expand_enable = show_file_tree && multi_file_torrent; + m_collapse_all->setEnabled(collapse_expand_enable); + m_expand_all->setEnabled(collapse_expand_enable); + + m_select_all->setEnabled(multi_file_torrent); + m_select_none->setEnabled(multi_file_torrent); + m_invert_selection->setEnabled(multi_file_torrent); + m_expand_all->setEnabled(multi_file_torrent); + + m_file_view->setRootIsDecorated(collapse_expand_enable); + m_file_view->setAlternatingRowColors(false); + return exec(); +} + +void FileSelectDlg::reject() +{ + QDialog::reject(); +} + +void FileSelectDlg::accept() +{ + QStringList pe_ex; + + QString cn = m_completedLocation->url().toLocalFile(); + if (!cn.endsWith(QLatin1Char('/'))) + cn += QLatin1Char('/'); + if (m_moveCompleted->isChecked() && !cn.isEmpty()) { + move_on_completion_location_history.removeOne(cn); + move_on_completion_location_history.prepend(cn); + } + + QString dn = m_downloadLocation->url().toLocalFile(); + if (!dn.endsWith(QLatin1Char('/'))) + dn += QLatin1Char('/'); + if (!dn.isEmpty()) { + download_location_history.removeOne(dn); + download_location_history.prepend(dn); + } + + QString tld = tc->getUserModifiedFileName(); + // If the move on completion is on, check completed dir for files of the torrent + // but only if the completed directory is not selected + if (m_moveCompleted->checkState() == Qt::Checked && dn != cn) { + bool completed_files_found = false; + bool all_found = true; + QStringList cf; + if (tc->getStats().multi_file_torrent) { + for (Uint32 i = 0; i < tc->getNumFiles(); i++) { + bt::TorrentFileInterface &file = tc->getTorrentFile(i); + QString path = cn + tld + bt::DirSeparator() + file.getUserModifiedPath(); + if (bt::Exists(path)) { + completed_files_found = true; + cf.append(file.getUserModifiedPath()); + } else + all_found = false; + } + } else { + QString path = cn + tld; + completed_files_found = bt::Exists(path); + } + + if (completed_files_found) { + QString msg; + if (tc->getStats().multi_file_torrent) { + if (!all_found) + msg = i18n( + "Some files of this torrent have been found in the completed downloads directory. " // + "Do you want to import these files and use the completed downloads directory as the location?"); + else + msg = i18n( + "All files of this torrent have been found in the completed downloads directory. " // + "Do you want to import these files and use the completed downloads directory as the location?"); + } else + msg = i18n("The file %1 was found in the completed downloads directory. Do you want to import this file?", tld); + + // better ask the user if (s)he wants to delete the already existing data + int ret = KMessageBox::questionYesNoList(nullptr, msg, cf, QString()); + if (ret == KMessageBox::Yes) { + dn = cn; + } + } + } + + if (!bt::Exists(dn)) { + try { + if (KMessageBox::questionYesNo(this, i18n("The directory %1 does not exist, do you want to create it?", dn)) == KMessageBox::Yes) + MakePath(dn); + else + return; + } catch (bt::Error &err) { + KMessageBox::error(this, err.toString()); + QDialog::reject(); + return; + } + } + + if (!bt::Exists(cn)) { + try { + if (KMessageBox::questionYesNo(this, i18n("The directory %1 does not exist, do you want to create it?", cn)) == KMessageBox::Yes) + MakePath(cn); + else + return; + } catch (bt::Error &err) { + KMessageBox::error(this, err.toString()); + QDialog::reject(); + return; + } + } + + for (Uint32 i = 0; i < tc->getNumFiles(); i++) { + bt::TorrentFileInterface &file = tc->getTorrentFile(i); + + // check for preexisting files + QString path = dn + tld + bt::DirSeparator() + file.getUserModifiedPath(); + if (bt::Exists(path)) + file.setPreExisting(true); + + if (file.doNotDownload() && file.isPreExistingFile()) { + // we have excluded a preexisting file + pe_ex.append(file.getUserModifiedPath()); + } + file.setPathOnDisk(path); + } + + if (pe_ex.count() > 0) { + QString msg = i18n("You have deselected the following existing files. You will lose all data in these files, are you sure you want to do this?"); + // better ask the user if (s)he wants to delete the already existing data + int ret = KMessageBox::warningYesNoList(nullptr, msg, pe_ex, QString(), KGuiItem(i18n("Yes, delete the files")), KGuiItem(i18n("No, keep the files"))); + if (ret == KMessageBox::No) { + for (Uint32 i = 0; i < tc->getNumFiles(); i++) { + bt::TorrentFileInterface &file = tc->getTorrentFile(i); + if (file.doNotDownload() && file.isPreExistingFile()) + file.setDoNotDownload(false); + } + } + } + + // Setup custom download location + QString ddir = tc->getDataDir(); + if (!ddir.endsWith(bt::DirSeparator())) + ddir += bt::DirSeparator(); + + if (tc->getStats().multi_file_torrent && tld != tc->getStats().torrent_name) + tc->changeOutputDir(dn + tld, bt::TorrentInterface::FULL_PATH); + else if (dn != ddir) + tc->changeOutputDir(dn, 0); + + QStringList conflicting; + if (qman->checkFileConflicts(tc, conflicting)) { + QString err = i18n( + "Opening the torrent %1, " + "would share one or more files with the following torrents. " + "Torrents are not allowed to write to the same files. " + "Please select a different location.", + tc->getDisplayName()); + KMessageBox::errorList(this, err, conflicting); + return; + } + + for (Uint32 i = 0; i < tc->getNumFiles(); i++) { + bt::TorrentFileInterface &file = tc->getTorrentFile(i); + file.setEmitDownloadStatusChanged(true); + } + + // Make it user controlled if needed + *start = m_chkStartTorrent->isChecked(); + *skip_check = m_skip_data_check->isChecked(); + + // set display name for non-multifile torrent as file name inside + if (Settings::autoRenameSingleFileTorrents() && !tc->getStats().multi_file_torrent) + tc->setDisplayName(QFileInfo(tc->getUserModifiedFileName()).completeBaseName()); + + // Now add torrent to selected group + if (m_cmbGroups->currentIndex() > 0) { + QString groupName = m_cmbGroups->currentText(); + + Group *group = gman->find(groupName); + if (group) { + group->addTorrent(tc, true); + gman->saveGroups(); + } + } + + // Set this value after the group policy is applied, + // so that the user selection in the dialog is not + // overwritten by the group policy + if (m_moveCompleted->checkState() == Qt::Checked) + tc->setMoveWhenCompletedDir(cn); + else + tc->setMoveWhenCompletedDir(QString()); + + // update the last save directory + Settings::setLastSaveDir(dn); + Settings::self()->save(); + QDialog::accept(); +} + +QString FileSelectDlg::selectedGroup() const +{ + if (m_cmbGroups->currentIndex() == 0) + return QString(); + + return m_cmbGroups->currentText(); +} + +void FileSelectDlg::selectAll() +{ + model->checkAll(); +} + +void FileSelectDlg::selectNone() +{ + model->uncheckAll(); +} + +void FileSelectDlg::invertSelection() +{ + model->invertCheck(); +} + +void FileSelectDlg::populateFields(const QString &location_hint) +{ + QString dir; + QString comp_dir; + if (!location_hint.isEmpty() && QDir(location_hint).exists()) { + dir = location_hint; + } else { + dir = Settings::saveDir(); + if (!Settings::useSaveDir() || dir.isNull()) { + dir = Settings::lastSaveDir(); + if (dir.isNull()) + dir = QDir::homePath(); + } + } + + comp_dir = Settings::completedDir(); + if (!Settings::useCompletedDir() || comp_dir.isEmpty()) { + comp_dir = dir; + } + + m_downloadLocation->setUrl(QUrl::fromLocalFile(dir)); + m_completedLocation->setUrl(QUrl::fromLocalFile(comp_dir)); + loadGroups(); +} + +void FileSelectDlg::loadGroups() +{ + GroupManager::Itr it = gman->begin(); + + QStringList grps; + + // First default group + grps << i18n("All Torrents"); + + int cnt = 0; + int selected = 0; + // now custom ones + while (it != gman->end()) { + if (!it->second->isStandardGroup()) { + grps << it->first; + if (it->second == initial_group) + selected = cnt + 1; + cnt++; + } + ++it; + } + + m_cmbGroups->addItems(grps); + connect(m_cmbGroups, qOverload(&QComboBox::activated), this, &FileSelectDlg::groupActivated); + + if (selected > 0 && initial_group) { + m_cmbGroups->setCurrentIndex(selected); + QString dir = initial_group->groupPolicy().default_save_location; + if (!dir.isEmpty() && bt::Exists(dir)) + m_downloadLocation->setUrl(QUrl(dir)); + + dir = initial_group->groupPolicy().default_move_on_completion_location; + if (!dir.isEmpty() && bt::Exists(dir)) + m_completedLocation->setUrl(QUrl::fromLocalFile(dir)); + } +} + +void FileSelectDlg::groupActivated(int idx) +{ + if (idx == 0) + return; // No group selected + + // find the selected group + Group *g = gman->find(m_cmbGroups->itemText(idx)); + if (!g) + return; + + QString dir = g->groupPolicy().default_save_location; + if (!dir.isEmpty() && bt::Exists(dir)) + m_downloadLocation->setUrl(QUrl(dir)); + + dir = g->groupPolicy().default_move_on_completion_location; + if (!dir.isEmpty() && bt::Exists(dir)) { + m_moveCompleted->setChecked(true); + m_completedLocation->setUrl(QUrl::fromLocalFile(dir)); + } else { + m_moveCompleted->setChecked(false); + } +} + +void FileSelectDlg::updateSizeLabels() +{ + if (!model) + return; + + updateExistingFiles(); + + // calculate free disk space + QUrl sdir = m_downloadLocation->url(); + while (sdir.isValid() && sdir.isLocalFile() && (!sdir.isEmpty()) && (!QDir(sdir.toLocalFile()).exists())) { + sdir = KIO::upUrl(sdir); + } + + Uint64 bytes_free = 0; + if (!FreeDiskSpace(sdir.toLocalFile(), bytes_free)) { + lblRequired->setText(bt::BytesToString(model->bytesToDownload())); + lblFree->setText(i18n("Unable to determine free space")); + lblStatus->clear(); + } else { + Uint64 bytes_to_download = model->bytesToDownload(); + + lblFree->setText(bt::BytesToString(bytes_free)); + if (already_downloaded > 0) + lblRequired->setText(i18n("%1 (%2 in use by existing files)", bt::BytesToString(bytes_to_download), bt::BytesToString(already_downloaded))); + else + lblRequired->setText(bt::BytesToString(bytes_to_download)); + + bytes_to_download -= already_downloaded; + if (bytes_to_download > bytes_free) + lblStatus->setText( + QLatin1String("") + + i18nc("We are %1 bytes short of what we need", "%1 short", bt::BytesToString(-1 * (long long)(bytes_free - bytes_to_download)))); + else + lblStatus->setText(bt::BytesToString(bytes_free - bytes_to_download)); + } +} + +void FileSelectDlg::updateExistingFiles() +{ + if (tc->getStats().multi_file_torrent) { + already_downloaded = 0; + bt::Uint32 found = 0; + QString path = m_downloadLocation->url().path() + QLatin1Char('/') + tc->getDisplayName() + QLatin1Char('/'); + for (bt::Uint32 i = 0; i < tc->getNumFiles(); i++) { + const bt::TorrentFileInterface &file = tc->getTorrentFile(i); + if (bt::Exists(path + file.getUserModifiedPath())) { + found++; + if (!file.doNotDownload()) { // Do not include excluded files in the already downloaded calculation + bt::Uint64 size = bt::FileSize(path + file.getUserModifiedPath()); + if (size <= file.getSize()) + already_downloaded += file.getSize() - size; + else + already_downloaded += file.getSize(); + } + } + } + + if (found == 0) + m_existing_found->setText(i18n("Existing files: None")); + else if (found == tc->getNumFiles()) + m_existing_found->setText(i18n("Existing files: All")); + else + m_existing_found->setText(i18n("Existing files: %1 of %2", found, tc->getNumFiles())); + } else { + QString path = m_downloadLocation->url().path() + QLatin1Char('/') + tc->getDisplayName(); + if (!bt::Exists(path)) { + m_existing_found->setText(i18n("Existing file: No")); + } else { + already_downloaded = bt::FileSize(path); + m_existing_found->setText(i18n("Existing file: Yes")); + } + } +} + +void FileSelectDlg::downloadLocationChanged(const QString &path) +{ + Q_UNUSED(path); + updateSizeLabels(); +} + +void FileSelectDlg::onCodecChanged(const int index) +{ + const QString text = m_encoding->itemText(index); + QTextCodec *codec = QTextCodec::codecForName(text.toLocal8Bit()); + if (codec) { + tc->changeTextCodec(codec); + model->onCodecChange(); + } +} + +void FileSelectDlg::loadState(KSharedConfigPtr cfg) +{ + KConfigGroup g = cfg->group("FileSelectDlg"); + QSize s = g.readEntry("size", sizeHint()); + resize(s); + show_file_tree = g.readEntry("show_file_tree", true); + m_tree->setChecked(show_file_tree); + m_list->setChecked(!show_file_tree); + download_location_history = g.readEntry("download_location_history", QStringList()); + for (QString &s : download_location_history) + if (s.endsWith(QLatin1String("//"))) + s.chop(1); + download_location_history.removeDuplicates(); + move_on_completion_location_history = g.readEntry("move_on_completion_location_history", QStringList()); + move_on_completion_location_history.removeDuplicates(); + + if (download_location_history.count()) { + QMenu *m = createHistoryMenu(download_location_history, &FileSelectDlg::downloadLocationHistoryTriggered); + m_download_location_history->setMenu(m); + } else + m_download_location_history->setEnabled(false); + + if (move_on_completion_location_history.count()) { + QMenu *m = createHistoryMenu(move_on_completion_location_history, &FileSelectDlg::moveOnCompletionLocationHistoryTriggered); + m_move_when_completed_history->setMenu(m); + } else + m_move_when_completed_history->setEnabled(false); + + QByteArray state = g.readEntry("file_view", QByteArray()); + if (state.size() > 0) + m_file_view->header()->restoreState(state); +} + +void FileSelectDlg::saveState(KSharedConfigPtr cfg) +{ + KConfigGroup g = cfg->group("FileSelectDlg"); + g.writeEntry("size", size()); + g.writeEntry("show_file_tree", show_file_tree); + g.writeEntry("download_location_history", download_location_history); + g.writeEntry("move_on_completion_location_history", move_on_completion_location_history); + g.writeEntry("file_view", m_file_view->header()->saveState()); +} + +QMenu *FileSelectDlg::createHistoryMenu(const QStringList &urls, Func slot) +{ + QMenu *m = new QMenu(this); + for (const QString &url : urls) { + QAction *a = m->addAction(url); + a->setData(url); + } + m->addSeparator(); + m->addAction(i18n("Clear History")); + connect(m, &QMenu::triggered, this, slot); + return m; +} + +void FileSelectDlg::clearDownloadLocationHistory() +{ + download_location_history.clear(); + m_download_location_history->setEnabled(false); +} + +void FileSelectDlg::clearMoveOnCompletionLocationHistory() +{ + move_on_completion_location_history.clear(); + m_move_when_completed_history->setEnabled(false); +} + +void FileSelectDlg::downloadLocationHistoryTriggered(QAction *act) +{ + if (!act->data().isNull()) + m_downloadLocation->setUrl(QUrl::fromLocalFile(act->data().toString())); + else + clearDownloadLocationHistory(); +} +void FileSelectDlg::moveOnCompletionLocationHistoryTriggered(QAction *act) +{ + if (!act->data().isNull()) + m_completedLocation->setUrl(QUrl::fromLocalFile(act->data().toString())); + else + clearMoveOnCompletionLocationHistory(); +} + +void FileSelectDlg::fileTree(bool) +{ + setShowFileTree(true); +} + +void FileSelectDlg::fileList(bool) +{ + setShowFileTree(false); +} + +void FileSelectDlg::setShowFileTree(bool on) +{ + if (show_file_tree == on) + return; + + show_file_tree = on; + QByteArray hs = m_file_view->header()->saveState(); + + filter_model->setSourceModel(nullptr); + delete model; + if (show_file_tree) + model = new TorrentFileTreeModel(tc, TorrentFileTreeModel::DELETE_FILES, this); + else + model = new TorrentFileListModel(tc, TorrentFileTreeModel::DELETE_FILES, this); + + model->setFileNamesEditable(true); + connect(model, &TorrentFileModel::checkStateChanged, this, &FileSelectDlg::updateSizeLabels); + + filter_model->setSourceModel(model); + m_file_view->header()->restoreState(hs); + m_file_view->expandAll(); + m_file_view->setRootIsDecorated(show_file_tree && tc->getStats().multi_file_torrent); + + m_collapse_all->setEnabled(show_file_tree); + m_expand_all->setEnabled(show_file_tree); +} + +void FileSelectDlg::setFilter(const QString &f) +{ + Q_UNUSED(f); + filter_model->setFilterFixedString(m_filter->text()); +} + +void FileSelectDlg::moveCompletedToggled(bool on) +{ + m_completedLocation->setEnabled(on); +} +} diff --git a/ktorrent/dialogs/fileselectdlg.h b/ktorrent/dialogs/fileselectdlg.h new file mode 100644 index 0000000..b671afa --- /dev/null +++ b/ktorrent/dialogs/fileselectdlg.h @@ -0,0 +1,102 @@ +/* + SPDX-FileCopyrightText: 2005-2007 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef FILESELECTDLG_H +#define FILESELECTDLG_H + +#include +#include + +#include + +#include "ui_fileselectdlg.h" +#include + +namespace bt +{ +class TorrentInterface; +} + +namespace kt +{ +class GroupManager; +class QueueManager; +class TorrentFileModel; +class Group; + +/** + * @author Joris Guisson + * + * Dialog to select which files to download from a multifile torrent. + */ +class FileSelectDlg : public QDialog, public Ui_FileSelectDlg +{ + Q_OBJECT + + typedef void (FileSelectDlg::*Func)(QAction *); + +public: + FileSelectDlg(kt::QueueManager *qman, kt::GroupManager *gman, const QString &group_hint, QWidget *parent); + ~FileSelectDlg() override; + + int execute(bt::TorrentInterface *tc, bool *start, bool *skip_check, const QString &location_hint); + + /// Which group did the user select + QString selectedGroup() const; + + /** + * Load the state of the dialog + */ + void loadState(KSharedConfigPtr cfg); + + /** + * Save the state of the dialog + */ + void saveState(KSharedConfigPtr cfg); + +protected Q_SLOTS: + void reject() override; + void accept() override; + void selectAll(); + void selectNone(); + void invertSelection(); + void updateSizeLabels(); + void onCodecChanged(const int index); + void groupActivated(int idx); + void fileTree(bool on); + void fileList(bool on); + void setShowFileTree(bool on); + void setFilter(const QString &filter); + void updateExistingFiles(); + void moveCompletedToggled(bool on); + QMenu *createHistoryMenu(const QStringList &urls, Func slot); + void clearDownloadLocationHistory(); + void clearMoveOnCompletionLocationHistory(); + void downloadLocationHistoryTriggered(QAction *act); + void moveOnCompletionLocationHistoryTriggered(QAction *act); + void downloadLocationChanged(const QString &path); + +private: + void populateFields(const QString &location_hint); + void loadGroups(); + +private: + bt::TorrentInterface *tc; + TorrentFileModel *model; + kt::QueueManager *qman; + kt::GroupManager *gman; + bool *start; + bool *skip_check; + QList encodings; + kt::Group *initial_group; + bool show_file_tree; + QSortFilterProxyModel *filter_model; + QStringList download_location_history; + QStringList move_on_completion_location_history; + bt::Uint64 already_downloaded; +}; +} + +#endif diff --git a/ktorrent/dialogs/fileselectdlg.ui b/ktorrent/dialogs/fileselectdlg.ui new file mode 100644 index 0000000..8823331 --- /dev/null +++ b/ktorrent/dialogs/fileselectdlg.ui @@ -0,0 +1,400 @@ + + + FileSelectDlg + + + + 0 + 0 + 491 + 490 + + + + Select Which Files You Want to Download + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + Download to: + + + false + + + + + + + + + + History of all recently used download locations + + + ... + + + + + + + + + + + Moves the files to the specified location when the torrent finishes downloading. + + + Move when completed to: + + + + + + + + + + History of all recently used move when completed locations + + + ... + + + + + + + + + + + ... + + + + + + + ... + + + + + + + + + + Text encoding: + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + All files in the torrent, you can change them by double clicking on them. + + + true + + + + + + + + + Select &All + + + + + + + Select &None + + + + + + + Invert Selection + + + + + + + Collapse All + + + + + + + Expand All + + + + + + + Qt::Vertical + + + + 20 + 10 + + + + + + + + + + + + No existing files are found + + + + + + + + + Options + + + + + + + + + 0 + 0 + + + + Group: + + + false + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + true + + + + 0 + 0 + + + + Start torrent + + + true + + + + + + + <p>When existing files have been found, skip the data check and assume that the files are fully downloaded.</p> +<p><span style=" font-weight:600;">Note: </span>only do this when you are absolutely sure.</p> + + + Skip data check if existing files are found + + + + + + + + + + Disk Space + + + + + + + + Required: + + + false + + + + + + + Available: + + + false + + + + + + + After download: + + + false + + + + + + + + + + + false + + + + + + + false + + + + + + + false + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + Qt::Horizontal + + + + + + + + + KComboBox + QComboBox +
kcombobox.h
+
+ + KUrlRequester + QFrame +
kurlrequester.h
+ 1 +
+
+ + m_downloadLocation + m_cmbGroups + m_chkStartTorrent + m_select_all + m_select_none + m_invert_selection + + + +
diff --git a/ktorrent/dialogs/importdialog.cpp b/ktorrent/dialogs/importdialog.cpp new file mode 100644 index 0000000..104f92f --- /dev/null +++ b/ktorrent/dialogs/importdialog.cpp @@ -0,0 +1,373 @@ +/* + SPDX-FileCopyrightText: 2005 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include + +#include +#include +#include +#include +#include +#include + +#include "importdialog.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace bt; + +namespace kt +{ +ImportDialog::ImportDialog(CoreInterface *core, QWidget *parent) + : QDialog(parent) + , core(core) + , dc(nullptr) + , dc_thread(nullptr) + , canceled(false) +{ + setAttribute(Qt::WA_DeleteOnClose); + setupUi(this); + KUrlRequester *r = m_torrent_url; + r->setMode(KFile::File | KFile::LocalOnly); + r->setFilter(kt::TorrentFileFilter(true)); + + r = m_data_url; + r->setMode(KFile::File | KFile::Directory | KFile::LocalOnly); + + connect(m_import_btn, &QPushButton::clicked, this, &ImportDialog::onImport); + connect(m_cancel_btn, &QPushButton::clicked, this, &ImportDialog::cancelImport); + m_progress->setEnabled(false); + m_progress->setValue(0); + KGuiItem::assign(m_cancel_btn, KStandardGuiItem::cancel()); + m_import_btn->setIcon(QIcon::fromTheme(QStringLiteral("document-import"))); +} + +ImportDialog::~ImportDialog() +{ +} + +void ImportDialog::progress(quint32 num, quint32 total) +{ + m_progress->setMaximum(total); + m_progress->setValue(num); +} + +void ImportDialog::finished() +{ + QString data_dir = m_data_url->url().toLocalFile(); + QUrl tor_url = m_torrent_url->url(); + if (canceled || !dc_thread->getError().isEmpty()) { + if (!canceled) + KMessageBox::error(this, dc_thread->getError()); + dc_thread->deleteLater(); + dc_thread = nullptr; + reject(); + return; + } + + // find a new torrent dir and make it if necessary + QString tor_dir = core->findNewTorrentDir(); + if (!tor_dir.endsWith(bt::DirSeparator())) + tor_dir += bt::DirSeparator(); + + try { + if (!bt::Exists(tor_dir)) + bt::MakeDir(tor_dir); + + // write the index file + writeIndex(tor_dir + QStringLiteral("index"), dc->getResult()); + + // copy the torrent file + bt::CopyFile(tor_url.url(), tor_dir + QStringLiteral("torrent")); + + Uint64 imported = calcImportedBytes(dc->getResult(), tor); + + // make the cache + if (tor.isMultiFile()) { + QList dnd_files; + + // first make tor_dir/dnd + QString dnd_dir = tor_dir + QStringLiteral("dnd") + bt::DirSeparator(); + if (!bt::Exists(dnd_dir)) + MakeDir(dnd_dir); + + if (!data_dir.endsWith(bt::DirSeparator())) + data_dir += bt::DirSeparator(); + + for (Uint32 i = 0; i < tor.getNumFiles(); i++) { + TorrentFile &tf = tor.getFile(i); + makeDirs(dnd_dir, data_dir, tf.getPath()); + tf.setPathOnDisk(data_dir + tf.getPath()); + } + + saveFileMap(tor, tor_dir); + + QString durl = data_dir; + // if (durl.endsWith(bt::DirSeparator())) + durl.chop(1); + int ds = durl.lastIndexOf(bt::DirSeparator()); + if (durl.midRef(ds + 1) == tor.getNameSuggestion()) { + durl.truncate(ds); + saveStats(tor_dir + QStringLiteral("stats"), durl, imported, false); + } else { + saveStats(tor_dir + QStringLiteral("stats"), durl, imported, true); + } + saveFileInfo(tor_dir + QStringLiteral("file_info"), dnd_files); + } else { + // single file, just symlink the data_url to tor_dir/cache + QString durl = data_dir; + int ds = durl.lastIndexOf(bt::DirSeparator()); + durl.truncate(ds); + saveStats(tor_dir + QStringLiteral("stats"), durl, imported, false); + saveFileMap(tor_dir, data_dir); + } + + // everything went OK, so load the whole shabang and start downloading + core->loadExistingTorrent(tor_dir); + } catch (Error &e) { + // delete tor_dir + bt::Delete(tor_dir, true); + KMessageBox::error(this, e.toString()); + dc_thread->deleteLater(); + dc_thread = nullptr; + reject(); + return; + } + + dc_thread->deleteLater(); + dc_thread = nullptr; + accept(); +} + +void ImportDialog::import() +{ + // get the urls + QUrl tor_url = m_torrent_url->url(); + QUrl data_url = m_data_url->url(); + + // now we need to check the data + if (tor.isMultiFile()) { + dc = new MultiDataChecker(0, tor.getNumChunks()); + QString path = data_url.toLocalFile(); + if (!path.endsWith(bt::DirSeparator())) + path += bt::DirSeparator(); + + for (Uint32 i = 0; i < tor.getNumFiles(); i++) { + bt::TorrentFile &tf = tor.getFile(i); + tf.setPathOnDisk(path + tf.getPath()); + } + } else + dc = new SingleDataChecker(0, tor.getNumChunks()); + + connect(dc, &bt::DataChecker::progress, this, &ImportDialog::progress, Qt::QueuedConnection); + + BitSet bs(tor.getNumChunks()); + bs.setAll(false); + dc_thread = new DataCheckerThread(dc, bs, data_url.toLocalFile(), tor, QString()); + connect(dc_thread, &bt::DataCheckerThread::finished, this, &ImportDialog::finished, Qt::QueuedConnection); + dc_thread->start(); +} + +void ImportDialog::onTorrentGetReult(KJob *j) +{ + if (j->error()) { + j->uiDelegate()->showErrorMessage(); + reject(); + } else { + // try to load the torrent + try { + KIO::StoredTransferJob *stj = (KIO::StoredTransferJob *)j; + tor.load(stj->data(), false); + } catch (Error &e) { + KMessageBox::error(this, i18n("Cannot load the torrent file: %1", e.toString())); + reject(); + return; + } + import(); + } +} + +void ImportDialog::onImport() +{ + m_progress->setEnabled(true); + m_import_btn->setEnabled(false); + m_cancel_btn->setEnabled(false); + m_torrent_url->setEnabled(false); + m_data_url->setEnabled(false); + + QUrl tor_url = m_torrent_url->url(); + if (!tor_url.isLocalFile()) { + // download the torrent file + KIO::StoredTransferJob *j = KIO::storedGet(tor_url); + connect(j, &KIO::StoredTransferJob::result, this, &ImportDialog::onTorrentGetReult); + } else { + // try to load the torrent + try { + tor.load(bt::LoadFile(tor_url.toLocalFile()), false); + } catch (Error &e) { + KMessageBox::error(this, i18n("Cannot load the torrent file: %1", e.toString())); + reject(); + return; + } + import(); + } +} + +void ImportDialog::cancelImport() +{ + if (dc_thread) { + canceled = true; + dc->stop(); + dc_thread->wait(); + dc_thread->deleteLater(); + dc_thread = nullptr; + } + + reject(); +} + +void ImportDialog::writeIndex(const QString &file, const BitSet &chunks) +{ + // first try to open it + File fptr; + if (!fptr.open(file, QStringLiteral("wb"))) + throw Error(i18n("Cannot open %1: %2", file, fptr.errorString())); + + // write all chunks to the file + for (Uint32 i = 0; i < chunks.getNumBits(); i++) { + if (!chunks.get(i)) + continue; + + // we have the chunk so write a NewChunkHeader struct to the file + NewChunkHeader hdr; + hdr.index = i; + hdr.deprecated = 0; + fptr.write(&hdr, sizeof(NewChunkHeader)); + } +} + +void ImportDialog::makeDirs(const QString &dnd_dir, const QString &data_url, const QString &fpath) +{ + QStringList sl = fpath.split(bt::DirSeparator()); + + // create all necessary subdirs + QString otmp = data_url; + // if (!otmp.endsWith(bt::DirSeparator())) + // otmp += bt::DirSeparator(); + + QString dtmp = dnd_dir; + for (int i = 0; i < sl.count() - 1; i++) { + otmp += sl[i]; + dtmp += sl[i]; + if (!bt::Exists(otmp)) + MakeDir(otmp); + if (!bt::Exists(dtmp)) + MakeDir(dtmp); + otmp += bt::DirSeparator(); + dtmp += bt::DirSeparator(); + } +} + +void ImportDialog::saveStats(const QString &stats_file, const QString &data_dir, Uint64 imported, bool custom_output_name) +{ + QFile fptr(stats_file); + if (!fptr.open(QIODevice::WriteOnly)) { + Out(SYS_GEN | LOG_IMPORTANT) << "Warning : can't create stats file" << endl; + return; + } + + QTextStream out(&fptr); + out << "OUTPUTDIR=" << data_dir << Qt::endl; + out << "UPLOADED=0" << Qt::endl; + out << "RUNNING_TIME_DL=0" << Qt::endl; + out << "RUNNING_TIME_UL=0" << Qt::endl; + out << "PRIORITY=0" << Qt::endl; + out << "AUTOSTART=1" << Qt::endl; + if (Settings::maxRatio() > 0) + out << QStringLiteral("MAX_RATIO=%1").arg(Settings::maxRatio(), 0, 'f', 2) << Qt::endl; + out << QStringLiteral("IMPORTED=%1").arg(imported) << Qt::endl; + if (custom_output_name) + out << "CUSTOM_OUTPUT_NAME=1" << Qt::endl; +} + +Uint64 ImportDialog::calcImportedBytes(const bt::BitSet &chunks, const Torrent &tor) +{ + Uint64 nb = 0; + Uint64 ls = tor.getLastChunkSize(); + + for (Uint32 i = 0; i < chunks.getNumBits(); i++) { + if (!chunks.get(i)) + continue; + + if (i == chunks.getNumBits() - 1) + nb += ls; + else + nb += tor.getChunkSize(); + } + return nb; +} + +void ImportDialog::saveFileInfo(const QString &file_info_file, QList &dnd) +{ + // saves which TorrentFile's do not need to be downloaded + File fptr; + if (!fptr.open(file_info_file, QStringLiteral("wb"))) { + Out(SYS_GEN | LOG_IMPORTANT) << "Warning : Can't save chunk_info file : " << fptr.errorString() << endl; + return; + } + + ; + + // first write the number of excluded ones + Uint32 tmp = dnd.count(); + fptr.write(&tmp, sizeof(Uint32)); + // then all the excluded ones + for (int i = 0; i < dnd.count(); i++) { + tmp = dnd[i]; + fptr.write(&tmp, sizeof(Uint32)); + } + fptr.flush(); +} + +void ImportDialog::saveFileMap(const Torrent &tor, const QString &tor_dir) +{ + QString file_map = tor_dir + QLatin1String("file_map"); + QFile fptr(file_map); + if (!fptr.open(QIODevice::WriteOnly)) + throw Error(i18n("Failed to create %1: %2", file_map, fptr.errorString())); + + QTextStream out(&fptr); + + Uint32 num = tor.getNumFiles(); + for (Uint32 i = 0; i < num; i++) { + const TorrentFile &tf = tor.getFile(i); + out << tf.getPathOnDisk() << Qt::endl; + } +} + +void ImportDialog::saveFileMap(const QString &tor_dir, const QString &ddir) +{ + QString file_map = tor_dir + QLatin1String("file_map"); + QFile fptr(file_map); + if (!fptr.open(QIODevice::WriteOnly)) + throw Error(i18n("Failed to create %1: %2", file_map, fptr.errorString())); + + QTextStream out(&fptr); + out << ddir << Qt::endl; +} + +} diff --git a/ktorrent/dialogs/importdialog.h b/ktorrent/dialogs/importdialog.h new file mode 100644 index 0000000..d621b56 --- /dev/null +++ b/ktorrent/dialogs/importdialog.h @@ -0,0 +1,64 @@ +/* + SPDX-FileCopyrightText: 2005-2007 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef IMPORTDIALOG_H +#define IMPORTDIALOG_H + +#include "ui_importdialog.h" +#include +#include +#include + +class KJob; + +namespace bt +{ +class BitSet; +class Torrent; +class DataChecker; +class DataCheckerThread; +} + +namespace kt +{ +class CoreInterface; + +class ImportDialog : public QDialog, public Ui_ImportDialog +{ + Q_OBJECT + +public: + ImportDialog(CoreInterface *core, QWidget *parent = nullptr); + ~ImportDialog() override; + +public Q_SLOTS: + void onImport(); + void onTorrentGetReult(KJob *j); + +private Q_SLOTS: + void progress(quint32 num, quint32 total); + void finished(); + void cancelImport(); + +private: + void writeIndex(const QString &file, const bt::BitSet &chunks); + void makeDirs(const QString &dnd_dir, const QString &data_url, const QString &fpath); + void saveStats(const QString &stats_file, const QString &data_url, bt::Uint64 imported, bool custom_output_name); + bt::Uint64 calcImportedBytes(const bt::BitSet &chunks, const bt::Torrent &tor); + void saveFileInfo(const QString &file_info_file, QList &dnd); + void saveFileMap(const bt::Torrent &tor, const QString &tor_dir); + void saveFileMap(const QString &tor_dir, const QString &ddir); + void import(); + +private: + CoreInterface *core; + bt::DataChecker *dc; + bt::DataCheckerThread *dc_thread; + bt::Torrent tor; + bool canceled; +}; +} + +#endif diff --git a/ktorrent/dialogs/importdialog.ui b/ktorrent/dialogs/importdialog.ui new file mode 100644 index 0000000..fe22692 --- /dev/null +++ b/ktorrent/dialogs/importdialog.ui @@ -0,0 +1,133 @@ + + ImportDialog + + + + 0 + 0 + 473 + 156 + + + + Import an existing torrent + + + + + + + + + + + 60 + 0 + + + + Torrent: + + + false + + + + + + + + 60 + 0 + + + + Data: + + + false + + + + + + + + + + + + + + + + + + + + + Select the torrent file and the data which belongs with it. + + + + + + + 24 + + + + + + + Qt::Horizontal + + + + + + + + + Qt::Horizontal + + + QSizePolicy::Expanding + + + + 61 + 20 + + + + + + + + &Import + + + + + + + Ca&ncel + + + + + + + + + + + KUrlRequester + QWidget +
kurlrequester.h
+
+
+ + +
diff --git a/ktorrent/dialogs/missingfilesdlg.cpp b/ktorrent/dialogs/missingfilesdlg.cpp new file mode 100644 index 0000000..2843ece --- /dev/null +++ b/ktorrent/dialogs/missingfilesdlg.cpp @@ -0,0 +1,154 @@ +/* + SPDX-FileCopyrightText: 2007 Joris Guisson + SPDX-FileCopyrightText: 2007 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "missingfilesdlg.h" + +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include + +namespace kt +{ +MissingFilesDlg::MissingFilesDlg(const QString &text, const QStringList &missing, bt::TorrentInterface *tc, QWidget *parent) + : QDialog(parent) + , ret(CANCEL) + , tc(tc) +{ + setupUi(this); + + m_text->setText(text); + connect(m_cancel, &QPushButton::clicked, this, &MissingFilesDlg::cancelPressed); + connect(m_recreate, &QPushButton::clicked, this, &MissingFilesDlg::recreatePressed); + connect(m_dnd, &QPushButton::clicked, this, &MissingFilesDlg::dndPressed); + connect(m_select_new, &QPushButton::clicked, this, &MissingFilesDlg::selectNewPressed); + + KGuiItem::assign(m_cancel, KStandardGuiItem::cancel()); + + QMimeDatabase mimeDatabase; + for (const QString &s : missing) { + QListWidgetItem *lwi = new QListWidgetItem(m_file_list); + lwi->setText(s); + lwi->setIcon(QIcon::fromTheme(mimeDatabase.mimeTypeForFile(s).iconName())); + } + + m_dnd->setEnabled(tc->getStats().multi_file_torrent); + int disabled_files = 0; + for (bt::Uint32 i = 0; i < tc->getNumFiles(); i++) + if (tc->getTorrentFile(i).getPriority() == bt::EXCLUDED) + disabled_files++; + // select new is only possible if all files are missing + m_select_new->setEnabled(!tc->getStats().multi_file_torrent || missing.count() == (int)tc->getNumFiles() - disabled_files); +} + +MissingFilesDlg::~MissingFilesDlg() +{ +} + +void MissingFilesDlg::dndPressed() +{ + ret = DO_NOT_DOWNLOAD; + accept(); +} + +void MissingFilesDlg::selectNewPressed() +{ + if (tc->getStats().multi_file_torrent) { + QString recentDirClass; + QString dir = + QFileDialog::getExistingDirectory(this, + i18n("Select the directory where the data now is."), + KFileWidget::getStartUrl(QUrl(QStringLiteral("kfiledialog:///saveTorrentData")), recentDirClass).toLocalFile()); + + if (dir.isEmpty()) + return; + + if (!recentDirClass.isEmpty()) + KRecentDirs::add(recentDirClass, dir); + + QString old = tc->getStats().output_path; + tc->changeOutputDir(dir, bt::TorrentInterface::FULL_PATH); + QStringList dummy; + if (tc->hasMissingFiles(dummy)) { + int ans = KMessageBox::No; + if ((bt::Uint32)dummy.count() == tc->getNumFiles()) + ans = KMessageBox::questionYesNo( + this, + i18n("The data files are not present in the location you selected. Do you want to create all the files in the selected directory?")); + else + ans = KMessageBox::questionYesNo(this, + i18n("Not all files were found in the new location; some are still missing. Do you want to create the missing " + "files in the selected directory?")); + + if (ans == KMessageBox::Yes) { + tc->recreateMissingFiles(); + ret = NEW_LOCATION_SELECTED; + accept(); + } else + tc->changeOutputDir(old, bt::TorrentInterface::FULL_PATH); + } else { + ret = NEW_LOCATION_SELECTED; + accept(); + } + } else { + QString recentDirClass; + QString dir = + QFileDialog::getExistingDirectory(this, + i18n("Select the directory where the data now is."), + KFileWidget::getStartUrl(QUrl(QStringLiteral("kfiledialog:///saveTorrentData")), recentDirClass).toLocalFile()); + + if (dir.isEmpty()) + return; + + if (!recentDirClass.isEmpty()) + KRecentDirs::add(recentDirClass, dir); + + QString old = tc->getDataDir(); + tc->changeOutputDir(dir, 0); + QStringList dummy; + if (tc->hasMissingFiles(dummy)) { + if (KMessageBox::questionYesNo( + this, + i18n("The data file is not present in the location you selected. Do you want to create the file in the selected directory?")) + == KMessageBox::Yes) { + tc->recreateMissingFiles(); + ret = NEW_LOCATION_SELECTED; + accept(); + } else + tc->changeOutputDir(old, 0); + } else { + ret = NEW_LOCATION_SELECTED; + accept(); + } + } +} + +void MissingFilesDlg::recreatePressed() +{ + ret = RECREATE; + accept(); +} + +void MissingFilesDlg::cancelPressed() +{ + ret = CANCEL; + accept(); +} + +MissingFilesDlg::ReturnCode MissingFilesDlg::execute() +{ + exec(); + return ret; +} +} diff --git a/ktorrent/dialogs/missingfilesdlg.h b/ktorrent/dialogs/missingfilesdlg.h new file mode 100644 index 0000000..9b65ed1 --- /dev/null +++ b/ktorrent/dialogs/missingfilesdlg.h @@ -0,0 +1,63 @@ +/* + SPDX-FileCopyrightText: 2007 Joris Guisson + SPDX-FileCopyrightText: 2007 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KTMISSINGFILESDLG_H +#define KTMISSINGFILESDLG_H + +#include "ui_missingfilesdlg.h" +#include + +namespace bt +{ +class TorrentInterface; +} + +namespace kt +{ +/** + Dialog to show when files are missing. +*/ +class MissingFilesDlg : public QDialog, public Ui_MissingFilesDlg +{ + Q_OBJECT +public: + /** + * Constructor + * @param text Text to show above file list + * @param missing The list of missing files + * @param tc The torrent + * @param parent The parent widget + */ + MissingFilesDlg(const QString &text, const QStringList &missing, bt::TorrentInterface *tc, QWidget *parent); + ~MissingFilesDlg() override; + + enum ReturnCode { + RECREATE, + DO_NOT_DOWNLOAD, + CANCEL, + NEW_LOCATION_SELECTED, + }; + + /** + * Execute the dialog + * @return What to do + */ + ReturnCode execute(); + +private Q_SLOTS: + void dndPressed(); + void recreatePressed(); + void cancelPressed(); + void selectNewPressed(); + +private: + ReturnCode ret; + bt::TorrentInterface *tc; +}; + +} + +#endif diff --git a/ktorrent/dialogs/missingfilesdlg.ui b/ktorrent/dialogs/missingfilesdlg.ui new file mode 100644 index 0000000..d1d07df --- /dev/null +++ b/ktorrent/dialogs/missingfilesdlg.ui @@ -0,0 +1,92 @@ + + + MissingFilesDlg + + + + 0 + 0 + 515 + 268 + + + + Files are missing + + + + + + The following files are missing: + + + + + + + + + + + + + + They are no longer there, recreate them. + + + Recreate + + + + + + + Do not download the missing files. + + + Do Not Download + + + + + + + The files have been moved to another location, select the new location. + + + Select New Location + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Cancel, do not start the torrent. + + + Cancel + + + + + + + + + + + + diff --git a/ktorrent/dialogs/pastedialog.cpp b/ktorrent/dialogs/pastedialog.cpp new file mode 100644 index 0000000..aba0335 --- /dev/null +++ b/ktorrent/dialogs/pastedialog.cpp @@ -0,0 +1,140 @@ +/* + SPDX-FileCopyrightText: 2005 Joris Guisson joris.guisson@gmail.com + SPDX-FileCopyrightText: 2005 Ivan Vasic ivasic@gmail.com + SPDX-FileCopyrightText: 2020 Madhav Kanbur + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "pastedialog.h" +#include "core.h" +#include "settings.h" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include + +namespace kt +{ +PasteDialog::PasteDialog(Core *core, QWidget *parent, Qt::WindowFlags fl) + : QDialog(parent, fl) +{ + setupUi(this); + setWindowTitle(i18n("Open an URL")); + + m_core = core; + QClipboard *cb = QApplication::clipboard(); + QString text = cb->text(QClipboard::Clipboard); + + QUrl url = QUrl(text); + + connect(m_buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); + connect(m_buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); + + if (url.isValid()) + m_url->setText(text); + + loadGroups(); +} + +PasteDialog::~PasteDialog() +{ +} + +void PasteDialog::loadGroups() +{ + GroupManager *gman = m_core->getGroupManager(); + GroupManager::Itr it = gman->begin(); + QStringList grps; + // First default group + grps << i18n("All Torrents"); + + // now custom ones + while (it != gman->end()) { + if (!it->second->isStandardGroup()) + grps << it->first; + ++it; + } + + m_groups->addItems(grps); +} + +void PasteDialog::loadState(KSharedConfig::Ptr cfg) +{ + KConfigGroup g = cfg->group("PasteDlg"); + m_silently->setChecked(g.readEntry("silently", false)); + m_groups->setCurrentIndex(g.readEntry("group", 0)); +} + +void PasteDialog::saveState(KSharedConfig::Ptr cfg) +{ + KConfigGroup g = cfg->group("PasteDlg"); + g.writeEntry("silently", m_silently->isChecked()); + g.writeEntry("group", m_groups->currentIndex()); +} + +void PasteDialog::accept() +{ + QUrl url; + + // Handle Infohash case + QRegularExpression re(QStringLiteral("^([0-9a-fA-Z]{40}|[0-9a-fA-Z]{32})$")); + if (re.match(m_url->text()).hasMatch()) { + QString magnetLink = QStringLiteral("magnet:?xt=urn:btih:").append(m_url->text()); + + if (!Settings::trackerListUrl().isEmpty()) { + QNetworkAccessManager *manager = new QNetworkAccessManager(this); + QUrl trackerListUrl = QUrl(Settings::trackerListUrl()); + QNetworkReply *reply = manager->get(QNetworkRequest(trackerListUrl)); + + QEventLoop loop; + connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit); + loop.exec(); + + if (reply->error() == QNetworkReply::NoError) { + while (reply->canReadLine()) { + QString trackerUrl = QString::fromLatin1(reply->readLine()); + trackerUrl.chop(1); + if (!trackerUrl.isEmpty()) + magnetLink.append(QStringLiteral("&tr=")).append(trackerUrl); + } + } else { + QMessageBox::warning(this, + i18n("Error fetching tracker list"), + i18n("Please check if the URL in Settings > Advanced > Tracker list URL is reachable.")); + } + + delete manager; + } + + url = QUrl(magnetLink); + } else { + url = QUrl(m_url->text()); + } + + if (url.isValid()) { + QString group; + if (m_groups->currentIndex() > 0) + group = m_groups->currentText(); + + if (m_silently->isChecked()) + m_core->loadSilently(url, group); + else + m_core->load(url, group); + QDialog::accept(); + } else { + KMessageBox::error(this, i18n("Invalid URL: %1", m_url->text())); + } +} +} diff --git a/ktorrent/dialogs/pastedialog.h b/ktorrent/dialogs/pastedialog.h new file mode 100644 index 0000000..12395e2 --- /dev/null +++ b/ktorrent/dialogs/pastedialog.h @@ -0,0 +1,48 @@ +/* + SPDX-FileCopyrightText: 2005 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef PASTEDIALOG_H +#define PASTEDIALOG_H + +#include "ui_pastedlgbase.h" +#include +#include + +namespace kt +{ +class Core; + +/** + * @author Ivan Vasic + * @brief Torrent URL paste dialog + **/ +class PasteDialog : public QDialog, public Ui_PasteDlgBase +{ + Q_OBJECT +public: + PasteDialog(Core *core, QWidget *parent = nullptr, Qt::WindowFlags fl = {}); + ~PasteDialog() override; + + /** + * Load the state of the dialog + */ + void loadState(KSharedConfig::Ptr cfg); + + /** + * Save the state of the dialog + */ + void saveState(KSharedConfig::Ptr cfg); + +public Q_SLOTS: + void accept() override; + +private: + void loadGroups(); + +private: + Core *m_core; +}; +} +#endif diff --git a/ktorrent/dialogs/pastedlgbase.ui b/ktorrent/dialogs/pastedlgbase.ui new file mode 100644 index 0000000..ba07983 --- /dev/null +++ b/ktorrent/dialogs/pastedlgbase.ui @@ -0,0 +1,103 @@ + + + PasteDlgBase + + + + 0 + 0 + 478 + 148 + + + + Open an URL + + + + + + + + URL: + + + false + + + + + + + + 0 + 0 + + + + + 400 + 0 + + + + + + + + + + + + Open silently + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Group: + + + + + + + + + + + + + + Qt::Horizontal + + + + + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + diff --git a/ktorrent/dialogs/speedlimitsdlg.cpp b/ktorrent/dialogs/speedlimitsdlg.cpp new file mode 100644 index 0000000..baea637 --- /dev/null +++ b/ktorrent/dialogs/speedlimitsdlg.cpp @@ -0,0 +1,153 @@ +/* + SPDX-FileCopyrightText: 2007 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include +#include +#include + +#include + +#include "core.h" +#include "speedlimitsdlg.h" +#include "speedlimitsmodel.h" +#include "spinboxdelegate.h" +#include +#include +#include +#include +#include +#include + +using namespace bt; + +namespace kt +{ +SpeedLimitsDlg::SpeedLimitsDlg(bt::TorrentInterface *current, Core *core, QWidget *parent) + : QDialog(parent) + , core(core) + , current(current) +{ + setupUi(this); + setWindowIcon(QIcon::fromTheme(QStringLiteral("kt-speed-limits"))); + setWindowTitle(i18n("Speed Limits")); + + model = new SpeedLimitsModel(core, this); + QSortFilterProxyModel *pm = new QSortFilterProxyModel(this); + pm->setSourceModel(model); + pm->setSortRole(Qt::UserRole); + + m_speed_limits_view->setModel(pm); + m_speed_limits_view->setItemDelegate(new SpinBoxDelegate(this)); + m_speed_limits_view->setUniformRowHeights(true); + m_speed_limits_view->setSortingEnabled(true); + m_speed_limits_view->sortByColumn(0, Qt::AscendingOrder); + m_speed_limits_view->header()->setSortIndicatorShown(true); + m_speed_limits_view->header()->setSectionsClickable(true); + m_speed_limits_view->setAlternatingRowColors(true); + + QPushButton *apply_btn = m_buttonBox->button(QDialogButtonBox::Apply); + apply_btn->setEnabled(false); + connect(model, &SpeedLimitsModel::enableApply, apply_btn, &QPushButton::setEnabled); + connect(apply_btn, &QPushButton::clicked, this, &SpeedLimitsDlg::apply); + connect(m_buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); + connect(m_buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); + + m_upload_rate->setValue(Settings::maxUploadRate()); + m_download_rate->setValue(Settings::maxDownloadRate()); + connect(m_upload_rate, qOverload(&QSpinBox::valueChanged), this, &SpeedLimitsDlg::spinBoxValueChanged); + connect(m_download_rate, qOverload(&QSpinBox::valueChanged), this, &SpeedLimitsDlg::spinBoxValueChanged); + connect(m_filter, &QLineEdit::textChanged, pm, &QSortFilterProxyModel::setFilterFixedString); + loadState(); + + // if current is specified, select it and scroll to it + if (current) { + kt::QueueManager *qman = core->getQueueManager(); + int idx = 0; + QList::iterator itr = qman->begin(); + while (itr != qman->end()) { + if (*itr == current) + break; + + idx++; + itr++; + } + + if (itr != qman->end()) { + QItemSelectionModel *sel = m_speed_limits_view->selectionModel(); + QModelIndex midx = pm->mapFromSource(model->index(idx, 0)); + QModelIndex midx2 = pm->mapFromSource(model->index(idx, 4)); + sel->select(QItemSelection(midx, midx2), QItemSelectionModel::Select); + m_speed_limits_view->scrollTo(midx); + } + } +} + +SpeedLimitsDlg::~SpeedLimitsDlg() +{ +} + +void SpeedLimitsDlg::saveState() +{ + KConfigGroup g = KSharedConfig::openConfig()->group("SpeedLimitsDlg"); + QByteArray s = m_speed_limits_view->header()->saveState(); + g.writeEntry("view_state", s.toBase64()); + g.writeEntry("size", size()); +} + +void SpeedLimitsDlg::loadState() +{ + KConfigGroup g = KSharedConfig::openConfig()->group("SpeedLimitsDlg"); + QByteArray s = QByteArray::fromBase64(g.readEntry("view_state", QByteArray())); + if (!s.isEmpty()) { + m_speed_limits_view->header()->restoreState(s); + m_speed_limits_view->header()->setSortIndicatorShown(true); + m_speed_limits_view->header()->setSectionsClickable(true); + } + + QSize ws = g.readEntry("size", size()); + resize(ws); +} + +void SpeedLimitsDlg::accept() +{ + apply(); + saveState(); + QDialog::accept(); +} + +void SpeedLimitsDlg::reject() +{ + saveState(); + QDialog::reject(); +} + +void SpeedLimitsDlg::apply() +{ + model->apply(); + m_buttonBox->button(QDialogButtonBox::Apply)->setEnabled(false); + + bool apply = false; + if (Settings::maxUploadRate() != m_upload_rate->value()) { + Settings::setMaxUploadRate(m_upload_rate->value()); + apply = true; + } + + if (Settings::maxDownloadRate() != m_download_rate->value()) { + Settings::setMaxDownloadRate(m_download_rate->value()); + apply = true; + } + + if (apply) { + kt::ApplySettings(); + Settings::self()->save(); + } +} + +void SpeedLimitsDlg::spinBoxValueChanged(int) +{ + m_buttonBox->button(QDialogButtonBox::Apply)->setEnabled(true); +} + +} diff --git a/ktorrent/dialogs/speedlimitsdlg.h b/ktorrent/dialogs/speedlimitsdlg.h new file mode 100644 index 0000000..9a3b2bb --- /dev/null +++ b/ktorrent/dialogs/speedlimitsdlg.h @@ -0,0 +1,46 @@ +/* + SPDX-FileCopyrightText: 2007 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef SPEEDLIMITSDLG_H +#define SPEEDLIMITSDLG_H + +#include "ui_speedlimitsdlg.h" +#include + +namespace bt +{ +class TorrentInterface; +} + +namespace kt +{ +class Core; +class SpeedLimitsModel; + +/// Dialog to modify the speed limits of a torrent +class SpeedLimitsDlg : public QDialog, public Ui_SpeedLimitsDlg +{ + Q_OBJECT + +public: + SpeedLimitsDlg(bt::TorrentInterface *current, Core *core, QWidget *parent); + ~SpeedLimitsDlg() override; + +protected Q_SLOTS: + void accept() override; + void reject() override; + void apply(); + void spinBoxValueChanged(int); + void saveState(); + void loadState(); + +private: + Core *core; + SpeedLimitsModel *model; + bt::TorrentInterface *current; +}; +} + +#endif diff --git a/ktorrent/dialogs/speedlimitsdlg.ui b/ktorrent/dialogs/speedlimitsdlg.ui new file mode 100644 index 0000000..adb815c --- /dev/null +++ b/ktorrent/dialogs/speedlimitsdlg.ui @@ -0,0 +1,135 @@ + + + SpeedLimitsDlg + + + + 0 + 0 + 546 + 372 + + + + Speed Limits + + + + + + Speed limits for individual torrents (double click to edit): + + + false + + + + + + + + + Filter: + + + + + + + true + + + + + + + + + true + + + false + + + true + + + + + + + Global Limits + + + + + + Maximum download speed: + + + false + + + + + + + No limit + + + KiB/s + + + 1000000 + + + + + + + Maximum upload speed: + + + false + + + + + + + No limit + + + KiB/s + + + 1000000 + + + + + + + + + + QDialogButtonBox::Apply|QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + QFrame::HLine + + + QFrame::Sunken + + + + + + + + + diff --git a/ktorrent/dialogs/speedlimitsmodel.cpp b/ktorrent/dialogs/speedlimitsmodel.cpp new file mode 100644 index 0000000..8fb78d9 --- /dev/null +++ b/ktorrent/dialogs/speedlimitsmodel.cpp @@ -0,0 +1,233 @@ +/* + SPDX-FileCopyrightText: 2007 Joris Guisson + SPDX-FileCopyrightText: 2007 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include + +#include "core.h" +#include "speedlimitsmodel.h" +#include +#include +#include + +using namespace bt; + +namespace kt +{ +SpeedLimitsModel::SpeedLimitsModel(Core *core, QObject *parent) + : QAbstractTableModel(parent) + , core(core) +{ + kt::QueueManager *qman = core->getQueueManager(); + QList::iterator itr = qman->begin(); + while (itr != qman->end()) { + Limits lim; + bt::TorrentInterface *tc = *itr; + tc->getTrafficLimits(lim.up_original, lim.down_original); + lim.down = lim.down_original; + lim.up = lim.up_original; + tc->getAssuredSpeeds(lim.assured_up_original, lim.assured_down_original); + lim.assured_down = lim.assured_down_original; + lim.assured_up = lim.assured_up_original; + limits.insert(tc, lim); + itr++; + } + + connect(core, &Core::torrentAdded, this, &SpeedLimitsModel::onTorrentAdded); + connect(core, &Core::torrentRemoved, this, &SpeedLimitsModel::onTorrentRemoved); +} + +SpeedLimitsModel::~SpeedLimitsModel() +{ +} + +void SpeedLimitsModel::onTorrentAdded(bt::TorrentInterface *tc) +{ + Limits lim; + tc->getTrafficLimits(lim.up_original, lim.down_original); + lim.down = lim.down_original; + lim.up = lim.up_original; + limits.insert(tc, lim); + tc->getAssuredSpeeds(lim.assured_up_original, lim.assured_down_original); + lim.assured_down = lim.assured_down_original; + lim.assured_up = lim.assured_up_original; + insertRow(limits.count() - 1); +} + +void SpeedLimitsModel::onTorrentRemoved(bt::TorrentInterface *tc) +{ + kt::QueueManager *qman = core->getQueueManager(); + int idx = 0; + QList::iterator itr = qman->begin(); + while (itr != qman->end()) { + if (*itr == tc) + break; + idx++; + itr++; + } + + limits.remove(tc); + removeRow(idx); +} + +int SpeedLimitsModel::rowCount(const QModelIndex &parent) const +{ + if (parent.isValid()) + return 0; + else + return core->getQueueManager()->count(); +} + +int SpeedLimitsModel::columnCount(const QModelIndex &parent) const +{ + if (parent.isValid()) + return 0; + else + return 5; +} + +QVariant SpeedLimitsModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if (role != Qt::DisplayRole || orientation != Qt::Horizontal) + return QVariant(); + + switch (section) { + case 0: + return i18n("Torrent"); + case 1: + return i18n("Download Limit"); + case 2: + return i18n("Upload Limit"); + case 3: + return i18n("Assured Download Speed"); + case 4: + return i18n("Assured Upload Speed"); + default: + return QVariant(); + } +} + +QVariant SpeedLimitsModel::data(const QModelIndex &index, int role) const +{ + if (role != Qt::DisplayRole && role != Qt::EditRole && role != Qt::UserRole) + return QVariant(); + + bt::TorrentInterface *tc = torrentForIndex(index); + if (!tc) + return QVariant(); + + const Limits &lim = limits[tc]; + + switch (index.column()) { + case 0: + return tc->getDisplayName(); + case 1: + if (role == Qt::EditRole || role == Qt::UserRole) + return lim.down / 1024; + else + return lim.down == 0 ? i18n("No limit") : BytesPerSecToString(lim.down); + case 2: + if (role == Qt::EditRole || role == Qt::UserRole) + return lim.up / 1024; + else + return lim.up == 0 ? i18n("No limit") : BytesPerSecToString(lim.up); + case 3: + if (role == Qt::EditRole || role == Qt::UserRole) + return lim.assured_down / 1024; + else + return lim.assured_down == 0 ? i18n("No assured speed") : BytesPerSecToString(lim.assured_down); + case 4: + if (role == Qt::EditRole || role == Qt::UserRole) + return lim.assured_up / 1024; + else + return lim.assured_up == 0 ? i18n("No assured speed") : BytesPerSecToString(lim.assured_up); + default: + return QVariant(); + } +} + +bool SpeedLimitsModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + if (role != Qt::EditRole) + return false; + + bt::TorrentInterface *tc = torrentForIndex(index); + if (!tc || !limits.contains(tc)) + return false; + + bool ok = false; + Limits &lim = limits[tc]; + + switch (index.column()) { + case 1: + lim.down = value.toInt(&ok) * 1024; + break; + case 2: + lim.up = value.toInt(&ok) * 1024; + break; + case 3: + lim.assured_down = value.toInt(&ok) * 1024; + break; + case 4: + lim.assured_up = value.toInt(&ok) * 1024; + break; + } + + if (ok) { + Q_EMIT dataChanged(index, index); + if (lim.up != lim.up_original || lim.down != lim.down_original || lim.assured_down != lim.assured_down_original + || lim.up_original != lim.assured_up_original) { + enableApply(true); + } + } + + return ok; +} + +Qt::ItemFlags SpeedLimitsModel::flags(const QModelIndex &index) const +{ + if (!index.isValid()) + return Qt::ItemIsEnabled; + + if (index.column() > 0) + return QAbstractItemModel::flags(index) | Qt::ItemIsEditable; + else + return QAbstractItemModel::flags(index); +} + +bt::TorrentInterface *SpeedLimitsModel::torrentForIndex(const QModelIndex &index) const +{ + kt::QueueManager *qman = core->getQueueManager(); + int r = index.row(); + QList::iterator itr = qman->begin(); + itr += r; + + if (itr == qman->end()) + return nullptr; + else + return *itr; +} + +void SpeedLimitsModel::apply() +{ + QMap::iterator itr = limits.begin(); + while (itr != limits.end()) { + bt::TorrentInterface *tc = itr.key(); + Limits &lim = itr.value(); + if (lim.up != lim.up_original || lim.down != lim.down_original) { + tc->setTrafficLimits(lim.up, lim.down); + lim.up_original = lim.up; + lim.down_original = lim.down; + } + + if (lim.assured_up != lim.assured_up_original || lim.assured_down != lim.assured_down_original) { + tc->setAssuredSpeeds(lim.assured_up, lim.assured_down); + lim.assured_up_original = lim.assured_up; + lim.assured_down_original = lim.assured_down; + } + itr++; + } +} +} diff --git a/ktorrent/dialogs/speedlimitsmodel.h b/ktorrent/dialogs/speedlimitsmodel.h new file mode 100644 index 0000000..12c2205 --- /dev/null +++ b/ktorrent/dialogs/speedlimitsmodel.h @@ -0,0 +1,68 @@ +/* + SPDX-FileCopyrightText: 2007 Joris Guisson + SPDX-FileCopyrightText: 2007 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KTSPEEDLIMITSMODEL_H +#define KTSPEEDLIMITSMODEL_H + +#include + +namespace bt +{ +class TorrentInterface; +} + +namespace kt +{ +class Core; + +/** + * Model for the SpeedLimitsDlg main list view + */ +class SpeedLimitsModel : public QAbstractTableModel +{ + Q_OBJECT +public: + SpeedLimitsModel(Core *core, QObject *parent); + ~SpeedLimitsModel() override; + + int rowCount(const QModelIndex &parent) const override; + int columnCount(const QModelIndex &parent) const override; + QVariant headerData(int section, Qt::Orientation orientation, int role) const override; + QVariant data(const QModelIndex &index, int role) const override; + bool setData(const QModelIndex &index, const QVariant &value, int role) override; + Qt::ItemFlags flags(const QModelIndex &index) const override; + + void apply(); + +Q_SIGNALS: + void enableApply(bool on); + +private: + bt::TorrentInterface *torrentForIndex(const QModelIndex &index) const; + +private Q_SLOTS: + void onTorrentAdded(bt::TorrentInterface *tc); + void onTorrentRemoved(bt::TorrentInterface *tc); + +private: + struct Limits { + bt::Uint32 up; + bt::Uint32 up_original; + bt::Uint32 down; + bt::Uint32 down_original; + bt::Uint32 assured_up; + bt::Uint32 assured_up_original; + bt::Uint32 assured_down; + bt::Uint32 assured_down_original; + }; + + Core *core; + QMap limits; +}; + +} + +#endif diff --git a/ktorrent/dialogs/spinboxdelegate.cpp b/ktorrent/dialogs/spinboxdelegate.cpp new file mode 100644 index 0000000..af5e1dc --- /dev/null +++ b/ktorrent/dialogs/spinboxdelegate.cpp @@ -0,0 +1,68 @@ +/* + SPDX-FileCopyrightText: 2007 Joris Guisson + SPDX-FileCopyrightText: 2007 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include +#include + +#include + +#include "spinboxdelegate.h" + +namespace kt +{ +SpinBoxDelegate::SpinBoxDelegate(QObject *parent) + : QItemDelegate(parent) +{ +} + +SpinBoxDelegate::~SpinBoxDelegate() +{ +} + +QWidget *SpinBoxDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &, const QModelIndex &index) const +{ + QSpinBox *editor = new QSpinBox(parent); + editor->setSuffix(i18n(" KiB/s")); + if (index.column() < 3) + editor->setSpecialValueText(i18n("No limit")); + else + editor->setSpecialValueText(i18n("No assured speed")); + editor->setMinimum(0); + editor->setMaximum(10000000); + return editor; +} + +void SpinBoxDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const +{ + int value = index.model()->data(index, Qt::EditRole).toInt(); + QSpinBox *spinBox = static_cast(editor); + spinBox->setValue(value); +} + +void SpinBoxDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const +{ + QSpinBox *spinBox = static_cast(editor); + spinBox->interpretText(); + int value = spinBox->value(); + + model->setData(index, value, Qt::EditRole); +} + +void SpinBoxDelegate::updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex &) const +{ + QRect r = option.rect; + if (option.rect.height() < editor->sizeHint().height()) + r.setHeight(editor->sizeHint().height()); + editor->setGeometry(r); +} + +QSize SpinBoxDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const +{ + Q_UNUSED(option) + Q_UNUSED(index) + return QSpinBox().sizeHint(); +} +} diff --git a/ktorrent/dialogs/spinboxdelegate.h b/ktorrent/dialogs/spinboxdelegate.h new file mode 100644 index 0000000..bf454c8 --- /dev/null +++ b/ktorrent/dialogs/spinboxdelegate.h @@ -0,0 +1,33 @@ +/* + SPDX-FileCopyrightText: 2007 Joris Guisson + SPDX-FileCopyrightText: 2007 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KTSPINBOXDELEGATE_H +#define KTSPINBOXDELEGATE_H + +#include + +namespace kt +{ +/** + @author +*/ +class SpinBoxDelegate : public QItemDelegate +{ + Q_OBJECT + +public: + SpinBoxDelegate(QObject *parent = nullptr); + ~SpinBoxDelegate() override; + + QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const override; + void setEditorData(QWidget *editor, const QModelIndex &index) const override; + void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const override; + void updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex &index) const override; + QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override; +}; +} + +#endif diff --git a/ktorrent/dialogs/torrentcreatordlg.cpp b/ktorrent/dialogs/torrentcreatordlg.cpp new file mode 100644 index 0000000..aa664de --- /dev/null +++ b/ktorrent/dialogs/torrentcreatordlg.cpp @@ -0,0 +1,404 @@ +/* + SPDX-FileCopyrightText: 2007 Joris Guisson + SPDX-FileCopyrightText: 2007 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include + +#include + +#include +#include +#include + +#include "core.h" +#include "gui.h" +#include "torrentcreatordlg.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace bt; + +namespace kt +{ +TorrentCreatorDlg::TorrentCreatorDlg(Core *core, GUI *gui, QWidget *parent) + : QDialog(parent) + , core(core) + , gui(gui) + , mktor(nullptr) +{ + setAttribute(Qt::WA_DeleteOnClose); + tracker_completion = webseeds_completion = nodes_completion = nullptr; + setWindowTitle(i18n("Create A Torrent")); + setupUi(this); + adjustSize(); + loadGroups(); + + m_url->setMode(KFile::ExistingOnly | KFile::LocalOnly | KFile::Directory); + m_selectDirectory->setChecked(true); + + m_dht_tab->setEnabled(false); + + connect(m_buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); + connect(m_buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); + + connect(m_selectDirectory, &QRadioButton::clicked, this, &TorrentCreatorDlg::selectDirectory); + connect(m_selectFile, &QRadioButton::clicked, this, &TorrentCreatorDlg::selectFile); + + connect(m_dht, &QCheckBox::toggled, this, &TorrentCreatorDlg::dhtToggled); + + // tracker box stuff + connect(m_add_tracker, &QPushButton::clicked, this, &TorrentCreatorDlg::addTrackerPressed); + connect(m_tracker, &QLineEdit::returnPressed, this, &TorrentCreatorDlg::addTrackerPressed); + connect(m_remove_tracker, &QPushButton::clicked, this, &TorrentCreatorDlg::removeTrackerPressed); + connect(m_move_up, &QPushButton::clicked, this, &TorrentCreatorDlg::moveUpPressed); + connect(m_move_down, &QPushButton::clicked, this, &TorrentCreatorDlg::moveDownPressed); + connect(m_tracker, &QLineEdit::textChanged, this, &TorrentCreatorDlg::trackerTextChanged); + connect(m_tracker_list, &QListWidget::itemSelectionChanged, this, &TorrentCreatorDlg::trackerSelectionChanged); + m_add_tracker->setEnabled(false); // disable until there is text in m_tracker + m_remove_tracker->setEnabled(false); + m_move_up->setEnabled(false); + m_move_down->setEnabled(false); + + // dht box + connect(m_add_node, &QPushButton::clicked, this, &TorrentCreatorDlg::addNodePressed); + connect(m_node, &QLineEdit::returnPressed, this, &TorrentCreatorDlg::addNodePressed); + connect(m_remove_node, &QPushButton::clicked, this, &TorrentCreatorDlg::removeNodePressed); + connect(m_node, &QLineEdit::textChanged, this, &TorrentCreatorDlg::nodeTextChanged); + connect(m_node_list, &QTreeWidget::itemSelectionChanged, this, &TorrentCreatorDlg::nodeSelectionChanged); + m_add_node->setEnabled(false); + m_remove_node->setEnabled(false); + + // populate dht box with some nodes from our own table + const QMap n = bt::Globals::instance().getDHT().getClosestGoodNodes(10); + + for (QMap::const_iterator it = n.cbegin(); it != n.cend(); ++it) { + QTreeWidgetItem *twi = new QTreeWidgetItem(m_node_list); + twi->setText(0, it.key()); + twi->setText(1, QString::number(it.value())); + m_node_list->addTopLevelItem(twi); + } + + // webseed stuff + connect(m_add_webseed, &QPushButton::clicked, this, &TorrentCreatorDlg::addWebSeedPressed); + connect(m_remove_webseed, &QPushButton::clicked, this, &TorrentCreatorDlg::removeWebSeedPressed); + connect(m_webseed, &QLineEdit::textChanged, this, &TorrentCreatorDlg::webSeedTextChanged); + connect(m_webseed_list, &QListWidget::itemSelectionChanged, this, &TorrentCreatorDlg::webSeedSelectionChanged); + m_add_webseed->setEnabled(false); + m_remove_webseed->setEnabled(false); + + connect(&update_timer, &QTimer::timeout, this, &TorrentCreatorDlg::updateProgressBar); + loadCompleterData(); + m_progress->setValue(0); +} + +TorrentCreatorDlg::~TorrentCreatorDlg() +{ + tracker_completion->save(); + webseeds_completion->save(); + nodes_completion->save(); + + delete mktor; +} + +void TorrentCreatorDlg::loadGroups() +{ + GroupManager *gman = core->getGroupManager(); + GroupManager::Itr it = gman->begin(); + + QStringList grps; + + // First default group + grps << i18n("All Torrents"); + + // now custom ones + while (it != gman->end()) { + if (!it->second->isStandardGroup()) + grps << it->first; + ++it; + } + + m_group->addItems(grps); +} + +void TorrentCreatorDlg::loadCompleterData() +{ + QString file = kt::DataDir() + QStringLiteral("torrent_creator_known_trackers"); + tracker_completion = new StringCompletionModel(file, this); + tracker_completion->load(); + m_tracker->setCompleter(new QCompleter(tracker_completion, this)); + + file = kt::DataDir() + QStringLiteral("torrent_creator_known_webseeds"); + webseeds_completion = new StringCompletionModel(file, this); + webseeds_completion->load(); + m_webseed->setCompleter(new QCompleter(webseeds_completion, this)); + + file = kt::DataDir() + QStringLiteral("torrent_creator_known_nodes"); + nodes_completion = new StringCompletionModel(file, this); + nodes_completion->load(); + m_node->setCompleter(new QCompleter(nodes_completion, this)); +} + +void TorrentCreatorDlg::addTrackerPressed() +{ + if (m_tracker->text().length() > 0) { + tracker_completion->addString(m_tracker->text()); + m_tracker_list->addItem(m_tracker->text()); + m_tracker->clear(); + } +} + +void TorrentCreatorDlg::removeTrackerPressed() +{ + qDeleteAll(m_tracker_list->selectedItems()); +} + +void TorrentCreatorDlg::moveUpPressed() +{ + const QList sel = m_tracker_list->selectedItems(); + for (QListWidgetItem *s : sel) { + int r = m_tracker_list->row(s); + if (r > 0) { + m_tracker_list->insertItem(r - 1, m_tracker_list->takeItem(r)); + m_tracker_list->setCurrentRow(r - 1); + } + } +} + +void TorrentCreatorDlg::moveDownPressed() +{ + const QList sel = m_tracker_list->selectedItems(); + for (QListWidgetItem *s : sel) { + int r = m_tracker_list->row(s); + if (r + 1 < m_tracker_list->count()) { + m_tracker_list->insertItem(r + 1, m_tracker_list->takeItem(r)); + m_tracker_list->setCurrentRow(r + 1); + } + } +} + +void TorrentCreatorDlg::addNodePressed() +{ + if (m_node->text().length() > 0) { + QTreeWidgetItem *twi = new QTreeWidgetItem(m_node_list); + nodes_completion->addString(m_node->text()); + twi->setText(0, m_node->text()); + twi->setText(1, QString::number(m_port->value())); + m_node_list->addTopLevelItem(twi); + m_node->clear(); + } +} + +void TorrentCreatorDlg::removeNodePressed() +{ + qDeleteAll(m_node_list->selectedItems()); +} + +void TorrentCreatorDlg::dhtToggled(bool on) +{ + m_private->setEnabled(!on); + m_dht_tab->setEnabled(on); + m_tracker_tab->setEnabled(!on); +} + +void TorrentCreatorDlg::nodeTextChanged(const QString &str) +{ + m_add_node->setEnabled(str.length() > 0); +} + +void TorrentCreatorDlg::nodeSelectionChanged() +{ + m_remove_node->setEnabled(m_node_list->selectedItems().count() > 0); +} + +void TorrentCreatorDlg::trackerTextChanged(const QString &str) +{ + m_add_tracker->setEnabled(str.length() > 0); +} + +void TorrentCreatorDlg::trackerSelectionChanged() +{ + bool enable_buttons = m_tracker_list->selectedItems().count() > 0; + m_remove_tracker->setEnabled(enable_buttons); + m_move_up->setEnabled(enable_buttons); + m_move_down->setEnabled(enable_buttons); +} + +void TorrentCreatorDlg::addWebSeedPressed() +{ + QUrl url(m_webseed->text()); + if (!url.isValid()) { + KMessageBox::error(this, i18n("Invalid URL: %1", url.toDisplayString())); + return; + } + + if (url.scheme() != QLatin1String("http")) { + KMessageBox::error(this, i18n("Only HTTP is supported for webseeding.")); + return; + } + + webseeds_completion->addString(m_webseed->text()); + m_webseed_list->addItem(m_webseed->text()); + m_webseed->clear(); +} + +void TorrentCreatorDlg::removeWebSeedPressed() +{ + qDeleteAll(m_webseed_list->selectedItems()); +} + +void TorrentCreatorDlg::webSeedTextChanged(const QString &str) +{ + m_add_webseed->setEnabled(str.length() > 0); +} + +void TorrentCreatorDlg::webSeedSelectionChanged() +{ + m_remove_webseed->setEnabled(m_webseed_list->selectedItems().count() > 0); +} + +void TorrentCreatorDlg::accept() +{ + if (!m_url->url().isValid()) { + gui->errorMsg(i18n("You must select a file or a folder.")); + return; + } + + if (m_tracker_list->count() == 0 && !m_dht->isChecked()) { + QString msg = i18n( + "You have not added a tracker, " + "are you sure you want to create this torrent?"); + if (KMessageBox::warningYesNo(gui, msg) == KMessageBox::No) + return; + } + + if (m_node_list->topLevelItemCount() == 0 && m_dht->isChecked()) { + gui->errorMsg(i18n("You must add at least one node.")); + return; + } + + QUrl url = m_url->url(); + Uint32 chunk_size_table[] = {32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384}; + + int chunk_size = chunk_size_table[m_chunk_size->currentIndex()]; + QString name = url.toLocalFile(); + name = QFileInfo(name).isDir() ? QDir(name).dirName() : url.fileName(); + + QStringList trackers; + + if (m_dht->isChecked()) { + for (int i = 0; i < m_node_list->topLevelItemCount(); ++i) { + QTreeWidgetItem *item = m_node_list->topLevelItem(i); + trackers.append(item->text(0) + QLatin1Char(',') + item->text(1)); + } + } else { + for (int i = 0; i < m_tracker_list->count(); ++i) { + QListWidgetItem *item = m_tracker_list->item(i); + trackers.append(item->text()); + } + } + + QList webseeds; + for (int i = 0; i < m_webseed_list->count(); ++i) { + QListWidgetItem *item = m_webseed_list->item(i); + webseeds.append(QUrl(item->text())); + } + + try { + mktor = new bt::TorrentCreator(url.toLocalFile(), trackers, webseeds, chunk_size, name, m_comments->text(), m_private->isChecked(), m_dht->isChecked()); + + connect(mktor, &bt::TorrentCreator::finished, this, &TorrentCreatorDlg::hashCalculationDone, Qt::QueuedConnection); + mktor->start(); + setProgressBarEnabled(true); + update_timer.start(1000); + m_progress->setMaximum(mktor->getNumChunks()); + } catch (bt::Error &err) { + delete mktor; + mktor = nullptr; + Out(SYS_GEN | LOG_IMPORTANT) << "Error: " << err.toString() << endl; + gui->errorMsg(err.toString()); + } +} + +void TorrentCreatorDlg::hashCalculationDone() +{ + setProgressBarEnabled(false); + update_timer.stop(); + + QString recentDirClass; + QString s = QFileDialog::getSaveFileName(this, + i18n("Choose a file to save the torrent"), + KFileWidget::getStartUrl(QUrl(QStringLiteral("kfiledialog:///openTorrent")), recentDirClass).toLocalFile(), + kt::TorrentFileFilter(false)); + + if (s.isEmpty()) { + QDialog::reject(); + return; + } + + if (!recentDirClass.isEmpty()) + KRecentDirs::add(recentDirClass, QFileInfo(s).absolutePath()); + + if (!s.endsWith(QLatin1String(".torrent"))) + s += QLatin1String(".torrent"); + + mktor->saveTorrent(s); + bt::TorrentInterface *tc = core->createTorrent(mktor, m_start_seeding->isChecked()); + if (m_group->currentIndex() > 0 && tc) { + QString groupName = m_group->currentText(); + + GroupManager *gman = core->getGroupManager(); + Group *group = gman->find(groupName); + if (group) { + group->addTorrent(tc, true); + gman->saveGroups(); + } + } + + QDialog::accept(); +} + +void TorrentCreatorDlg::reject() +{ + if (mktor && mktor->isRunning()) { + disconnect(mktor, &bt::TorrentCreator::finished, this, &TorrentCreatorDlg::hashCalculationDone); + mktor->stop(); + mktor->wait(); + } + QDialog::reject(); +} + +void TorrentCreatorDlg::setProgressBarEnabled(bool on) +{ + m_progress->setEnabled(on); + m_options->setEnabled(!on); + m_url->setEnabled(!on); + m_tabs->setEnabled(!on); + m_comments->setEnabled(!on); + m_buttonBox->button(QDialogButtonBox::Ok)->setEnabled(!on); +} + +void TorrentCreatorDlg::updateProgressBar() +{ + m_progress->setValue(mktor->getCurrentChunk()); +} + +void TorrentCreatorDlg::selectFile() +{ + m_url->setMode(KFile::File | KFile::ExistingOnly | KFile::LocalOnly); +} + +void TorrentCreatorDlg::selectDirectory() +{ + m_url->setMode(KFile::ExistingOnly | KFile::LocalOnly | KFile::Directory); +} + +} diff --git a/ktorrent/dialogs/torrentcreatordlg.h b/ktorrent/dialogs/torrentcreatordlg.h new file mode 100644 index 0000000..c07d5b0 --- /dev/null +++ b/ktorrent/dialogs/torrentcreatordlg.h @@ -0,0 +1,80 @@ +/* + SPDX-FileCopyrightText: 2007 Joris Guisson + SPDX-FileCopyrightText: 2007 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KT_TORRENTCREATORDLG_HH +#define KT_TORRENTCREATORDLG_HH + +#include +#include + +#include "ui_torrentcreatordlg.h" +#include + +namespace kt +{ +class StringCompletionModel; +class Core; +class GUI; + +/** + * Dialog to create torrents with + */ +class TorrentCreatorDlg : public QDialog, public Ui_TorrentCreatorDlg +{ + Q_OBJECT +public: + TorrentCreatorDlg(Core *core, GUI *gui, QWidget *parent); + ~TorrentCreatorDlg() override; + +private Q_SLOTS: + void addTrackerPressed(); + void removeTrackerPressed(); + void moveUpPressed(); + void moveDownPressed(); + + void addWebSeedPressed(); + void removeWebSeedPressed(); + + void addNodePressed(); + void removeNodePressed(); + + void dhtToggled(bool on); + + void nodeTextChanged(const QString &str); + void nodeSelectionChanged(); + + void trackerTextChanged(const QString &str); + void trackerSelectionChanged(); + + void webSeedTextChanged(const QString &str); + void webSeedSelectionChanged(); + + void hashCalculationDone(); + void updateProgressBar(); + + void accept() override; + void reject() override; + + void selectFile(); // required for radio button for new torrent creation + void selectDirectory(); + +private: + void loadGroups(); + void loadCompleterData(); + void setProgressBarEnabled(bool on); + +private: + Core *core; + GUI *gui; + StringCompletionModel *tracker_completion; + StringCompletionModel *webseeds_completion; + StringCompletionModel *nodes_completion; + bt::TorrentCreator *mktor; + QTimer update_timer; +}; +} + +#endif diff --git a/ktorrent/dialogs/torrentcreatordlg.ui b/ktorrent/dialogs/torrentcreatordlg.ui new file mode 100644 index 0000000..c43b805 --- /dev/null +++ b/ktorrent/dialogs/torrentcreatordlg.ui @@ -0,0 +1,426 @@ + + + TorrentCreatorDlg + + + + 0 + 0 + 474 + 646 + + + + Create a torrent + + + + + + File or directory to create torrent from: + + + + + + + 0 + 0 + + + + + 0 + 0 + + + + + + + + + + Select Directory + + + + + + + Select File + + + + + + + + + + + + Options + + + + + + + + Size of each chunk: + + + + + + + 4 + + + + 32 KiB + + + + + 64 KiB + + + + + 128 KiB + + + + + 256 KiB + + + + + 512 KiB + + + + + 1 MiB + + + + + 2 MiB + + + + + 4 MiB + + + + + 8 MiB + + + + + 16 MiB + + + + + + + + + + Start seeding + + + true + + + + + + + Private torrent (DHT not allowed) + + + + + + + Decentralized (DHT only) + + + + + + + + + Add torrent to group: + + + + + + + + + + + + + + + 0 + + + + Trackers + + + + + + + + + Add + + + + + + + + + + + + Remove + + + + + + + Move Up + + + + + + + Move Down + + + + + + + Qt::Vertical + + + + 20 + 91 + + + + + + + + + + + DHT Nodes + + + + + + + + Node: + + + + + + + + + + Port: + + + + + + + 1 + + + 65535 + + + 6881 + + + + + + + + + Add + + + + + + + false + + + true + + + + IP or Hostname + + + + + Port + + + + + + + + + + Remove + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + Web Seeds + + + + + + + + + Add + + + + + + + + + + + + Remove + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + + + + + Comments: + + + + + + + + + + + + false + + + 24 + + + + + + + Qt::Horizontal + + + + + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + KUrlRequester + QWidget +
kurlrequester.h
+ 1 +
+
+ + +
diff --git a/ktorrent/groups/groupfiltermodel.cpp b/ktorrent/groups/groupfiltermodel.cpp new file mode 100644 index 0000000..46a4002 --- /dev/null +++ b/ktorrent/groups/groupfiltermodel.cpp @@ -0,0 +1,52 @@ +/* + SPDX-FileCopyrightText: 2008 Joris Guisson + SPDX-FileCopyrightText: 2008 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "groupfiltermodel.h" +#include "view/viewmodel.h" +#include + +namespace kt +{ +GroupFilterModel::GroupFilterModel(ViewModel *view_model, QObject *parent) + : QSortFilterProxyModel(parent) + , group(nullptr) + , view_model(view_model) +{ + setSourceModel(view_model); +} + +GroupFilterModel::~GroupFilterModel() +{ +} + +void GroupFilterModel::setGroup(Group *g) +{ + group = g; + invalidateFilter(); +} + +void GroupFilterModel::refilter() +{ + invalidateFilter(); +} + +bool GroupFilterModel::filterAcceptsColumn(int source_column, const QModelIndex &source_parent) const +{ + Q_UNUSED(source_column); + Q_UNUSED(source_parent); + return true; +} + +bool GroupFilterModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const +{ + Q_UNUSED(source_parent); + if (!group) + return true; + else + return group->isMember(view_model->torrentFromRow(source_row)); +} + +} diff --git a/ktorrent/groups/groupfiltermodel.h b/ktorrent/groups/groupfiltermodel.h new file mode 100644 index 0000000..8054d06 --- /dev/null +++ b/ktorrent/groups/groupfiltermodel.h @@ -0,0 +1,48 @@ +/* + SPDX-FileCopyrightText: 2008 Joris Guisson + SPDX-FileCopyrightText: 2008 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KTGROUPFILTERMODEL_H +#define KTGROUPFILTERMODEL_H + +#include + +namespace kt +{ +class Group; +class ViewModel; + +/** + Model to filter out torrents based upon group membership +*/ +class GroupFilterModel : public QSortFilterProxyModel +{ + Q_OBJECT +public: + GroupFilterModel(ViewModel *view_model, QObject *parent); + ~GroupFilterModel() override; + + bool filterAcceptsColumn(int source_column, const QModelIndex &source_parent) const override; + bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override; + + /** + * Set the group to filter + * @param g The Group + * */ + void setGroup(Group *g); + + /** + * Filter again. + */ + void refilter(); + +private: + Group *group; + ViewModel *view_model; +}; + +} + +#endif diff --git a/ktorrent/groups/grouppolicydlg.cpp b/ktorrent/groups/grouppolicydlg.cpp new file mode 100644 index 0000000..09bdfab --- /dev/null +++ b/ktorrent/groups/grouppolicydlg.cpp @@ -0,0 +1,61 @@ +/* + SPDX-FileCopyrightText: 2008 Joris Guisson + SPDX-FileCopyrightText: 2008 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "grouppolicydlg.h" +#include + +namespace kt +{ +GroupPolicyDlg::GroupPolicyDlg(Group *group, QWidget *parent) + : QDialog(parent) + , group(group) +{ + setupUi(this); + connect(buttonBox, &QDialogButtonBox::accepted, this, &GroupPolicyDlg::accept); + connect(buttonBox, &QDialogButtonBox::rejected, this, &GroupPolicyDlg::reject); + setWindowTitle(i18n("Policy for the %1 group", group->groupName())); + + const Group::Policy &p = group->groupPolicy(); + m_default_location_enabled->setChecked(!p.default_save_location.isEmpty()); + m_default_location->setEnabled(!p.default_save_location.isEmpty()); + m_default_location->setUrl(QUrl::fromLocalFile(p.default_save_location)); + m_default_location->setMode(KFile::Directory | KFile::ExistingOnly | KFile::LocalOnly); + + m_default_move_on_completion_enabled->setChecked(!p.default_move_on_completion_location.isEmpty()); + m_default_move_on_completion_location->setEnabled(!p.default_move_on_completion_location.isEmpty()); + m_default_move_on_completion_location->setUrl(QUrl::fromLocalFile(p.default_move_on_completion_location)); + m_default_move_on_completion_location->setMode(KFile::Directory | KFile::ExistingOnly | KFile::LocalOnly); + + m_only_new->setChecked(p.only_apply_on_new_torrents); + m_max_share_ratio->setValue(p.max_share_ratio); + m_max_seed_time->setValue(p.max_seed_time); + m_max_upload_rate->setValue(p.max_upload_rate); + m_max_download_rate->setValue(p.max_download_rate); +} + +GroupPolicyDlg::~GroupPolicyDlg() +{ +} + +void GroupPolicyDlg::accept() +{ + Group::Policy p; + if (m_default_location_enabled->isChecked() && m_default_location->url().isValid()) + p.default_save_location = m_default_location->url().toDisplayString(QUrl::PreferLocalFile); + + if (m_default_move_on_completion_enabled->isChecked() && m_default_move_on_completion_location->url().isValid()) + p.default_move_on_completion_location = m_default_move_on_completion_location->url().toDisplayString(QUrl::PreferLocalFile); + + p.only_apply_on_new_torrents = m_only_new->isChecked(); + p.max_share_ratio = m_max_share_ratio->value(); + p.max_seed_time = m_max_seed_time->value(); + p.max_upload_rate = m_max_upload_rate->value(); + p.max_download_rate = m_max_download_rate->value(); + group->setGroupPolicy(p); + QDialog::accept(); +} + +} diff --git a/ktorrent/groups/grouppolicydlg.h b/ktorrent/groups/grouppolicydlg.h new file mode 100644 index 0000000..acc96fb --- /dev/null +++ b/ktorrent/groups/grouppolicydlg.h @@ -0,0 +1,34 @@ +/* + SPDX-FileCopyrightText: 2008 Joris Guisson + SPDX-FileCopyrightText: 2008 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KTGROUPPOLICYDLG_H +#define KTGROUPPOLICYDLG_H + +#include "ui_grouppolicydlg.h" +#include + +namespace kt +{ +class Group; + +/** + @author +*/ +class GroupPolicyDlg : public QDialog, public Ui_GroupPolicyDlg +{ +public: + GroupPolicyDlg(Group *group, QWidget *parent); + ~GroupPolicyDlg() override; + + void accept() override; + +private: + Group *group; +}; + +} + +#endif diff --git a/ktorrent/groups/grouppolicydlg.ui b/ktorrent/groups/grouppolicydlg.ui new file mode 100644 index 0000000..54b8ec6 --- /dev/null +++ b/ktorrent/groups/grouppolicydlg.ui @@ -0,0 +1,229 @@ + + + GroupPolicyDlg + + + + 0 + 0 + 446 + 307 + + + + Dialog + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Default save location for torrents from this group. <br/><br/>This is only used in the file selection dialog, when you change the group, the download location in the dialog will be set to this. You can still override it, if you want to. + + + Default save location: + + + + + + + Default save location for torrents from this group. <br/><br/>This is only used in the file selection dialog, when you change the group, the download location in the dialog will be set to this. You can still override it, if you want to. + + + + + + + Default move when completed location: + + + + + + + Default move on completion location for torrents from this group. <br/><br/>This is only used in the file selection dialog, when you change the group, the move when completed location in the dialog will be set to this. You can still override it, if you want to. + + + + + + + Default settings for torrents which are added to the group. + + + Torrent Limits + + + + + + + + Maximum download rate: + + + + + + + No limit + + + KiB/s + + + 10000000 + + + + + + + Maximum upload rate: + + + + + + + No limit + + + KiB/s + + + 10000000 + + + + + + + Maximum seed time: + + + + + + + No limit + + + Hours + + + 10000000.000000000000000 + + + 0.010000000000000 + + + + + + + Maximum share ratio: + + + + + + + No limit + + + 10000000.000000000000000 + + + 0.010000000000000 + + + + + + + + + When this is enabled, these settings will only be applied when a torrent is added to the group in the file selection dialog or the torrent creation dialog.<p> +If this is not enabled, the settings will always be applied when you add a torrent to this group. + + + Apply settings only for newly opened or created torrents + + + + + + + + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + Qt::Horizontal + + + + + + + + KUrlRequester + QFrame +
kurlrequester.h
+ 1 +
+
+ + + + m_default_location_enabled + toggled(bool) + m_default_location + setEnabled(bool) + + + 90 + 19 + + + 394 + 27 + + + + + m_default_move_on_completion_enabled + toggled(bool) + m_default_move_on_completion_location + setEnabled(bool) + + + 98 + 39 + + + 310 + 47 + + + + +
diff --git a/ktorrent/groups/groupswitcher.cpp b/ktorrent/groups/groupswitcher.cpp new file mode 100644 index 0000000..270f4d9 --- /dev/null +++ b/ktorrent/groups/groupswitcher.cpp @@ -0,0 +1,244 @@ +/* + SPDX-FileCopyrightText: 2012 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include + +#include +#include +#include +#include +#include + +#include "grouppolicydlg.h" +#include "groupswitcher.h" +#include +#include +#include + +using namespace bt; + +namespace kt +{ +GroupSwitcher::GroupSwitcher(View *view, GroupManager *gman, QWidget *parent) + : QWidget(parent) + , new_tab(new QToolButton(this)) + , close_tab(new QToolButton(this)) + , edit_group_policy(new QToolButton(this)) + , tool_bar(new KToolBar(this)) + , action_group(new QActionGroup(this)) + , gman(gman) + , view(view) + , current_tab(0) +{ + QHBoxLayout *layout = new QHBoxLayout(this); + layout->addWidget(new_tab); + layout->addWidget(edit_group_policy); + layout->addWidget(tool_bar); + layout->addWidget(close_tab); + layout->setMargin(0); + + new_tab->setIcon(QIcon::fromTheme(QStringLiteral("list-add"))); + new_tab->setToolButtonStyle(Qt::ToolButtonIconOnly); + new_tab->setToolTip(i18n("Open a new tab")); + connect(new_tab, &QToolButton::clicked, this, &GroupSwitcher::newTab); + + close_tab->setIcon(QIcon::fromTheme(QStringLiteral("list-remove"))); + close_tab->setToolButtonStyle(Qt::ToolButtonIconOnly); + close_tab->setToolTip(i18n("Close the current tab")); + connect(close_tab, &QToolButton::clicked, this, qOverload<>(&GroupSwitcher::closeTab)); + + edit_group_policy->setIcon(QIcon::fromTheme(QStringLiteral("preferences-other"))); + edit_group_policy->setToolButtonStyle(Qt::ToolButtonIconOnly); + edit_group_policy->setToolTip(i18n("Edit Group Policy")); + connect(edit_group_policy, &QToolButton::clicked, this, &GroupSwitcher::editGroupPolicy); + + tool_bar->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); + action_group->setExclusive(true); + connect(action_group, &QActionGroup::triggered, this, &GroupSwitcher::onActivated); + + connect(gman, &GroupManager::groupRemoved, this, &GroupSwitcher::groupRemoved); +} + +GroupSwitcher::~GroupSwitcher() +{ +} + +void GroupSwitcher::loadState(KSharedConfig::Ptr cfg) +{ + KConfigGroup g = cfg->group("GroupSwitcher"); + + QStringList default_groups; + default_groups << QStringLiteral("/all") << QStringLiteral("/all/downloads") << QStringLiteral("/all/uploads"); + + const QStringList groups = g.readEntry("groups", default_groups); + for (const QString &group : groups) { + addTab(gman->findByPath(group)); + } + + if (tabs.isEmpty()) { + for (const QString &group : qAsConst(default_groups)) + addTab(gman->findByPath(group)); + } + + int idx = 0; + for (Tab &tab : tabs) { + tab.view_settings = g.readEntry(QStringLiteral("tab%1_settings").arg(idx++), view->defaultState()); + } + + updateGroupCount(); + connect(gman, &GroupManager::customGroupChanged, this, &GroupSwitcher::updateGroupCount); + + current_tab = g.readEntry("current_tab", 0); + if (current_tab >= 0 && current_tab < tabs.count()) { + Tab &ct = tabs[current_tab]; + ct.action->setChecked(true); + view->setGroup(ct.group); + view->restoreState(ct.view_settings); + } else { + tabs.first().action->setChecked(true); + view->setGroup(tabs.first().group); + view->restoreState(tabs.first().view_settings); + current_tab = 0; + } + + edit_group_policy->setEnabled(!tabs.at(current_tab).group->isStandardGroup()); +} + +void GroupSwitcher::saveState(KSharedConfig::Ptr cfg) +{ + KConfigGroup g = cfg->group("GroupSwitcher"); + QStringList groups; + int idx = 0; + for (Tab &tab : tabs) { + groups << tab.group->groupPath(); + if (idx == current_tab) + tab.view_settings = view->header()->saveState(); + g.writeEntry(QStringLiteral("tab%1_settings").arg(idx++), tab.view_settings); + } + + g.writeEntry("groups", groups); + g.writeEntry("current_tab", current_tab); +} + +void GroupSwitcher::addTab(Group *group) +{ + if (!group) + return; + + QString name = group->groupName() + QStringLiteral(" %1/%2").arg(group->runningTorrents()).arg(group->totalTorrents()); + QAction *action = tool_bar->addAction(group->groupIcon(), name); + action->setCheckable(true); + action_group->addAction(action); + tabs.append(Tab(group, action)); + + action->toggle(); + view->setGroup(group); + view->restoreState(view->defaultState()); + tabs.last().view_settings = view->header()->saveState(); + current_tab = tabs.count() - 1; + + close_tab->setEnabled(tabs.count() > 1); +} + +void GroupSwitcher::newTab() +{ + addTab(gman->allGroup()); +} + +GroupSwitcher::TabList::iterator GroupSwitcher::closeTab(TabList::iterator i) +{ + action_group->removeAction(i->action); + tool_bar->removeAction(i->action); + i->action->deleteLater(); + TabList::iterator ret = tabs.erase(i); + tabs.first().action->toggle(); + view->setGroup(tabs.first().group); + view->restoreState(tabs.first().view_settings); + current_tab = 0; + return ret; +} + +void GroupSwitcher::closeTab() +{ + if (tabs.size() <= 1) // Need at least one tab visible + return; + + TabList::iterator i = tabs.begin(); + while (i != tabs.end()) { + if (i->action->isChecked()) { + closeTab(i); + break; + } + i++; + } + + close_tab->setEnabled(tabs.count() > 1); +} + +void GroupSwitcher::onActivated(QAction *action) +{ + tabs[current_tab].view_settings = view->header()->saveState(); + + int idx = 0; + for (const Tab &tab : qAsConst(tabs)) { + if (tab.action == action) { + view->setGroup(tab.group); + view->restoreState(tab.view_settings); + current_tab = idx; + edit_group_policy->setEnabled(!tab.group->isStandardGroup()); + break; + } + idx++; + } +} + +void GroupSwitcher::currentGroupChanged(Group *group) +{ + for (Tab &tab : tabs) { + if (tab.action->isChecked()) { + tab.group = group; + QString name = group->groupName() + QStringLiteral(" %1/%2").arg(group->runningTorrents()).arg(group->totalTorrents()); + tab.action->setText(name); + tab.action->setIcon(group->groupIcon()); + edit_group_policy->setEnabled(!group->isStandardGroup()); + break; + } + } +} + +void GroupSwitcher::updateGroupCount() +{ + for (Tab &tab : tabs) + tab.action->setText(tab.group->groupName() + QStringLiteral(" %1/%2").arg(tab.group->runningTorrents()).arg(tab.group->totalTorrents())); +} + +void GroupSwitcher::groupRemoved(Group *group) +{ + for (TabList::iterator i = tabs.begin(); i != tabs.end();) { + if (i->group == group) { + if (tabs.size() > 1) { + i = closeTab(i); + } else { + i->group = gman->allGroup(); + i->action->setIcon(i->group->groupIcon()); + i->action->setText(i->group->groupName() + QStringLiteral(" %1/%2").arg(i->group->runningTorrents()).arg(i->group->totalTorrents())); + i++; + } + } else + i++; + } +} + +void GroupSwitcher::editGroupPolicy() +{ + Group *g = tabs[current_tab].group; + if (g) { + GroupPolicyDlg dlg(g, this); + if (dlg.exec() == QDialog::Accepted) + gman->saveGroups(); + } +} + +} diff --git a/ktorrent/groups/groupswitcher.h b/ktorrent/groups/groupswitcher.h new file mode 100644 index 0000000..7f1161e --- /dev/null +++ b/ktorrent/groups/groupswitcher.h @@ -0,0 +1,120 @@ +/* + SPDX-FileCopyrightText: 2012 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KT_GROUPSWITCHER_H +#define KT_GROUPSWITCHER_H + +#include + +#include +#include + +#include + +class QToolButton; + +namespace kt +{ +class View; + +/** + * toolbar to switch between groups + */ +class GroupSwitcher : public QWidget +{ + Q_OBJECT +public: + GroupSwitcher(View *view, GroupManager *gman, QWidget *parent); + ~GroupSwitcher() override; + + /** + * Load state of widget from config + * @param cfg The config + **/ + void loadState(KSharedConfig::Ptr cfg); + + /** + * Save state of widget to config + * @param cfg The config + **/ + void saveState(KSharedConfig::Ptr cfg); + +public Q_SLOTS: + /** + * Add a tab + * @param group The group of the tab + **/ + void addTab(Group *group); + + /** + * Add a new tab showing the all group. + **/ + void newTab(); + + /** + * Close the current tab + **/ + void closeTab(); + + /** + * An action was activated + * @param action The action + **/ + void onActivated(QAction *action); + + /** + * The current group has changed + * @param group The new group + **/ + void currentGroupChanged(kt::Group *group); + + /** + * Update the group count + */ + void updateGroupCount(); + + /** + * A group has been removed + * @param group The group + **/ + void groupRemoved(Group *group); + + /** + * Edit the group policy + **/ + void editGroupPolicy(); + +private: + struct Tab { + Group *group; + QAction *action; + QByteArray view_settings; + + Tab(Group *group, QAction *action) + : group(group) + , action(action) + { + } + }; + + typedef QList TabList; + + TabList::iterator closeTab(TabList::iterator i); + +private: + QToolButton *new_tab; + QToolButton *close_tab; + QToolButton *edit_group_policy; + KToolBar *tool_bar; + QActionGroup *action_group; + GroupManager *gman; + View *view; + TabList tabs; + int current_tab; +}; + +} + +#endif // KT_GROUPSWITCHER_H diff --git a/ktorrent/groups/groupview.cpp b/ktorrent/groups/groupview.cpp new file mode 100644 index 0000000..9ac5133 --- /dev/null +++ b/ktorrent/groups/groupview.cpp @@ -0,0 +1,194 @@ +/* + SPDX-FileCopyrightText: 2007 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "groupview.h" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "core.h" +#include "grouppolicydlg.h" +#include "gui.h" +#include "view/view.h" +#include +#include +#include +#include +#include +#include + +using namespace bt; + +namespace kt +{ +GroupView::GroupView(GroupManager *gman, View *view, Core *core, GUI *gui, QWidget *parent) + : QTreeView(parent) + , gui(gui) + , core(core) + , view(view) + , gman(gman) + , model(new GroupViewModel(gman, view, parent)) +{ + setRootIsDecorated(false); + setContextMenuPolicy(Qt::CustomContextMenu); + setModel(model); + header()->hide(); + + connect(this, &GroupView::clicked, this, &GroupView::onItemClicked); + connect(this, &GroupView::customContextMenuRequested, this, &GroupView::showContextMenu); + connect(this, &GroupView::currentGroupChanged, view, &View::onCurrentGroupChanged); + connect(gman, &GroupManager::customGroupChanged, this, &GroupView::updateGroupCount); + + setAcceptDrops(true); + setDropIndicatorShown(true); + setDragDropMode(QAbstractItemView::DropOnly); +} + +GroupView::~GroupView() +{ +} + +void GroupView::setupActions(KActionCollection *col) +{ + open_in_new_tab = new QAction(QIcon::fromTheme(QStringLiteral("list-add")), i18n("Open In New Tab"), this); + connect(open_in_new_tab, &QAction::triggered, this, &GroupView::openInNewTab); + col->addAction(QStringLiteral("open_in_new_tab"), open_in_new_tab); + + new_group = new QAction(QIcon::fromTheme(QStringLiteral("document-new")), i18n("New Group"), this); + connect(new_group, &QAction::triggered, this, &GroupView::addGroup); + col->addAction(QStringLiteral("new_group"), new_group); + + edit_group = new QAction(QIcon::fromTheme(QStringLiteral("insert-text")), i18n("Edit Name"), this); + connect(edit_group, &QAction::triggered, this, &GroupView::editGroupName); + col->addAction(QStringLiteral("edit_group_name"), edit_group); + + remove_group = new QAction(QIcon::fromTheme(QStringLiteral("edit-delete")), i18n("Remove Group"), this); + connect(remove_group, &QAction::triggered, this, &GroupView::removeGroup); + col->addAction(QStringLiteral("remove_group"), remove_group); + + edit_group_policy = new QAction(QIcon::fromTheme(QStringLiteral("preferences-other")), i18n("Group Policy"), this); + connect(edit_group_policy, &QAction::triggered, this, &GroupView::editGroupPolicy); + col->addAction(QStringLiteral("edit_group_policy"), edit_group_policy); +} + +void GroupView::addGroup() +{ + addNewGroup(); +} + +Group *GroupView::addNewGroup() +{ + bool ok = false; + QString name = QInputDialog::getText(this, QString(), i18n("Please enter the group name."), QLineEdit::Normal, QString(), &ok); + + if (name.isEmpty() || name.length() == 0 || !ok) + return nullptr; + + if (gman->find(name)) { + KMessageBox::error(this, i18n("The group %1 already exists.", name)); + return nullptr; + } + + Group *g = gman->newGroup(name); + gman->saveGroups(); + return g; +} + +void GroupView::removeGroup() +{ + Group *g = model->groupForIndex(selectionModel()->currentIndex()); + if (g) { + gman->removeGroup(g); + gman->saveGroups(); + } +} + +void GroupView::editGroupName() +{ + edit(selectionModel()->currentIndex()); +} + +void GroupView::showContextMenu(const QPoint &p) +{ + Group *g = model->groupForIndex(selectionModel()->currentIndex()); + + bool enable = g && gman->canRemove(g); + edit_group->setEnabled(enable); + remove_group->setEnabled(enable); + edit_group_policy->setEnabled(enable); + + open_in_new_tab->setEnabled(g != nullptr); + + QMenu *menu = gui->getTorrentActivity()->part()->menu(QStringLiteral("GroupsMenu")); + if (menu) + menu->popup(viewport()->mapToGlobal(p)); +} + +void GroupView::onItemClicked(const QModelIndex &index) +{ + Group *g = model->groupForIndex(index); + if (g) + currentGroupChanged(g); +} + +void GroupView::editGroupPolicy() +{ + Group *g = model->groupForIndex(selectionModel()->currentIndex()); + if (g) { + GroupPolicyDlg dlg(g, this); + if (dlg.exec() == QDialog::Accepted) + gman->saveGroups(); + } +} + +void GroupView::saveState(KSharedConfigPtr cfg) +{ + KConfigGroup g = cfg->group("GroupView"); + g.writeEntry("expanded", model->expandedGroups(this)); + g.writeEntry("visible", isVisible()); +} + +void GroupView::loadState(KSharedConfigPtr cfg) +{ + KConfigGroup g = cfg->group("GroupView"); + QStringList default_expanded; + default_expanded << QStringLiteral("/all") << QStringLiteral("/all/downloads") << QStringLiteral("/all/uploads") << QStringLiteral("/all/active") + << QStringLiteral("/all/passive") << QStringLiteral("/all/custom"); + QStringList slist = g.readEntry("expanded", default_expanded); + model->expandGroups(this, slist); + setVisible(g.readEntry("visible", true)); + expand(model->index(0, 0)); +} + +void GroupView::keyPressEvent(QKeyEvent *event) +{ + if (event->key() == Qt::Key_Enter || event->key() == Qt::Key_Return) + onItemClicked(selectionModel()->currentIndex()); + else + QTreeView::keyPressEvent(event); +} + +void GroupView::updateGroupCount() +{ + model->updateGroupCount(model->index(0, 0)); +} + +void GroupView::openInNewTab() +{ + Group *g = model->groupForIndex(selectionModel()->currentIndex()); + if (g) + openTab(g); +} +} diff --git a/ktorrent/groups/groupview.h b/ktorrent/groups/groupview.h new file mode 100644 index 0000000..4274897 --- /dev/null +++ b/ktorrent/groups/groupview.h @@ -0,0 +1,87 @@ +/* + SPDX-FileCopyrightText: 2007 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KTGROUPVIEW_H +#define KTGROUPVIEW_H + +#include +#include + +#include "groupviewmodel.h" + +class QAction; +class KActionCollection; + +namespace kt +{ +class GUI; +class Core; +class View; +class Group; +class GroupView; +class GroupManager; +class View; + +/** + @author Joris Guisson +*/ +class GroupView : public QTreeView +{ + Q_OBJECT +public: + GroupView(GroupManager *gman, View *view, Core *core, GUI *gui, QWidget *parent); + ~GroupView() override; + + /// Save the status of the group view + void saveState(KSharedConfigPtr cfg); + + /// Load status from config + void loadState(KSharedConfigPtr cfg); + + /// Create a new group + Group *addNewGroup(); + + /// Setup all the actions of the GroupView + void setupActions(KActionCollection *col); + +public Q_SLOTS: + /// Update the group count + void updateGroupCount(); + +private Q_SLOTS: + void onItemClicked(const QModelIndex &index); + void showContextMenu(const QPoint &p); + void addGroup(); + void removeGroup(); + void editGroupName(); + void editGroupPolicy(); + void openInNewTab(); + +Q_SIGNALS: + void currentGroupChanged(kt::Group *g); + void openTab(Group *g); + +private: + void keyPressEvent(QKeyEvent *event) override; + +private: + GUI *gui; + Core *core; + View *view; + GroupManager *gman; + GroupViewModel *model; + + QAction *open_in_new_tab; + QAction *new_group; + QAction *edit_group; + QAction *remove_group; + QAction *edit_group_policy; + + friend class GroupViewItem; +}; + +} + +#endif diff --git a/ktorrent/groups/groupviewmodel.cpp b/ktorrent/groups/groupviewmodel.cpp new file mode 100644 index 0000000..507077c --- /dev/null +++ b/ktorrent/groups/groupviewmodel.cpp @@ -0,0 +1,412 @@ +/* + SPDX-FileCopyrightText: 2012 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "groupviewmodel.h" +#include "groupview.h" + +#include + +#include +#include + +#include +#include +#include +#include +#include +#include + +using namespace bt; + +namespace kt +{ +GroupViewModel::GroupViewModel(kt::GroupManager *gman, View *view, QObject *parent) + : QAbstractItemModel(parent) + , root(QStringLiteral("all"), nullptr, 0, this) + , gman(gman) + , view(view) +{ + for (GroupManager::CItr i = gman->begin(); i != gman->end(); i++) + root.insert(i->second, index(0, 0)); + + root.insert(i18n("Custom Groups"), QStringLiteral("/all/custom"), index(0, 0)); + // root.dump(); + + connect(gman, &GroupManager::groupRemoved, this, &GroupViewModel::groupRemoved); + connect(gman, &GroupManager::groupAdded, this, &GroupViewModel::groupAdded); +} + +GroupViewModel::~GroupViewModel() +{ +} + +QVariant GroupViewModel::data(const QModelIndex &index, int role) const +{ + Item *item = (Item *)index.internalPointer(); + if (!item) + return QVariant(); + + switch (role) { + case Qt::DisplayRole: + return item->displayData(); + case Qt::DecorationRole: + return item->decoration(); + } + + return QVariant(); +} + +bool GroupViewModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + if (role != Qt::EditRole) + return false; + + Item *item = (Item *)index.internalPointer(); + if (!item) + return false; + + Group *group = item->group; + QString new_name = value.toString(); + if (new_name.isEmpty() || gman->find(new_name)) + return false; + + item->name = new_name; + gman->renameGroup(group->groupName(), new_name); + dataChanged(index, index); + return true; +} + +int GroupViewModel::columnCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent); + return 1; +} + +int GroupViewModel::rowCount(const QModelIndex &parent) const +{ + if (!parent.isValid()) + return 1; + + Item *item = (Item *)parent.internalPointer(); + if (!item) + return 0; + else + return item->children.size(); +} + +QModelIndex GroupViewModel::parent(const QModelIndex &child) const +{ + Item *item = (Item *)child.internalPointer(); + if (!item || !item->parent) + return QModelIndex(); + else + return createIndex(item->parent->row, 0, (void *)item->parent); +} + +QModelIndex GroupViewModel::index(int row, int column, const QModelIndex &parent) const +{ + if (!parent.isValid()) + return createIndex(row, column, (void *)&root); + + Item *item = (Item *)parent.internalPointer(); + if (!item || row < 0 || row >= item->children.count()) + return QModelIndex(); + + return createIndex(row, column, (void *)&item->children.at(row)); +} + +bool GroupViewModel::dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) +{ + Q_UNUSED(data); + Q_UNUSED(action); + if (row != -1 || column != -1) + return false; + + TorrentGroup *g = dynamic_cast(groupForIndex(parent)); + if (!g) + return false; + + QList sel; + view->getSelection(sel); + for (TorrentInterface *ti : qAsConst(sel)) { + g->addTorrent(ti, false); + } + gman->saveGroups(); + return true; +} + +Qt::DropActions GroupViewModel::supportedDropActions() const +{ + return Qt::CopyAction; +} + +QStringList GroupViewModel::mimeTypes() const +{ + QStringList sl; + sl << QStringLiteral("application/x-ktorrent-drag-object"); + return sl; +} + +Qt::ItemFlags GroupViewModel::flags(const QModelIndex &index) const +{ + Item *item = (Item *)index.internalPointer(); + if (item && item->group && !item->group->isStandardGroup()) + return Qt::ItemIsEnabled | Qt::ItemIsEditable | Qt::ItemIsDropEnabled; + else + return Qt::ItemIsEnabled; +} + +void GroupViewModel::groupAdded(Group *g) +{ + root.insert(g, index(0, 0)); +} + +void GroupViewModel::groupRemoved(Group *g) +{ + // QModelIndex idx = findGroup(g).parent(); + root.remove(g, index(0, 0)); + // root.dump(); + view->onGroupRemoved(g); +} + +Group *GroupViewModel::groupForIndex(const QModelIndex &index) const +{ + Item *item = (Item *)index.internalPointer(); + return item ? item->group : nullptr; +} + +QModelIndex GroupViewModel::findGroup(Group *g) +{ + QModelIndex idx = index(0, 0); + return root.findGroup(g, idx); +} + +QStringList GroupViewModel::expandedGroups(GroupView *gview) +{ + QStringList ret; + QModelIndex idx = createIndex(0, 0, &root); + root.expandedGroups(gview, ret, idx); + return ret; +} + +void GroupViewModel::expandGroups(GroupView *gview, const QStringList &groups) +{ + QModelIndex idx = createIndex(0, 0, &root); + root.expandGroups(gview, groups, idx); +} + +void GroupViewModel::updateGroupCount(const QModelIndex &idx) +{ + int row = 0; + QModelIndex child = this->index(row, 0, idx); + while (child.isValid()) { + updateGroupCount(child); + row++; + child = this->index(row, 0, idx); + } + + dataChanged(idx, idx); +} + +bool GroupViewModel::removeRows(int row, int count, const QModelIndex &parent) +{ + Item *item = (Item *)parent.internalPointer(); + if (!item) + return false; + + beginRemoveRows(parent, row, row + count); + for (int i = 0; i < count; i++) + item->children.removeAt(row); + int row_index = 0; + for (Item &i : item->children) + i.row = row_index++; + endRemoveRows(); + return true; +} + +/////////////////////////////////////////:: + +GroupViewModel::Item::Item(const QString &name, kt::GroupViewModel::Item *parent, int row, kt::GroupViewModel *model) + : name(name) + , display_name(name) + , parent(parent) + , row(row) + , group(nullptr) + , model(model) +{ +} + +void GroupViewModel::Item::insert(Group *g, const QModelIndex &idx) +{ + QString group_path = g->groupPath(); + QString item_path = path(); + if (!group_path.startsWith(item_path)) + return; + + QString remainder = group_path.remove(0, item_path.size()); + if (remainder.isEmpty()) { + group = g; + } else { + QString child_name; + if (remainder.indexOf(QLatin1Char('/')) == -1) + child_name = remainder; + else + child_name = remainder.section(QLatin1Char('/'), 1, 1); + + QList::iterator i = std::find(children.begin(), children.end(), child_name); + if (i == children.end()) { + int npos = children.count(); + model->beginInsertRows(idx, npos, npos); + children.append(Item(child_name, this, npos, model)); + children.last().insert(g, model->index(npos, 0, idx)); + model->endInsertRows(); + } else + i->insert(g, model->index(i->row, 0, idx)); + } +} + +void GroupViewModel::Item::insert(const QString &name, const QString &p, const QModelIndex &idx) +{ + QString item_path = path(); + if (!p.startsWith(item_path)) + return; + + QString tmp = p; + QString remainder = tmp.remove(0, item_path.size()); + if (remainder.isEmpty()) { + display_name = name; + } else { + QString child_name; + if (remainder.indexOf(QLatin1Char('/')) == -1) + child_name = remainder; + else + child_name = remainder.section(QLatin1Char('/'), 1, 1); + + QList::iterator i = std::find(children.begin(), children.end(), child_name); + if (i == children.end()) { + int npos = children.count(); + model->beginInsertRows(idx, npos, npos); + children.append(Item(child_name, this, npos, model)); + children.last().insert(name, p, model->index(npos, 0, idx)); + model->endInsertRows(); + } else + i->insert(name, p, model->index(i->row, 0, idx)); + } +} + +void GroupViewModel::Item::remove(kt::Group *g, const QModelIndex &idx) +{ + QString group_path = g->groupPath(); + QString item_path = path(); + if (!group_path.startsWith(item_path)) + return; + + QString remainder = group_path.remove(0, item_path.size()); + if (remainder.count(QLatin1Char('/')) == 1) { + QList::iterator i = std::find(children.begin(), children.end(), remainder.mid(1)); + if (i != children.end()) { + model->removeRows(i->row, 1, idx); + } + } else { + QString child_name = remainder.section(QLatin1Char('/'), 1, 1); + QList::iterator i = std::find(children.begin(), children.end(), child_name); + if (i != children.end()) + i->remove(g, model->index(i->row, 0, idx)); + } +} + +bool GroupViewModel::Item::operator==(const QString &n) const +{ + return name == n; +} + +QVariant GroupViewModel::Item::displayData() +{ + if (group) + return QStringLiteral("%1 (%2/%3)").arg(group->groupName()).arg(group->runningTorrents()).arg(group->totalTorrents()); + else + return display_name; +} + +QVariant GroupViewModel::Item::decoration() +{ + if (group) + return group->groupIcon(); + else + return QIcon::fromTheme(QStringLiteral("folder")); +} + +QString GroupViewModel::Item::path() const +{ + if (!parent) + return QLatin1Char('/') + name; + else + return parent->path() + QLatin1Char('/') + name; +} + +void GroupViewModel::Item::expandedGroups(GroupView *gview, QStringList &groups, const QModelIndex &idx) const +{ + if (children.empty()) + return; + + if (gview->isExpanded(idx)) + groups << path(); + + int row = 0; + for (const Item &child : qAsConst(children)) { + child.expandedGroups(gview, groups, model->index(row, 0, idx)); + row++; + } +} + +void GroupViewModel::Item::expandGroups(kt::GroupView *gview, const QStringList &groups, const QModelIndex &idx) +{ + if (children.empty()) + return; + + if (groups.contains(path())) + gview->expand(idx); + + int row = 0; + for (Item &i : children) { + i.expandGroups(gview, groups, model->index(row, 0, idx)); + row++; + } +} + +QModelIndex GroupViewModel::Item::findGroup(Group *g, const QModelIndex &idx) +{ + if (group == g) + return idx; + + int row = 0; + for (Item &i : children) { + QModelIndex ret = i.findGroup(g, model->index(row, 0, idx)); + row++; + if (ret.isValid()) + return ret; + } + + return QModelIndex(); +} + +void GroupViewModel::Item::dump() +{ + QString p = path(); + int indentation = p.count(QLatin1Char('/')) - 1; + QString indent = QStringLiteral("\t").repeated(indentation); + Out(SYS_GEN | LOG_DEBUG) << indent << path() << endl; + if (group) + Out(SYS_GEN | LOG_DEBUG) << indent << group->groupName() << endl; + else + Out(SYS_GEN | LOG_DEBUG) << indent << name << endl; + + for (Item &i : children) { + Out(SYS_GEN | LOG_DEBUG) << indent << "child " << i.row << endl; + i.dump(); + } +} + +} diff --git a/ktorrent/groups/groupviewmodel.h b/ktorrent/groups/groupviewmodel.h new file mode 100644 index 0000000..95cb7f0 --- /dev/null +++ b/ktorrent/groups/groupviewmodel.h @@ -0,0 +1,93 @@ +/* + SPDX-FileCopyrightText: 2012 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KT_GROUPVIEWMODEL_H +#define KT_GROUPVIEWMODEL_H + +#include +#include + +namespace kt +{ +class GroupView; + +class View; +class Group; +class GroupManager; + +/** + * Model for the GroupView + **/ +class GroupViewModel : public QAbstractItemModel +{ + Q_OBJECT +public: + GroupViewModel(GroupManager *gman, View *view, QObject *parent); + ~GroupViewModel() override; + + bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + int columnCount(const QModelIndex &parent = QModelIndex()) const override; + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QModelIndex parent(const QModelIndex &child) const override; + QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override; + bool dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) override; + Qt::DropActions supportedDropActions() const override; + Qt::ItemFlags flags(const QModelIndex &index) const override; + QStringList mimeTypes() const override; + bool removeRows(int row, int count, const QModelIndex &parent = QModelIndex()) override; + + /// Get the group given an index + Group *groupForIndex(const QModelIndex &index) const; + + /// Get all the expanded groups + QStringList expandedGroups(GroupView *gview); + + /// Expand all items in the tree which are in the groups list + void expandGroups(GroupView *gview, const QStringList &groups); + + /// Update the group count + void updateGroupCount(const QModelIndex &idx); + +private Q_SLOTS: + void groupAdded(Group *g); + void groupRemoved(Group *g); + +private: + struct Item { + Item(const QString &name, Item *parent, int row, GroupViewModel *model); + + void insert(const QString &name, const QString &p, const QModelIndex &idx); + void insert(Group *g, const QModelIndex &idx); + void remove(Group *g, const QModelIndex &idx); + bool operator==(const QString &n) const; + QVariant displayData(); + QVariant decoration(); + void expandedGroups(GroupView *gview, QStringList &groups, const QModelIndex &idx) const; + void expandGroups(GroupView *gview, const QStringList &groups, const QModelIndex &idx); + QString path() const; + void dump(); + QModelIndex findGroup(Group *g, const QModelIndex &idx); + + QString name; + QString display_name; + Item *parent; + int row; + Group *group; + QList children; + GroupViewModel *model; + }; + + QModelIndex findGroup(Group *g); + +private: + Item root; + GroupManager *gman; + View *view; +}; + +} + +#endif // KT_GROUPVIEWMODEL_H diff --git a/ktorrent/gui.cpp b/ktorrent/gui.cpp new file mode 100644 index 0000000..556a8ca --- /dev/null +++ b/ktorrent/gui.cpp @@ -0,0 +1,525 @@ +/* + SPDX-FileCopyrightText: 2007 Joris Guisson + SPDX-FileCopyrightText: 2007 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "core.h" +#include "dbus/dbus.h" +#include "dialogs/importdialog.h" +#include "dialogs/pastedialog.h" +#include "dialogs/torrentcreatordlg.h" +#include "groups/groupview.h" +#include "gui.h" +#include "ipfilterwidget.h" +#include "pref/prefdialog.h" +#include "statusbar.h" +#include "tools/queuemanagerwidget.h" +#include "trayicon.h" +#include "view/view.h" +#include + +#include "torrentactivity.h" + +#include + +namespace kt +{ +GUI::GUI() + : core(nullptr) + , pref_dlg(nullptr) +{ + // Marker markk("GUI::GUI()"); + part_manager = new KParts::PartManager(this); + connect(part_manager, &KParts::PartManager::activePartChanged, this, &GUI::activePartChanged); + core = new Core(this); + core->loadTorrents(); + + tray_icon = new TrayIcon(core, this); + + central = new CentralWidget(this); + setCentralWidget(central); + connect(central, &CentralWidget::changeActivity, this, &GUI::setCurrentActivity); + torrent_activity = new TorrentActivity(core, this, nullptr); + + status_bar = new kt::StatusBar(this); + setStatusBar(status_bar); + + setupActions(); + setupGUI(Default, QStringLiteral("ktorrentui.rc")); + + addActivity(torrent_activity); + + // mark.update(); + connect(&timer, &QTimer::timeout, this, &GUI::update); + timer.start(Settings::guiUpdateInterval()); + + applySettings(); + connect(core, &Core::settingsChanged, this, &GUI::applySettings); + + if (Settings::showSystemTrayIcon()) { + tray_icon->updateMaxRateMenus(); + tray_icon->show(); + } else + tray_icon->hide(); + + dbus_iface = new DBus(this, core, this); + core->loadPlugins(); + loadState(KSharedConfig::openConfig()); + + IPFilterWidget::registerFilterList(); + + // markk.update(); + updateActions(); + core->startUpdateTimer(); +} + +GUI::~GUI() +{ + delete core; +} + +bool GUI::event(QEvent *e) +{ + if (e->type() == QEvent::DeferredDelete) { + // HACK to prevent ktorrent from crashing on logout/shotdown (when launched e.g. via alt+f2) + delete core; + core = nullptr; + return true; + } + + return KParts::MainWindow::event(e); +} + +QSize GUI::sizeHint() const +{ + QSize desktop_size = QGuiApplication::primaryScreen()->availableSize(); + return KParts::MainWindow::sizeHint().expandedTo(desktop_size); +} + +void GUI::addActivity(Activity *act) +{ + unplugActionList(QStringLiteral("activities_list")); + central->addActivity(act); + if (act->part()) + part_manager->addPart(act->part(), false); + plugActionList(QStringLiteral("activities_list"), central->activitySwitchingActions()); +} + +void GUI::removeActivity(Activity *act) +{ + unplugActionList(QStringLiteral("activities_list")); + central->removeActivity(act); + if (act->part()) + part_manager->removePart(act->part()); + plugActionList(QStringLiteral("activities_list"), central->activitySwitchingActions()); +} + +void GUI::setCurrentActivity(Activity *act) +{ + central->setCurrentActivity(act); + part_manager->setActivePart(act ? act->part() : nullptr); +} + +void GUI::activePartChanged(KParts::Part *p) +{ + unplugActionList(QStringLiteral("activities_list")); + createGUI(p); + plugActionList(QStringLiteral("activities_list"), central->activitySwitchingActions()); +} + +void GUI::addPrefPage(PrefPageInterface *page) +{ + if (!pref_dlg) { + pref_dlg = new PrefDialog(this, core); + pref_dlg->loadState(KSharedConfig::openConfig()); + } + + pref_dlg->addPrefPage(page); +} + +void GUI::removePrefPage(PrefPageInterface *page) +{ + if (pref_dlg) + pref_dlg->removePrefPage(page); +} + +StatusBarInterface *GUI::getStatusBar() +{ + return status_bar; +} + +void GUI::mergePluginGui(Plugin *p) +{ + if (p->parentPart() == QStringLiteral("ktorrent")) { + guiFactory()->addClient(p); + } else { + const QList parts = part_manager->parts(); + for (KParts::Part *part : parts) { + if (part->domDocument().documentElement().attribute(QStringLiteral("name")) == p->parentPart()) { + part->insertChildClient(p); + break; + } + } + } +} + +void GUI::removePluginGui(Plugin *p) +{ + if (p->parentPart() == QStringLiteral("ktorrent")) { + guiFactory()->removeClient(p); + } else { + const QList parts = part_manager->parts(); + for (KParts::Part *part : parts) { + if (part->domDocument().documentElement().attribute(QStringLiteral("name")) == p->parentPart()) { + part->removeChildClient(p); + break; + } + } + } +} + +void GUI::errorMsg(const QString &err) +{ + KMessageBox::error(this, err); +} + +void GUI::errorMsg(KIO::Job *j) +{ + if (j->error()) + j->uiDelegate()->showErrorMessage(); +} + +void GUI::infoMsg(const QString &info) +{ + KMessageBox::information(this, info); +} + +void GUI::load(const QUrl &url) +{ + core->load(url, QString()); +} + +void GUI::loadSilently(const QUrl &url) +{ + core->loadSilently(url, QString()); +} + +void GUI::createTorrent() +{ + TorrentCreatorDlg *dlg = new TorrentCreatorDlg(core, this, this); + dlg->show(); +} + +void GUI::openTorrent(bool silently) +{ + QString recentDirClass; + QUrl defaultUrl = KFileWidget::getStartUrl(QUrl(QStringLiteral("kfiledialog:///openTorrent")), recentDirClass); + if (!QDir(defaultUrl.toLocalFile()).exists()) + defaultUrl = QUrl::fromLocalFile(QStandardPaths::writableLocation(QStandardPaths::DownloadLocation)); + const QList urls = QFileDialog::getOpenFileUrls(this, i18n("Open Location"), defaultUrl, kt::TorrentFileFilter(true)); + + if (urls.isEmpty()) + return; + + if (!recentDirClass.isEmpty() && defaultUrl.toLocalFile() != urls.first().toLocalFile()) + KRecentDirs::add(recentDirClass, QFileInfo(urls.first().toLocalFile()).absolutePath()); + + if (urls.count() == 1 && !silently) { + QUrl url = urls.front(); + if (url.isValid()) + load(url); + } else { + // load multiple torrents silently + for (const QUrl &url : urls) { + if (url.isValid()) { + if (silently || Settings::openMultipleTorrentsSilently()) + loadSilently(url); + else + load(url); + } + } + } +} + +void GUI::pasteURL() +{ + PasteDialog dlg(core, this); + dlg.loadState(KSharedConfig::openConfig()); + dlg.exec(); + dlg.saveState(KSharedConfig::openConfig()); +} + +void GUI::paste() +{ + if (!paste_action->isEnabled()) + return; + + QClipboard *cb = QApplication::clipboard(); + QString text = cb->text(QClipboard::Clipboard); + if (text.length() == 0) + return; + + QUrl url = QFile::exists(text) ? QUrl::fromLocalFile(text) : QUrl(text); + + if (url.isValid()) + load(url); + else + KMessageBox::error(this, i18n("Invalid URL: %1", url.toDisplayString())); +} + +void GUI::showPrefDialog() +{ + if (!pref_dlg) + pref_dlg = new PrefDialog(this, core); + + pref_dlg->updateWidgetsAndShow(); +} + +void GUI::showIPFilter() +{ + IPFilterWidget *dlg = new IPFilterWidget(this); + dlg->show(); +} + +void GUI::configureKeys() +{ + KShortcutsDialog::configure(actionCollection()); +} + +void GUI::configureToolbars() +{ + // KF5 saveMainWindowSettings(KSharedConfig::openConfig()->group("MainWindow")); + KEditToolBar dlg(factory()); + connect(&dlg, &KEditToolBar::newToolBarConfig, this, &GUI::newToolBarConfig); + dlg.exec(); + + // Replug action list + unplugActionList(QStringLiteral("activities_list")); + plugActionList(QStringLiteral("activities_list"), central->activitySwitchingActions()); +} + +void GUI::newToolBarConfig() // This is called when OK, Apply or Defaults is clicked +{ + applyMainWindowSettings(KSharedConfig::openConfig()->group("MainWindow")); +} + +void GUI::import() +{ + ImportDialog *dlg = new ImportDialog(core, this); + dlg->show(); +} + +void GUI::setupActions() +{ + KActionCollection *ac = actionCollection(); + QAction *new_action = KStandardAction::openNew(this, &GUI::createTorrent, ac); + new_action->setToolTip(i18n("Create a new torrent")); + QAction *open_action = KStandardAction::open(this, &GUI::openTorrent, ac); + open_action->setToolTip(i18n("Open a torrent")); + paste_action = KStandardAction::paste(this, &GUI::paste, ac); + + open_silently_action = new QAction(open_action->icon(), i18n("Open Silently"), this); + open_silently_action->setToolTip(i18n("Open a torrent without asking any questions")); + connect(open_silently_action, &QAction::triggered, this, &GUI::openTorrentSilently); + ac->addAction(QStringLiteral("file_open_silently"), open_silently_action); + + KStandardAction::quit(this, &GUI::quit, ac); + + show_status_bar_action = KStandardAction::showStatusbar(statusBar(), &GUI::setVisible, ac); + show_status_bar_action->setIcon(QIcon::fromTheme(QStringLiteral("kt-show-statusbar"))); + + show_menu_bar_action = KStandardAction::showMenubar(menuBar(), &GUI::setVisible, ac); + KStandardAction::preferences(this, &GUI::showPrefDialog, ac); + KStandardAction::keyBindings(this, &GUI::configureKeys, ac); + KStandardAction::configureToolbars(this, &GUI::configureToolbars, ac); + KStandardAction::configureNotifications(this, &GUI::configureNotifications, ac); + + paste_url_action = new QAction(QIcon::fromTheme(QStringLiteral("document-open-remote")), i18n("Open URL"), this); + paste_url_action->setToolTip(i18n("Open a URL which points to a torrent, magnet links are supported")); + connect(paste_url_action, &QAction::triggered, this, &GUI::pasteURL); + ac->addAction(QStringLiteral("paste_url"), paste_url_action); + ac->setDefaultShortcut(paste_url_action, QKeySequence(Qt::CTRL + Qt::Key_P)); + + ipfilter_action = new QAction(QIcon::fromTheme(QStringLiteral("view-filter")), i18n("IP Filter"), this); + ipfilter_action->setToolTip(i18n("Show the list of blocked IP addresses")); + connect(ipfilter_action, &QAction::triggered, this, &GUI::showIPFilter); + ac->addAction(QStringLiteral("ipfilter_action"), ipfilter_action); + ac->setDefaultShortcut(ipfilter_action, QKeySequence(Qt::CTRL + Qt::Key_I)); + + import_action = new QAction(QIcon::fromTheme(QStringLiteral("document-import")), i18n("Import Torrent"), this); + import_action->setToolTip(i18n("Import a torrent")); + connect(import_action, &QAction::triggered, this, &GUI::import); + ac->addAction(QStringLiteral("import"), import_action); + ac->setDefaultShortcut(import_action, QKeySequence(Qt::SHIFT + Qt::Key_I)); + + show_kt_action = new QAction(QIcon::fromTheme(QStringLiteral("kt-show-hide")), i18n("Show/Hide KTorrent"), this); + connect(show_kt_action, &QAction::triggered, this, &GUI::showOrHide); + ac->addAction(QStringLiteral("show_kt"), show_kt_action); + // KF5 show_kt_action->setGlobalShortcut(QKeySequence(Qt::ALT + Qt::SHIFT + Qt::Key_T)); + + setStandardToolBarMenuEnabled(true); +} + +void GUI::update() +{ + try { + CurrentStats stats = core->getStats(); + status_bar->updateSpeed(stats.upload_speed, stats.download_speed); + status_bar->updateTransfer(stats.bytes_uploaded, stats.bytes_downloaded); + status_bar->updateDHTStatus(Globals::instance().getDHT().isRunning(), Globals::instance().getDHT().getStats()); + + // All speed to Window status bar + if (Settings::showTotalSpeedInTitle()) { + QString down_up_speed = i18n("D: %1 | U: %2", BytesPerSecToString((double)stats.download_speed), BytesPerSecToString((double)stats.upload_speed)); + setCaption(down_up_speed); + } else + setCaption(core->getGroupManager()->allGroup()->groupName()); + + tray_icon->updateStats(stats); + core->updateGuiPlugins(); + torrent_activity->update(); + } catch (bt::Error &err) { + Out(SYS_GEN | LOG_IMPORTANT) << "Uncaught exception: " << err.toString() << endl; + } +} + +void GUI::applySettings() +{ + // Apply GUI update interval + timer.setInterval(Settings::guiUpdateInterval()); + if (Settings::showSystemTrayIcon()) { + tray_icon->updateMaxRateMenus(); + tray_icon->show(); + } else + tray_icon->hide(); +} + +void GUI::loadState(KSharedConfigPtr cfg) +{ + setAutoSaveSettings(QStringLiteral("MainWindow"), true); + central->loadState(cfg); + torrent_activity->loadState(cfg); + + KConfigGroup g = cfg->group("MainWindow"); + bool statusbar_hidden = g.readEntry("statusbar_hidden", false); + status_bar->setHidden(statusbar_hidden); + show_status_bar_action->setChecked(!statusbar_hidden); + + bool menubar_hidden = g.readEntry("menubar_hidden", false); + menuBar()->setHidden(menubar_hidden); + show_menu_bar_action->setChecked(!menubar_hidden); + + bool hidden_on_exit = g.readEntry("hidden_on_exit", false); + bool minimize_to_system_tray = Settings::alwaysMinimizeToSystemTray() || hidden_on_exit; + if (Settings::showSystemTrayIcon() && minimize_to_system_tray) { + Out(SYS_GEN | LOG_DEBUG) << "Starting minimized" << endl; + hide(); + } else { + show(); + } + + setCurrentActivity(central->currentActivity()); +} + +void GUI::saveState(KSharedConfigPtr cfg) +{ + KConfigGroup g = cfg->group("MainWindow"); + saveMainWindowSettings(g); + g.writeEntry("statusbar_hidden", status_bar->isHidden()); + g.writeEntry("menubar_hidden", menuBar()->isHidden()); + g.writeEntry("hidden_on_exit", isHidden()); + torrent_activity->saveState(cfg); + central->saveState(cfg); + if (pref_dlg) + pref_dlg->saveState(cfg); + cfg->sync(); +} + +bool GUI::queryClose() +{ + if (Settings::showSystemTrayIcon() && !qApp->isSavingSession()) { + hide(); + saveState(KSharedConfig::openConfig()); + return false; + } else { + saveState(KSharedConfig::openConfig()); + timer.stop(); + QTimer::singleShot(500, qApp, &QCoreApplication::quit); + return true; + } +} + +void GUI::quit() +{ + saveState(KSharedConfig::openConfig()); + qApp->quit(); +} + +void GUI::updateActions() +{ + torrent_activity->updateActions(); +} + +void GUI::showOrHide() +{ + setVisible(!isVisible()); +} + +void GUI::configureNotifications() +{ + KNotifyConfigWidget::configure(this); +} + +void GUI::setPasteDisabled(bool on) +{ + paste_action->setEnabled(!on); +} + +QWidget *GUI::container(const QString &name) +{ + return guiFactory()->container(name, this); +} + +TorrentActivityInterface *GUI::getTorrentActivity() +{ + return torrent_activity; +} + +} diff --git a/ktorrent/gui.h b/ktorrent/gui.h new file mode 100644 index 0000000..781d0a4 --- /dev/null +++ b/ktorrent/gui.h @@ -0,0 +1,141 @@ +/* + SPDX-FileCopyrightText: 2007 Joris Guisson + SPDX-FileCopyrightText: 2007 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KT_GUI_HH +#define KT_GUI_HH + +#include +#include +#include + +#include +#include + +class QAction; +class KToggleAction; + +namespace kt +{ +class Core; +class PrefDialog; +class StatusBar; +class TrayIcon; +class DBus; +class TorrentActivity; +class CentralWidget; + +class GUI : public KParts::MainWindow, public GUIInterface +{ + Q_OBJECT +public: + GUI(); + ~GUI() override; + + DBus *getDBusInterface() + { + return dbus_iface; + } + + // Stuff implemented from GUIInterface + KMainWindow *getMainWindow() override + { + return this; + } + void addPrefPage(PrefPageInterface *page) override; + void removePrefPage(PrefPageInterface *page) override; + void mergePluginGui(Plugin *p) override; + void removePluginGui(Plugin *p) override; + void errorMsg(const QString &err) override; + void errorMsg(KIO::Job *j) override; + void infoMsg(const QString &info) override; + StatusBarInterface *getStatusBar() override; + void addActivity(Activity *act) override; + void removeActivity(Activity *act) override; + TorrentActivityInterface *getTorrentActivity() override; + QSize sizeHint() const override; + + bool event(QEvent *e) override; + + /** + * Create a XML GUI container (menu or toolbar) + * @param name The name of the item + * @return The widget + */ + QWidget *container(const QString &name); + + /// load a torrent + void load(const QUrl &url); + + /// load a torrent silently + void loadSilently(const QUrl &url); + +public Q_SLOTS: + /// Update all actions + void updateActions(); + + /** + * Enable or disable the paste action + * @param on Set on + */ + void setPasteDisabled(bool on); + + /// Set the current activity + void setCurrentActivity(Activity *act) override; + +private Q_SLOTS: + void createTorrent(); + void openTorrent(bool silently = false); + void openTorrentSilently() + { + openTorrent(true); + } + void pasteURL(); + void paste(); + void showPrefDialog(); + void showIPFilter(); + void configureKeys(); + void configureToolbars() override; + void newToolBarConfig(); + void import(); + void update(); + /// apply gui specific settings + void applySettings(); + void showOrHide(); + void configureNotifications(); + void activePartChanged(KParts::Part *p); + void quit(); + +private: + void setupActions(); + + void loadState(KSharedConfigPtr cfg); + void saveState(KSharedConfigPtr cfg); + bool queryClose() override; + +private: + Core *core; + QTimer timer; + kt::StatusBar *status_bar; + TrayIcon *tray_icon; + DBus *dbus_iface; + TorrentActivity *torrent_activity; + CentralWidget *central; + PrefDialog *pref_dlg; + KParts::PartManager *part_manager; + + KToggleAction *show_status_bar_action; + KToggleAction *show_menu_bar_action; + QAction *open_silently_action; + + QAction *paste_url_action; + QAction *ipfilter_action; + QAction *import_action; + QAction *show_kt_action; + QAction *paste_action; +}; +} + +#endif diff --git a/ktorrent/icons/128-apps-ktorrent.png b/ktorrent/icons/128-apps-ktorrent.png new file mode 100644 index 0000000000000000000000000000000000000000..5aae905e5e875ac0a2b9ba2378e60cd97279bf2d GIT binary patch literal 6045 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 H%tHPHDz(T) literal 0 HcmV?d00001 diff --git a/ktorrent/icons/16-actions-kt-change-tracker.png b/ktorrent/icons/16-actions-kt-change-tracker.png new file mode 100644 index 0000000000000000000000000000000000000000..0d7a147084c53672efd31abdbd4a9d701f86a26d GIT binary patch literal 676 zcmV;V0$crwP)9LDF_w*3yDZ5=&Z%i6XZC&fgQxw^pGT#ekhyATotFXDXiK3z+tgWvhf{Z>* z@I*prbvlSdBJAz$Av-r096tX*_COJ;waqvxQmP38H|h6MGt#lNyabA(u)4MiVN@ip z0xYepL!FidSAVvBVxtU=i?yhmt$=@;a@3m@ZIhaw#sHQfR>WKn@DJu7s*sPkIzFh1 zVC3`(P)Sp$)Wx7`HW6ecb)1Y(H2~ytxxtgG0Nw%NP_}xZVB8n^W1h$#c1GT?6EpWf z;iNB$r~M$Vb3$rmlOcrVmF3F-TC0VZe<*l`?<1-EZ6tNRiNuyy5#RJY;@VzDeD~{! z>wVeK>}4*X6pG6sgmXC<8y|<4Uoe6*p9R(W5adnwLRNpfp;68ELDBvgqUvA9!5~{q z&^luhS4{v>bQBgA7r|n4;l=gEzb^md<6l1*a}qP4#=9VA00F5lo&WCep;Dzrjh)>c z17Kx!<#Gsf^Yex(>@sC!E*Z*b$$b*C#(VK!$cx9HeE+8(^m~cQWbEu10ERcl5IAhq z)z^c?Vqt1(3ZCBH2EecGFW}3Auj%f3<;&*?M*2OeT!!Z6W{@Nam0FEZRyYW*fREym z66ELSgQjUDsg&Rgg%-Cz_rQGW#jG^HnM2W>9!r1KZ829@v zEG+2Iu&{|JlC%#D419#RM*2O0NM!Hp?>k{m948ni zBp5su8XznhB`h2*F&!i_9Y`-7Nf{keAs$pVA0{>*H8mh9IUy=KAu2l|S|cJXJt9;; zB3d*fEk7eKKO`xL?$ywCN)MUG)N~jNhmc+C_76jKTRq+Q7TbED?3sv zJ5wuECo6F%EId>$VlOXXIxs_BF>fR^jWjiaG&hhqH<~v)Rd+pIeLY@$J#{QSf=EAY z9zkD(N{dEIu0&6EBwe#iXtzgdq&{-HS97#Ve8X#fwH`~CZW?MqR}d&<$R^uOr_mPso-9! z;$f@gVyo?tt=(6x<6Eufa;@iNuFw)2~}^r*V_ppyf&7ytkO1awkP zQ>Om@{b%7u1Q2eRCobnU|?iH0*nj{%zFCb@+>l{j-1@uEGB|X3<{JLJp;#TwnoNumGQZc1f8nNI*S63Mk-gms(Wp%>fjU@F@%u zR?=}z$Sd%Z2AQE4m6`8vY#Eu670d><#lSo!E!{UXHz!O5YOSG7TzpbWa-aa*9bzsK zvC$!tNDk9-OANQ-MRqBxmzfjP=}^y!s=+*m;e7xu{3w9*J;%`i0000BU>L#8V zre0cBf!fwV+Sb83HX#PivDTsGj`1xjowc4oi&#p6{DPPL`}c3l=iH4zzNM#&V@L&K zaza9iBU29#&zYo0Hh*;XIBm*#qogw_#)pR~=u*ugou)-aGINAF$}{2kn8B^n9}dJh-lIV%F47?BL)~Lys2Z1er_2-&k8)%kSH_FN@x1 z5#@>ULTaGs?ql5&dVWZLC2B+?HMgvB``)8H-}jK$$4z@~6wECQF=}{zYJKC0B_H~D zocZHY%Kd2vL(hGCDA@d3+qLcg)&KIY=rL7&f6RjFZ2oKkIA1x?0-%w!kq**Mn)<&1 zT_w*rHK}wJMN~#KV7&eFePGkUCO-s|{1_bFxzs1<&d>p$ZAeUk0viB@P?t(*#PpPE|d6Ep&*q{v^u>B11o(Y7n2a@*#Ip=_~ zyFjfDz<8qA^9&;v3fw62&xi(kUQ}eFwc5&3XH6w(xTdl+Vvv%gmxiJ^f;Qiq>fR*Z z5b20hNYu&qWHy=KZ^1D0`~UsWUspKhDEGIjM3jljIFyNOc=#0*=w|XzL*)_x0000< KMNUMnLSTXp{YeG@ literal 0 HcmV?d00001 diff --git a/ktorrent/icons/16-actions-kt-info-widget.png b/ktorrent/icons/16-actions-kt-info-widget.png new file mode 100644 index 0000000000000000000000000000000000000000..06844c8d7b0aca9f033e197da76645bdfcd66563 GIT binary patch literal 560 zcmV-00?+-4P)>UHmDKQwrvHqZQE{x+O}=mUgM2tR?qfZ`|PaElic6un;~9aUhkDkl~j?J z?j;<@D~(1o%w{XpYHg8Er!&BAcR;Jv%h?HyMhAz(362w#M4_ORG6W36A~ZC-$R~;t z6bc0-DFng6={Z!+!SbV&#NR(q$uJyfngz@9#l7@>p`7j|fq_9P`BzYpm+mEYyWNN) z6rYfU;4l+>1Ox2u7!;*+Z;=2h+>Eui1F`JBfQ7etEVv$wC;vi`oRU&HAYAXju>C(U z=%^nqeHHPK)u7LQFAO~7gff9F{(|ufydt!`2jYYEeDX2l8W|=u7XUw3o_>&Ui{&p z*XvPr!3U6;FHvdqC9vU!96-7^A0Srh@9mF=<*lxnUimUCrG_jqBwHe#CZ9AFF#BX)FJ>Ts2|8uTVc&eH; zYZ5bz#R(}dB-*xpk>vbQob!A{(`KzultxOW6LdIaQivP*Zr$L}n9*=#(nL5sc@pd& zHWacQIzU{AlRi~#UGmQnkEdBmh`T$xbb)-IJ`nYIz~^uPN+He|>=`^5a_!mywmDa_ zNy4(0<=V7?3=a7{o)(t5h7lu^T z$l{)H+FQ!aN|}T5LkUd-nhm;Ko&JKb!_ks1V{SLp;M(N`J|TP*lEc}6s&@QWPf;3R zt#5G@;83`NAWSnPh&r934n)CIw3A>@IYBLBITBD?zpgi}15=iDp7!%IcCKs#(>#&D zkPKs=iy*#khQ5N&vL2OvM9aEOA!*BcTm<(G8@B53w5j*A`EnOx6!PN5v(-g{YuB#zM0obai>K*}7cX`y!Fv$<@Zs(0$B!S+_;ddQ XvbPWHg^_=V00000NkvXXu0mjf!4Ds4 literal 0 HcmV?d00001 diff --git a/ktorrent/icons/16-actions-kt-pause.png b/ktorrent/icons/16-actions-kt-pause.png new file mode 100644 index 0000000000000000000000000000000000000000..043ca71f86c3c60c25dff065d69dbb372dc7dfa2 GIT binary patch literal 489 zcmVPx#32;bRa{vGZF#rGsF#)&jC{h3b00(qQO+^RT1RMnj02Aag<^TWy0%A)?L;(MX zkIcUS00C`DL_t&-8BM`GF9UHL!13R7FT)#fgUQ}aBsHA7HVr8QgWO3;h$ghI6+{ef zxEKuHNkh3HD~PMvDPRXl3EEDIO2{Ee(2KShc}uWv%pFyA|=z+ zTio+aOrL~=J~5x%vWu%rrntcj=O}s{utmzFNRM|en86KXK3e9Q4hc0DNfpSD(BXkq z<|CO^n>1;YFh`EmIKx29Gds*GDXJS_srUEILwqB>nCp{7dG;N-!Ve5h^BKdHFLvDGeLrhE; z#S_O&kPju5H=IV64K8@#nHO4I;*-T`FEG|Ai9$75yggHb0b4)v!j4WlT f$;b{IQ~vw{t?5)ffjYvm00000NkvXXu0mjfsSvzJ literal 0 HcmV?d00001 diff --git a/ktorrent/icons/16-actions-kt-queue-manager.png b/ktorrent/icons/16-actions-kt-queue-manager.png new file mode 100644 index 0000000000000000000000000000000000000000..aad904d28b65f132cf46f6d85e4b07ee474bb515 GIT binary patch literal 411 zcmV;M0c8G(P)*!C-Kohnz`kSeD%qLQ(^*n{OheMi2y0>OMC(lh!ymIBYqNS2qq^ z*MAq&v|nY?8m4J=gi!biq&1Qx85Ko|+cf_cq#-RE96AbXJI9z`-DMWn_pq^NKlGdX z4i?t_&-N|B9DDlFpxqlGP%YTLWZXbOG&&ExtZS#vLbuWE44F=U{Ae(n&FWVwkmotd zvP70;OjT8sAD|uG5)=;x)SR82VLF}S^70ac!GKBAl=1^tS66r%Y!JusD9}APVe4Cn zW*?+w1JUSW`@qD)$_BH%zKsLZf9MbG086V|*gtj==nf-WPYiC;3^%vww@7OZ8$?k& zy89mfPWl(ER*Sj0xuN{P`T04X25Ak?^R}WW8vO*){sZsQUGWykK??u?002ovPDHLk FV1nUlw-^8b literal 0 HcmV?d00001 diff --git a/ktorrent/icons/16-actions-kt-remove.png b/ktorrent/icons/16-actions-kt-remove.png new file mode 100644 index 0000000000000000000000000000000000000000..53bd05d407b90a05f7b82825d10996b2d2d02cd4 GIT binary patch literal 425 zcmV;a0apHrP)0{ezV%xTD+qP|6{mebtufE0n*ZMuYTUcRicRDQ#s#PmD zTcq3^EWlE%#%AonFXlqryY}c=B4g%YK9*oLHeol8;sS2t zMXg$+fA#E{_(v=2+qMm#>fb-NE1k}7ayZNuyWMQZZXCohoWW(>!ULPlyvk(qKYRDi z{ic=VtL46wv=|}D#`S#Xk(}&HQ>*-3Bbeo9IEmuzOHfdr$(aHpo z;P8qSbE6wHFk>;fVM8+ui+EmDzrNYTeHZTsxgW!6T;%-;%{y8dDIxlKpjvQ+L%sGBc~WsN(w^6lk)PWsaoZ~x zTzwzh78(8O&>h~(PbvsGR| z=fYbN@R%xr;ni0cqq?UNF%XQ;Ai&-Y^X6k9{Jk&wKiL~YAMXLfk9)zZycALPNfLe> zVdhC}{Ox`i3? z%T7dS^Z~;C7+G=+Hm&$gq~NyNUi_u;25*=aA?(M!Z>^p=4Q@=Y4!;8u09W$bgUl8 zrsJU3JPNnsZpb}P!P|B(zJ2k0BlXp@N1pZW1BX4>w((Dy9@zqJY%BQjZ7>O)WCq!? zTq>2yTTcBM%!(%=Zg~Q8-Jjz74<7xv{v(N^Xile7$Yj#>sT1mfcti%%_C1V~8{xGZ zuw==iuZg!?OCU2JNX3yQktUh!=V8^4U`+ipylxv-u3Y{zadxfA%>g2($P9923i&+Q zW{^&X;FN|jJ)$U)kw31>&4G@NjzRKcNaWj=5k-PQPqtb~r)8zF;Wp!MzyJ6mxp$Xl ZaR)X%>TMG639kSE002ovPDHLkV1l5de<}a~ literal 0 HcmV?d00001 diff --git a/ktorrent/icons/16-actions-kt-set-max-download-speed.png b/ktorrent/icons/16-actions-kt-set-max-download-speed.png new file mode 100644 index 0000000000000000000000000000000000000000..3c3ebd82826ee105332a4065657b7a8ebcb42a87 GIT binary patch literal 875 zcmV-x1C;!UP)@~^N5{sk zv{-FcmBIE?9;(VxgsZaDM%xdE^SRMvu`C2Y6(wI>wr#ui)m2(u{83IscH6~V^js&w z*!|v^b;1w5){D`8IS*&Vu~}BTBTQ|ulqqJQ|A2wzQy2?H(Au z*Bj#x`7+ld_xWJq*+61U1f|~gf*2{w7$B#D&EW{{xQvH(t7Pc5(G`PsdNKb+8MMO- z-8Z?CZ=~q9Mg+U_c>rVJ(Gz;e2*x=w+%E2X&*W2GR^JFbyq_%3%`lEr3N z?2d($)9LiBG3gF!&EcWLY8jd=77@f~v0REKOT=hQ?u)<}sXmi~TGQ|1#mkqzl+I++ zRvdp5o!9ZOl`X+;As5>@B1}9YK`)Z~raO_SHJ8s&lQ^k3m4)(C9-zX6o3J^b)08}z z{?liZf)wrwLe0sDeHn)^Ya-TOzl7+gX1sm>7B{^gquEjs5$1~9lREUY`PV}6tVppS9Bt^2ud*g zpf+7P`*ZriL)gLE0{P>E@VD-St8zb1D7It%$-U^a`Y0+*zsGoHLMKrS+2e;Ut0Z{! z`gIDm{q~Yy=RKD}{CEar%9(K0&VoWU7w+oCU_~v(($m|D8ECvnh;fJf(QXkRpFe-( zQ^}S~o0gv$4k6tIuBNU~Xa>UF)EjbRE9@2aL-Qs33&iFAG4%oJ&t`$;o|N(V^QRHa zBzlFrhU^nffWp)bo{lbruDBI56FD{{g(Ab^vpu0VV(d002ovPDHLkV1k74 BxO)Hq literal 0 HcmV?d00001 diff --git a/ktorrent/icons/16-actions-kt-set-max-upload-speed.png b/ktorrent/icons/16-actions-kt-set-max-upload-speed.png new file mode 100644 index 0000000000000000000000000000000000000000..1a1fc841bd4e85214a7274dc5b5190845abe0085 GIT binary patch literal 841 zcmV-P1GfB$P)#PI!p@r3D7E?24?g(6v#dIF};nAzMvRGc*KkPbH9x4@cvR+xU?2=lL+ zV)-o#dcN?Q8FoIkA;K64ja0-CAx*24Q!NxKmEFjFe=+P32jfoaW7b6z%~+3F7fdkz zls>t`#<(L4DAm!{w1B{1{@(jPUCZZQFu>SjJPbX^)xa8-JtI`mczIjkTa6B#Q9n%RZpl znjh%6g9Tk-Fh-rTM5nF)FzLJ@PJVLM%xOAq`HPlof1)|L+hWB>DAdt>ifhL&n0%6l z(Z_kX&iM_`_&|JhWuWzjpKuG2A~87;A5DYMZ!d#9)IJ_g;xO$r4;Jp>*_5fXAivqF zFBrG~Hf*I#I4U`CP;jyS)^*%tdcx5^0(TkS_+i1vDPoN#h{KQQV-$&nt+yzLk}6b! z*7NV7Gyo=iY3MzVj(U*-k*w#i! zM`;WR4_JH9e8u~E0($S##j;yg7`E#l($iD_QJYWgv;6D349uW~&<&hTm`Ruhoxmwz z1Ww0-W2@0>oz{wG8rJ?m_su`?)6lmxJw0s!wc@}g{=r963U@ykjjvvvYB=_G2sYoG zg4z2wq21=+XukdzT9Wj&B;(xUY>>y_YD!roifDT4&BN5X0;V5cgLXR@=(L-O4!c-r zyPbhntG{63Ne+}zF@hbtw$=0k&DfRGU)B{c^T0~9TJr_1)_y^ol^@ZYj5@}?Woc=t zKSKCH^_q!h==>R1j=dQKnzw^-_Ti~a3m1N&jeBUmSR(!}IXOu$Gc#j;!*>23%5H0M T2Gla!00000NkvXXu0mjf)A*c3 literal 0 HcmV?d00001 diff --git a/ktorrent/icons/16-actions-kt-show-hide.png b/ktorrent/icons/16-actions-kt-show-hide.png new file mode 100644 index 0000000000000000000000000000000000000000..a825522547a6f864665ced6aea3b320f008017f7 GIT binary patch literal 400 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!60wlNoGJgf67>k44ofy`glX(f`a29w(76TPu z2Vq7hjoB4ILG}_)Usv`UEHbR@g8$C&-3AIVC3(BMF#K=tKeHcbf{3S!V~B)g?m=5` zCPtY959^)e61m$7KPjU%HTzNE+)rNoZOOWuCf#4-R$S;>b@8%hoW{p9f{C$Kx;wvBF7=$Pm48I4 z+i&A+!%a^tmibNK%4Evh_*qg?XWPtY)5M~e?sy2SJq#1esUN+E_%A_kA; z3R4?e=}+ad$>50sFxC(}A1w(G2(E1^e(^cL*jbVQ%L%g}LerM82;ors5ZYPkQfpBM zsw@Cg1He-);MFookf<*MB&{J_(};|9ME5>~t^+&RMNr4Wh9$AXd11H2@hBJ*3~Da{ z9aO=K$zKLYT~AD$M>s?i14j^%Wt_Zd8C6tNxHkDe-rEIcgoB%FKs#+vWg2)sMj1dO z>w_#nWF281Koo9ar@B2f5(HM-^yH;%@S4PNeH?hb5v*|qH@GTh&|3Z+k~NkE>+zx+yp0ne;UmNM51hJ1eNrCPm=>c?^0nEl%8Z#*EBNHb44B#pkd!Vw>B_P}pP zLNNbq1U}y4jyKjg;i7x`h(@PR5Xsf72^)Ozs>JKOgR2M9f>dY1mcH7 zA^78XIPL#*I26k+MU&0kP%6__l1)lPVQy%*l!=7+EM1=DssU+)dZ z2iyIK#uM*vbc4=N5k)sBC@NWX#x7;#yS@H+Z?g|xTjyznY&Hcl|JFut{IEX&*S*q* z)H?k##$Ygn-~X2d`2X9{-Jf0IMsUZoD~&uu#^a!yJTT7z56`heWmQ!;BUNf8+}};X z`PUW#vdKdW9P#iXXUfOq?b|Jr?~ET>=mh?McHj~%?@u#m4HYGfcWg=TC)<4R)+TSd zKo^OKM>b0>R-vw;4mOcOytu}LtQn2pAUFMdhY!NjBppmhlCX#KmnFRccRXa1({33^ z$dlrdPZ8V`mDp#Og*V9`ys^;_Z<97AOWw(-w0a@;H`A#bQ0yq<=XhlN?ZF7VyWRuU zH8pumV`D?WgMXRhi`@ZuZlx;~&Bim!UGe+zNW8w>87(bM&P;Q2C-vEKbH7un-_HDmBOXV7vISR zN_KW8tFfuU!Nxapk^bR+}zw`TU=5&F`33guJ&_7 zL%pX;r7Q_gF6oO-EA3Gz6=h`3x2>)9hspXI5r$sOa!jv$00000NkvXXu0mjfI+S^; literal 0 HcmV?d00001 diff --git a/ktorrent/icons/16-actions-kt-start-all.png b/ktorrent/icons/16-actions-kt-start-all.png new file mode 100644 index 0000000000000000000000000000000000000000..e5fcd6e74976231772f01d51ba8957415dfe8f60 GIT binary patch literal 543 zcmV+)0^t3LP)y1|Z2EC4LV->FBWEZnBXxpx8meuB$TX)XHoO{l%zPkLA@%eo3D;2i9 z85|DBc_x)gbFCH+o)^Hf+%IV~nh-_li`MH6NRos|Bzm8TMq?bonjo9a|B^%^iBKp^ zG=*R=gk;jvAOeBleTICvSS*3fu(sy8&y-4~2G8@@4C!>{F{7%g(2`WtTk?OQn;k1B z&*%fObP0}?0A9XxQrR5r2H4gh6jqG3LryK z6dD(K5FE!*dcB^dU+DMy_-1T8pa|p11pUDfz5ak|x7Vi(h9gVAK=x@5G@C6<$el)` zvHn0Ho`=&D!F!jF@@XXqw>SD_z8Xm21D4SorKZEl`B^?>wzyx6h#r5rlDG`TF@jCr3t)n;R2o8Y_>0t zojiH6#&|rQ)1^}R?%&wEcdvV8WrZZMPQ7%y-I$-Bzrg(KAcuD^EiGL+cI;T|=+UFq hg@uKqi;IifJ_CF(Q(X9WQicEk002ovPDHLkV1gq9{`>#{ literal 0 HcmV?d00001 diff --git a/ktorrent/icons/16-actions-kt-start.png b/ktorrent/icons/16-actions-kt-start.png new file mode 100644 index 0000000000000000000000000000000000000000..927c23a95ecd4d15d1d89e7de6e66bafe5b15826 GIT binary patch literal 506 zcmVPx#32;bRa{vGZF#rGsF#)&jC{h3b00(qQO+^RT1RMni3GJYkjQ{`u0%A)?L;(MX zkIcUS00DkUL_t&-8BM{zOB-Pvz~S$7v*5o_w=1}Hbd(C`aLF0O5G~X}E|3;djI(IZ8f4$F?rP_Twd=}4jp_RktcPt9@HXV`KlJxgSsit zd4?BObcrh4Y*VE~MSG!V$fJs}+AOlkK5aUjbHV{z6j^On3`v#46?XYUmwSc`xaXQS z4OS?JB2qG4{h7bq5%YsP5*~=@ampsHE}7y6iyWdDk+6YBi`0l4j#$JEWI1}z2?G+K zOo0sF=#ensj1MeFvZU5&(W3xUlZ|A>JZEPj&GD2}mN2xQ*O>W{SPF=B@4 wK1D2S?SylNJm;9%AQRcjR+%UpIHo-L4?{3h&c}QyIRF3v07*qoM6N<$f=b2B{Qv*} literal 0 HcmV?d00001 diff --git a/ktorrent/icons/16-actions-kt-stop-all.png b/ktorrent/icons/16-actions-kt-stop-all.png new file mode 100644 index 0000000000000000000000000000000000000000..48933199c38172f600edcd8987a921a358df289c GIT binary patch literal 546 zcmV+-0^R+IP)B4AcW@Z*kX8fb~=1dspp7ZG!y~~Wx=X+nN zRLi@;VzHcOQmJ&U)9Iqy?V(z&{gPIz4VLA;XtUV@$8m^6qW76-G*;6f56EWoza)`J zA`}X1nnExbLNZA-h(I8CpV2P0SS*3v;C6fNGo@0g)$Mk%8`9~_V@4FkUWXIW?C}4E ztPV&|o>2#}bO{z`059KJDeo*U*gfGd&z@j{syCEGG*zUO>z%I-SYASp6aEWjCj&tc zdjKE^0T)ldg_Q7a0!eB7aRlaiNibNcIX(G$AkR%E9dYwR;NR=GCaNz=Sxg7L* zJ#;!9b#U_J$p+)`cuto}<-32wWHQ+u4hPMKIJVpESYKbi!2GLeKOf%O+PZS=*s;#h kqetr-8yiPAH#hfv2I$^T(!m&m-v9sr07*qoM6N<$g1!?2T>t<8 literal 0 HcmV?d00001 diff --git a/ktorrent/icons/16-actions-kt-stop.png b/ktorrent/icons/16-actions-kt-stop.png new file mode 100644 index 0000000000000000000000000000000000000000..56f6f7aaf40be1d9937eeffb4b848923b359519f GIT binary patch literal 512 zcmV+b0{{JqP)Px#32;bRa{vGZF#rGsF#)&jC{h3b00(qQO+^RT1RMnhCB|dRF8}}l0%A)?L;(MX zkIcUS00D$aL_t&-8BM{zPZMDrz~S$7qZ5CECJyh!gfR{}5E-r}*E=eqp(KU`yh|#- zLKAJhmMAe@Za5tr_)j{j3@ju!nNU$Ifdm4)NZVZB-p7zK@H`^78mbZ1lTe4co;0F{ zlDn!_HLjR4HJUW3QKn+5an+KMinY-S*y1~FI{amyAABNUqgAow##+3@SAKKBHGO(q zb4HtOmRO5Lq^x|i$shiaG9)8oNXjJ#Y~h=-5Px#24YJ`L;(K){{a7>y{D4^000SaNLh0L01ejw01ejxLMWSf00007bV*G`2iOE0 z1qCpGlY9pN00I67_Y#O= z2_Xe|7{URt#^CWzdYR`rF2{=}X_`O^flzRGIOotzL-6ZLyd6f*5OPM%j0^!N1J+h( zRX;C^%3vs7ypFhBdx!z~7e@iW)hb+UJRt{6 z1n^QZ{P?9HeWRpwLF9gdr2y&&@SXHHabEW!Yn`Yb0Njw{YEptR(AJqZt;2)^^B)P5 zyaJn=TU~5C=03nqPv2*?I}IdB0;S5AQc7K`)rZC!sO9|YXf%S1)91BX4O;6*K@jY3 zZ8gpv92T=Fce(LSR$g=FX-|rt`gH4?iB~i57 z?N+zjeK!~kd@Uq0h7EeX-gZ$GpNO{EY@RF@i@CMddC=;Kq9~8SaIoEIyyLk-tkEK@`WoGdr_8dwX}NED7Xt+9U`mV`U*J)IbWs zegl^N0j4lmNJ1bGut};*Zz)I`ZLCd`G}dBqvO(}b?lkVrk9!t!2N!oeB(Is_@xC8E zya)VevApK`PWs-jPRtB735OqFtF3=$MIM06#T?iCjVh0iG{^-+(j%rj5Jn&{&Ri`r)E>A)cBh+kJyFZC=IY@l8pU7DXK9S>)h02xBpILy~k zW7e*a#xyMU`%4{c=FzvaWf7|Ho|G4Gcus}@Lm-Qx*Ja%+YxUdKm)h=~SD5C8xG07*qoM6N<$f|4TH6951J literal 0 HcmV?d00001 diff --git a/ktorrent/icons/22-actions-kt-magnet.png b/ktorrent/icons/22-actions-kt-magnet.png new file mode 100644 index 0000000000000000000000000000000000000000..b7e618a5eccbac99e5d4c95f5502e40d8aab5d5f GIT binary patch literal 940 zcmV;d15^BoP)yhO>E=Aea$n*2Y=yctD6vb=)@5iarUEIL)3*yz)knGR_GM8Nj z`I~Qs+_l$2s;UZNZQ4N4Xgo1muWzuh>{u}2}*zCA>ZMj#t3akpr3URh~C6wj~-!kU;Q!Q@+RhBTSf*QynG zXd2wV|2+%?A)bdxq)Dc>2BM+}<0~sCmlY3W*`X04&0l>rB-*w6RWh98cD=3Dy6-;! z{I8yD)~v@*3^T{hvQ1u6%UyjHq{-wFS%&_4{Yyoq7PI+uH_z|S5HMX^3vLqZFUu>1 z(@v}Y7kbXq6xH3vamW2+oBUN*K|+%7B&&*wJ2A{XQLWhIt7 z>C1`=znh3-WZFzk4MfOEx>Hn}0`ez{`XWw>+4_3GHM<)Osd8tlKI<%fgD4&Hvn*um z>HxD{S@zoku$N9ZAVG?00^vjY-fZrxQTg;%lo`mP7_ILhcisP0P3REeuKqib@i_k|J=U)Pio>HOr}04#tdeMGK{YP`i`Pr z`a_IxN0Vuw>cmFlkiUxuF^r`E`uL)YuE5s@Pl0exv$;V{+|6Vfhisy%>Q`}Zn(kEq zJ$U~4&EkT9_xeq2Fa%Y_qU=I8{rkinwb~Dgg2Bql!x_@u%d*f{Q6{Rs6|$T`aq4&R zSIwK>T@-XxRBTI{%@80b8KfwS)WicsY4)!Zlk=hY&1TI^MZpOiw-kYJ>(($rl-8?> zhspA46laK-oU!Rnjppp4pyRdI(ldJX+!y@%t4)m`yuVmYOqd1UPd`~ZyI2JT? zUJ{&=jC&6qItaUW?}W8$S07RFp0s}b+M~O6?EuWIUb19cNirG@cSdN>o?WnG$2J%- zVuY>~J7~}#Vdu{6IK;2wxpQaFEh)BI8(u(Y@7~>jp<%;@U0RAAH*VCm$lwr|nKWrq zprp8C$Byl0&z`kDm&>jUg@TKF^yqP2DVEf3M<$!itia6Jv7^8Gd;J&D8{TQH(!H(# O0000Px#32;bRa{vGZF#rGsF#)&jC{h3b00(qQO+^RT1RMnkBsTo#b^rhX0%A)?L;(MX zkIcUS00MDIL_t&-83n;hXdGn#0O0SRncZ!&n{C(XLK3vAznP+0|M~cf=l^{L6|aU*0f;Y zntu#T$86o1*Eb=+s|)e-Q73oI{SK>1MTL=`t{-G--SEttn*?5N`EvAVu%x3QlS)KU zZlv!It4g(RJpFbreJNNOpB{&0$21*KRh7w9WGX6}4%-lqB>z41U3z@3c(>m;?h#E@ zBRQ%P`vxYar0PmnLv}QNkQGaLyKd2ps-gGnTJ#iFeWmc08Mz1iXxPq{l45!2PHh#b z#6ZsgOx$!G6*VpQ7%G;NyfZPU5G5GeGXi6~c14Mbs=G|&ovfUvE)|j6a>Ecz^o*5) zAk`3*^I4%30J*C+JqY@4N?Y-E>UaMdn~0ijR|hpMw;?yN>xxvl8eW(yf7)+9=?N_d>|xpr`Uavy z#`b*e3sXDEV%q7)EIhsyjyNb)*H8l$Q>w-We)FLa!yDGp4M)4-;mO^DNo>&1`%CIHVZxKx}NhdbQo zUJXItmMgBS#&q48Pd6a|Jl~3E<4V!aMJWnFCfx}a!o^Q|Kmg#`5N6{^r*z^xVO!Vb fuoka<76JYTOq2p9M00000NkvXXu0mjfA6Z5T literal 0 HcmV?d00001 diff --git a/ktorrent/icons/22-actions-kt-remove.png b/ktorrent/icons/22-actions-kt-remove.png new file mode 100644 index 0000000000000000000000000000000000000000..7cc245b09f9a92e0e7c70637089e53076eab9590 GIT binary patch literal 620 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 literal 0 HcmV?d00001 diff --git a/ktorrent/icons/22-actions-kt-speed-limits.png b/ktorrent/icons/22-actions-kt-speed-limits.png new file mode 100644 index 0000000000000000000000000000000000000000..54a3ac7660178a8b008c4ef30f4ba249567e5882 GIT binary patch literal 1143 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 literal 0 HcmV?d00001 diff --git a/ktorrent/icons/22-apps-ktorrent.png b/ktorrent/icons/22-apps-ktorrent.png new file mode 100644 index 0000000000000000000000000000000000000000..4068f61d3768b6d57218239130b1d1808589c7a2 GIT binary patch literal 737 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 VH{od-O*;U7?rR!od{?uH{6B(~H821G literal 0 HcmV?d00001 diff --git a/ktorrent/icons/32-actions-kt-info-widget.png b/ktorrent/icons/32-actions-kt-info-widget.png new file mode 100644 index 0000000000000000000000000000000000000000..a2d7af89c695fb156cf3bc5946dd3cfe501fb576 GIT binary patch literal 1516 zcmVPx#32;bRa{vGh*8l(w*8xH(n|J^K00(qQO+^RT1RMnm7R(*gbN~PV24YJ`L;(K) z{{a7>y{D4^00nGGL_t(|+P#)*h?G?v#((F$m$~fD?5%aR3{7!SSur%!$dpRU5<>kH zK@bs0MCSKCNS~r_1$_zn5=07x6=6}Zk$Kq+si@R)@sjPVd6}J^%g*J!oz8oPA)$6~ zw%7+A-gD-h_xzvp{Qu{?Blhjv_wvr2JGVF*%p-h_BS(&WWAEO*`>{=%?teUrBIXlP zN?g}X*&vcrQw4$`z_Kia5QJffQi={?-nDJpystOZ>c?&K8({jN01-23w+7?MnM}74 zQ7V;aG(0**S67!T6od02}!j&qgkPe*x7)qSF1FgaEuV+Koo^~$91Ns<5YxNWiQEOicV3flqr`> zac&tH7^GIK>75o#z6S?~@O_V|sl0J}(W1Lz2aEs{6a?99k7;Xl#*q$TL<=Et9P_?; zX0yF*Z+gujbsu4tX+)*5=fTl+>#tKUk2QLB$7^9bXDaUhjS z+HvIuL4tXNaVCy!FIcc3y>#gkI)PFU#?G1h0;CW)wuO{}PB1f5N!xS?&kvcd)EGOP z<7%NwsKB)(YnBc$yttpSC{pB0RX#jE&QG}t8GX!@%o2nVVGwZU`Vn&ue^JbM)wdN5>=~_bpoDP;V?YZ&5>UU9M1_raZO5cWshRzG%xL1 zM>d_DyMR{C503uK<*Gx%QbfuFVEsaur?!rePP+7DCi&{Uag>lW6f`Ws$%`{=JeT8< zb<1g25)#^D?3dqp@90JBL>GReL8uwj>qwOyCFc98I6V%nLa)QD%JNCQ7Nf zrJ(KyeD>{m5PkUd1_}kH=oGm+95{R)vvxcZX{sgx^OUyBM@P>vvTPx)ZT-6drP}2` z;LoWFR$m{U=b1$f5!ez|4>>Gfkml%kk?R#uQKT7w0^PzAFmapjAMqDIM@<^!0oPwk4SjvRw^0j>Xos zOOR54hUh^CZ$cT=29p& zcYon(5kJvq0_!m&bdgiyg(vUBb!~t-Ibe`Vy6k^?8=Hn|*xocqscCV#(9LMh<=m9u z(3L(SI}O4HBHT)j7q>5G`-W8r+Cd??4fJHvyuNok4-HNs^A`b_tR;B!>;SL*x|mNV z`#}f@8)UElz;o-;?A`Mq3D^FAKF0#svVIjG3@_o;w-592*q2PER#11km_Qp!uC!a*t?rO4`1Q)6F+ld;tG{&gMr>|Hs3qU#*up%(IVTN z`+weqVHkMOiHI&XuUloPjCuhmQ|X-|j%|;Qj(%RPRx6fe*~}wC9er$U?ATveFx%8R Sv|9rJ0000)J*5{NF4 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 LNkvXXu0mjfza*@c literal 0 HcmV?d00001 diff --git a/ktorrent/icons/32-actions-kt-remove.png b/ktorrent/icons/32-actions-kt-remove.png new file mode 100644 index 0000000000000000000000000000000000000000..020c682fe1cb1776e68a373aff4b2de56ebe2a8a GIT binary patch literal 966 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 Y1H28M7cMb6EdT%j07*qoM6N<$f{&}zF8}}l literal 0 HcmV?d00001 diff --git a/ktorrent/icons/32-actions-kt-start-all.png b/ktorrent/icons/32-actions-kt-start-all.png new file mode 100644 index 0000000000000000000000000000000000000000..1f64be8941a716e55bf7870d56d3217f7934167e GIT binary patch literal 1375 zcmV-l1)%zgP)jjyVGeqPCM?{S^w-)zw>U@&3BSpwW@oc zeXzC;gaZc->{Tk2=~}HeXDIRh{rlJ%GiH1(s8p)#mX;PYG&Bw=YHMrjVq#+E3JQf% z5eNj~_4*MGSE91A5eKTgoK3o zf?Tdp(BiacBobll8rXm!N=wUs63WZHsHv$%V`CE;B9a@KQB^1_t3Y*i4Vs#oIEj*y zvPgV<{CpuVPo|&=TrM}vW;0AC6P!*L3JZ&W65Q@WSS%Jk-fnlm<009~Wt+{;s> zay5m3a$pESInv0gV4>h}ILsX#9T;LzHMAxtrz{mb9*-3Ox_f%i-qAVe=eQOWxj>x>lf8^5h(#@VL9yXGUyC8yiZKU+mFc-WaX)0u(*(znkCUGGzJtf zT~yAIYiYiIQ6FiN5~%QpQRWQ}I?BBvG`C1YKn-YWZb4&H^Pr-owT&ly9|5OyB{Cwf>%kt{}37nLRG?(W9-d}0D_F#*)q*F&$@gIyZn^ZEE`W-u5?7K6@n z>Km0MsHmv;0RnCP6ClfIqiAn$Co9CV7`&apWhuyOKS&@oHMNfb-yoV=+Neec3{^FC zu-fc&0`gPV>GJSAW0B(oQc@TJm&?To=&glN7B~h3GOY#A65im*k)t?v>=@3SJBKr8 z&fxIj!#H{B479p@G&i>}0x6#fm>tD1+KQmoTalQSgHNg15@gGD&>0;_$&^WSGK~>h zru&$Zg?kU5V%P57xPSjXbUGbarGc#jcK^h+YuCUo!q8|mZOO^Wp9vI}`rt11!eTE( zu3Up0nOXvEfdzI~3FNALiOyhl!p3w@V-m1s>sDlBWFQ<4!|(U=M5a8V%Wo(Y!mC%W zuz&yl?xRPKt{0q6CnpdHRncbji*e!NC7eHho-fqe+KTM#Y`l5%hPk|%mn|h|AP^t| zShj50?I;0KQr7<>kd>P@ZK4IqYF;Jp-o3+`HEXb8!v-Qldjt{VURb<%u|aS+oRTtR z2}iL1jwk{w{K(As8RUx%P%o$(jY#ql*K-$&xHabR!bCQIf4#u>$PX(?{UMi4*wCU;cuB{p(-Yvu6*D z;a*5gOvJo-^9t!kBqksZ1z8xCC7|~HblbOY$HRvYIYE*yUc89m!-wO=ix*@Zjp5$9 ze*HS8PoJJBuxx$9I#<@!)m0A!9zT8@VX^I@HwJluyyEqGqsCF5aF5QKHLI0{WSzib z_8)dR9Q*R+tMLN?X7MccX%8Me$R8?XHRVt=`DpPSJ9dCQOka#1J^ByAx8JV@5^VCn zGWVvj7`NWMc@rN$e&p3~_3Bm3oH;YX^iSCDuZ6+7Fs`b>eN*B8=N+6S~2_o hhAljo>3$RK_YH|4GiwGDBGCW<002ovPDHLkV1j}Ng2Mm+ literal 0 HcmV?d00001 diff --git a/ktorrent/icons/32-actions-kt-start.png b/ktorrent/icons/32-actions-kt-start.png new file mode 100644 index 0000000000000000000000000000000000000000..2e7118c1d610e1521fb530789df4bdfc2fab94e8 GIT binary patch literal 1218 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 literal 0 HcmV?d00001 diff --git a/ktorrent/icons/48-actions-kt-bandwidth-scheduler.png b/ktorrent/icons/48-actions-kt-bandwidth-scheduler.png new file mode 100644 index 0000000000000000000000000000000000000000..30239762ce86cafe0fda410483f7bc3d93d9ed83 GIT binary patch literal 2640 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 I>zopr05q9^lmGw# literal 0 HcmV?d00001 diff --git a/ktorrent/icons/48-actions-kt-info-widget.png b/ktorrent/icons/48-actions-kt-info-widget.png new file mode 100644 index 0000000000000000000000000000000000000000..9acaab9acb9a5ce96703df00b1f0d224a93cda1d GIT binary patch literal 2438 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 il+v6!b?S{)@BbedcIGWN^*ECN0000MzCV_|S* zE^l&Yo9;Xs000KaNklme2>u%o)M!(G>iZ*96oc9a6csB-#X`-)+F098=8??Y?P76G z?!jpq>)xE1+r@YFI*UNN{M4?ZS5S zqhJ6mG=o|dWB52a_VUdCAUIgG0SusIFeLDw9zJICpQvCc8hT~+CI}8>Y!3#}m4J#C zwaJ!Yy|*S)KF*ZK#tl$B@t(QQrBa0He{50M%8P`zZy!)^kMXVrv z7LgP%QJ6>?sIy(fH}s{1JUTQE&m4O9x&$xv*;m%drNJx>AH}^iTE&*3!bc3ns3LGA zRgOM`aZaPCrJ@&`Up?7Iu&->Vt-D2oSsM0p{SmNJ@( zHXfctNbq9DzD2KQj}{)o8gyg10@1_Af~R;27s5?NTAg-NID(Axf)VxbYc0rgNwUX! zTj1JLw-UaLl?uS@f@b`G>Q6O?Wz;;D(`K9O=Sge5$)3Q27m7HSbT#?#3O3^b!4pd9 z6sH;MB=n*sRZOAIm^y(kXqyLZr*Y5gv(AHLAn9spyVhX?Zg=n*M?n&QG_)!GQOn7b zA~xbqqBKaFbR`4MgXh=S`J}|HLIu0AmYVa&02|WPZxeq)HJuL)B3!_mD4_sOt`gn( z#!MElomNV;QTJmtwF3ed{96jd1R2in*)U_!;`oGP^Ef~-A6I$Y!6p8g{q(tn9e67S zT06Lk%F8}FxjbC}&9R27_zQpH4}@ZmO-$7Xrc1b1gAklmZNeuf*n zx(L784UOEo!yR_*(oi1zy#Z|!5#*Sh-6U5A_3zEc$(E$R0Ojzz$X;^5{#a`U=V>77 zY3Nj)LnO^eMS=BbC|Hw1vL#M78M#Tp?ih{qI*~T1gbE2fu{l}AM@F4Q0 zY|>+bFQP>DCQUMF)H~`qt#T;r6?_)UK=A}FtXHAP=N({gPi$rKPzx@0A|*~rpt)*B zp(rd+(-SXo;73uI1}n$bql`pR4&hD9>7}@r^N9FYEM=kz)%WS zK9$qBNRk$#+YpFme~rPrlAfoMQY`mHLOE;jn&xXr(O?El;Sw$*l)rHW6;hN^;?O?8 zjBV1?SqUm1eFvM@$tL3^`cwOB?%O6~n@~5ql9V?E(t)Ov1z5U&0pY6uZ-O4kK=)WH zrEs4aB~G3#2-Eid)JEDu1@bKQASUM(C5Nx#Bsz+Dov?L4vUtC@q+TXk>^+Fay~95% z7)9a+3rGc5XGqZAuhD58_`Q}+fjb`CajQZ(a85a!1b&#V5&`i$l(0W70%S{!vqX~z zRXZNPU9&OhD7Zk!m^jW4?T79u0r5MOu;0SVlX?Z`q523m_I8a#_{b0`i@!u;fkJ*L z6bmRxLkasWEs>SqpE)!GKGc=Dn^&flnmW9m!#b?gymZ$tWcOm?7iWIqeEkiFX|Gz4 zSLVn}42;a!5D;r)##A@Y%YZXVx4TWkt<_jr)$p-H0&Hkx#@9tdmE5xhTZgIqUV;h*3a%xW)0qLwVGe6FS&j??=(vj{ zXw3Ly$kChnF{GWw{hSk((PL$rOk%>^&pAhHSVH~*ZYQ@2Yo$uy00000NkvXXu0mjf DtW;^g literal 0 HcmV?d00001 diff --git a/ktorrent/icons/48-actions-kt-plugins.png b/ktorrent/icons/48-actions-kt-plugins.png new file mode 100644 index 0000000000000000000000000000000000000000..eda13974f2be687fbe9af94fb00696809e881ebf GIT binary patch literal 1781 zcmV>?VZQJ(lcos8LyB(``M`2UjZe!~W-`ZW(&)Bw6 zAD{4?obTu07hMAY(;A0>L%<>65cu!o5NPY&^GTYhT@zJUvpK4;wyQ$d#l3Xt((Thi zptryKsmOf%?4!;(VBsnEd?-@?sh<`Cikh}vPkZDO!N)KoLNKuV;EA{X(E^5Q%`2(= zx(VS4-d zZu^e!{RrK^bM*7w`#Zl@ZkFa2{GQd4)W$fP)}FXe(92PE?eBbQDgp}(^$jpHk~b#& zEB?hMMbqZ>_WEu)wD~#XHXXg9vqYWMk+OSZL-g6@dS8>f@)ua9^D!y2si@boc&&DyUDBEp`&&0ReX(cvZu2&5=&yU1 zQWJO9>K1C|4B8c_6f4d??vg{A0FA5tTLI#5`m5cC(OEdatlM-T)Uf)jt+v7@&Skp- ze3@+UWuF4l1n7K>7??DK4crTM1@t(!!#TE`G=X^0zXeijvmsBX1uD*%k^r~9!Yokt zd(Cswc44ffP3YIv*?IF{0_cR71}9XKCXgh-psG%FVMYXRfVe*^iVyL4`lJQYs}oI< zX5spEhRA)y)>|N(4Vg75n`>I}M=b*W$pvh9J0Kq-Qn^sbl|fv#7#^GCxp1Y*E_@x# z85sRovP3&+0ix_W$>&WtL)@B*V<`Jwgur8n166#1QE$ZFLASkiMr?Ra1bM3xkf;E6 z)i0!;jT8e5P*$ep9ryY%@4&dG*=;?WG=VRII1r78kt(o38%+rGSv}e4VC#MiD}tM5 zr;x(KPB{>%iG@!&Z;{4&MI<3`!5lasjQPD^2-DRfAW=wLo^nouYran;g;|ljVOQw_ z_?Z31f6QFoH{c_3necdqDv1#2CAQCPUg4?wS6x~AS4A>su^5b-D5*u~uKQu~fp12$ z{?MZAzwR@6ilNDbCrD7xKx0&Hrpn^3e8PUy{MZ`tQcxLXgaDNg_-HjH&qNIL?4pDd z-!DzvbK{Ju&3It8@=TyA3}VWp1Dn{2fnP-PVX^`x>U6#0^O3$kf`8oR?NaQhqDWm{ZunyMqq9ByB8oaBV;it+OAdqp2?FqbE$>YI& z^KGAjPkdP0(JI_AYW5#qLoXV%)&BY!Nf0EDHgBUjmEXYfDnAJ3tb_3E~;zak7cai=cl|_3lK0lI2II#s35J@(tbN!_Tbv0@B!t`=^C~924Uq z#j9aq=^R*6;ti2y!B8nK9YibcGA#tmZ-}_2U#*p&LbuxyS%{|`e=#`(90Cr3>BqkS XXR3;4<*Jws00000NkvXXu0mjf)FEzP literal 0 HcmV?d00001 diff --git a/ktorrent/icons/48-actions-kt-queue-manager.png b/ktorrent/icons/48-actions-kt-queue-manager.png new file mode 100644 index 0000000000000000000000000000000000000000..08357d2a2261773a9b0cca17713e10c42dbb66ba GIT binary patch literal 1160 zcmYM!drVVT90%~ikOHBD7>9{7BBC@SnG&{v2GG?2r2>sXf@DNQI=6H~1Oe$bVLY5X zO!iQDBL$5lgO1lIFX>99v^+{Jectx=acK*sl+u zhs&Mkau??37Z#Ripwh|Zpj@R_C_sf; zuh8g0gc1Z*pjxj|X;jdf8lll>AdO~KqccE|7SbCa{l@C5c6C*!Md)-oy>8v8*RSc} zG;Hb(rnNQ0+J@O+Fm52=+}to(jK)ov39)4|+qMv94~u!*YTmY)Ejwnb-GZ=Mtv1At z-L~Vf+Z_(a?%v*>)9G}%T)!j!fVp9RzVVmG-!QlhGTshvh5O)CX$J8M$_tIb6YE=^ z{;#WtMtk$1|0exVY7g8-euP9^CdG>(Bz+r-3!br&kAQa%l%7^1a(-l|y1t*R zvKOVNYdGqA%g(fd+doLefdyN9iMVvYd?q$9u2%Fh*$BB@lnUc`Uow}APfe`@%F@X} z?wFvn)ui&`;z}ZsZ_?goEZ=Bsl-FH5JY-oa#20OdN8=g~cJxX3p+YH+5a zs%$R2Cg4kJc3XJ1t%{U#XKTEM@zd$|MCkaG=}6-{L0y`bC(E_8iHV8F-0!Xw$Fg^d zmHvAPq1b5yuA#5*^Aq<=Ha{3nyZRh(c132|y9d%T&_TVQoP6Jga+be1`s~>KFhZ+e zNHQKTD=HI5J*;~kVY=?y!_j_5p-|3M`o*%fSrdft%7{!KFefL+J;A&QBrBuk`_X>5 zN5Kg|+jdKINjI8RZ@YSmfeUrMO`bJia#OSi3RASc gA=|k=+aaM|5uZ+v#7Dhbj(jGBBx2&g#k`t-0Hrj`2LJ#7 literal 0 HcmV?d00001 diff --git a/ktorrent/icons/48-actions-kt-remove.png b/ktorrent/icons/48-actions-kt-remove.png new file mode 100644 index 0000000000000000000000000000000000000000..678b34df738d7e56d0db770465fd07ea497d2e80 GIT binary patch literal 1592 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 Eg2R(&(EtDd literal 0 HcmV?d00001 diff --git a/ktorrent/icons/48-actions-kt-set-max-upload-speed.png b/ktorrent/icons/48-actions-kt-set-max-upload-speed.png new file mode 100644 index 0000000000000000000000000000000000000000..32b66122cfd4350e3f4884df04042f4a824d9a21 GIT binary patch literal 3787 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 BVUqv= literal 0 HcmV?d00001 diff --git a/ktorrent/icons/48-actions-kt-show-hide.png b/ktorrent/icons/48-actions-kt-show-hide.png new file mode 100644 index 0000000000000000000000000000000000000000..26996e40e377bf6526a47c78e85589cad8b95068 GIT binary patch literal 1559 zcmV+y2I%>TP)MzCV_|S* zE^l&Yo9;Xs000G+NkleP_?CoLBkeqkQWT;pK0Z-6~-{ zYq63?0Y#KhHdt0s{`fcuN}ep=xYn~{$oI?^DI88kdaMD66PPuCmQ59)qVjanU*Ka|d zeHDr9;;JBBbkT#GKHT!zA(}g!V+9$kZn94COOX58N{<=4;GJ{|b`TE^DUZ~kNZPo| zQA}i!v)wO2_Jvd|oE3Z*eF7bkt3t{H`n_8ejeJgmC|17Vvmk3q#z-7HXrcqRj)xye zB%UhpDtc+54htz{7W*tne>(-0W38F)r zD40op%16QSU!p0Xmiu}S&S79aKLW6inY=;`Fv0+BoF$w@%(HzIB$UTs=7KJ0BZbAR z0dGrZu0Y#$fr~vvkkFAh(MN$PG=@-4bC)hQVygmIikT>5nLFszqHbbW%qPbnd2eNitl z!9T&%T6-uO2e?hM0DA=6FXg67l=T!5go2hAz=oq~>OeRK)^mkM9w7UHR5K0^5lJwD zKWKa`0D&mxsI?J-3MHKl?B_6Fu?7>12}9{oS7;Lh-t^T;N{KQU&N6~9NG&Bq;E^aN z)Y;>dDnv3%F)Lpvrxmw?Qm<4g1?oGhyqa+nR8Fp3WVhP(+HTo8>_=@IZN|!3FZIpndH^^{V!t92#Tk#uPzjDDl0_s5nHH`r!;vgV4Y)~AwWUlf;G<7 zSjgjN&T)mC+~Nk8In8FwWQ%0&rT_#5ldX5MNn#6s(#XHG&?cqN1L`=!D$+!qAMLkb z^|ovhspf_t+r6o%U#?!dMAO7&@BsZXYT7S~yX+*6OtO~vEXbUeP7J$o;N&;+Gf>c8?s$g2^Wedre1?O#om1UgI7@`XYrI<)P?6V-bC{dt)&_p+#WD$&YRFJ|v zqUhH5aXb%unJg=e&>;=N;htVGFL~@buugGMGWf>=F zqLU#2>UfU@%x57t0Khv2r6`zuALO`cv4rqF^|Pjiqd)o)&r-(!oxtu`~G^p+sOj=x=P< zCdq+cxh;xr2Jk$z7UhV*=!%-*uVAuvM>c!5>WA2Nk@R{`nVq7%M4ISTH2;@tQ7q7$ z5g1746t}rYGftZIbMiL?iCXZR0^BmS>1uVPpbTi_@F_be$3n2+!%-coVO%zS#iX=H z5=Mw%LbOL{{V9B8_NTW=}!Ov002ov JPDHLkV1k)z%Z>m5 literal 0 HcmV?d00001 diff --git a/ktorrent/icons/48-actions-kt-show-statusbar.png b/ktorrent/icons/48-actions-kt-show-statusbar.png new file mode 100644 index 0000000000000000000000000000000000000000..570c6f2e4cfc1cfb676ad7296030b3ecb6e0ae73 GIT binary patch literal 2014 zcmV<42O;>0P)pa!_4tAnHfJb-d~#f>+viGwX!sl zjx-NEzNf0YU;7y(i!>I2Uj>VRMZh9p5wHk&dU`Gqwt0AXG%a!h61tA?Pl5mEVo?)_ z(BO)6V#FH>pxZiYVVJOAxFbAzS5)fb|8?Q;0|yTLOOqyzKY1A%G-&XmX3d&0rAn3b zE)b9}b%uU9-!<0V*0PV=$ikK9ixOk6H36PAJrizM89aArqvkL%kI=;BTQKTgY@ru(LCCY(O} z;XTzFio{8F8$H>q`en+}%0i*oEln`Mvn7k3(HwLt|0dLKcpn-2HP`{l9DOrIi zTh*Ly!wT+`s_T7rjzs?nDsE*(0Ib?$h*|hfojP)G8?b#nm8?}>g6{*<_k*ds!TJ@! zQ61&n1~6%*b|ud5bp=XS)?oezetLY**>0+MzwxH?HJZ@Y)|LYX4k`%m=H^C2KPP6L z0kh77&1&h--3XTBv)~S?x?M5w#b%M&AzhL4Noz%7iG&V#H340bY~Le_+3E3;GsMDU z#F{W-Z8)(mLh+nq5}3Q4SbdpTc}ZjpY}~l9S@jz=YDAx5U~aT<4eZ)le=bJCW=4TE zDu7dm$mi2w#$hmRw@8GVB;x#EMPTku4dwh1(YiNb??fF=<%wwwhx7(lz?w*0^e44Mc|@B*!^!7?SmtOsC75a{d(esclL3Db^b98n2l zCsvEZ!dDR(weY7Wu(dT2xR=;*hv@EQIv;vNxK3=o%8tVfX4Sv4wF+0v0Bs~RpnUK? z7J^#s>?dQH^pdGx7{XoNw~cai5iUvK%2T5`kE- zdS&x&JKKXdmg!QPv^Q^ViaY9D+?^dCzA%C1|)NMFlPfa3PV`LBsZdkX~Y{6ft3JA(%grjw%` zzd50+j*M^qHWu22L@@meod>}Z><@8Zw~qQf_YQ$MkrMi(cA%s)n7j#0T%ixQpBGpY zrYlLq_5`3y>C)rGArY7ttj7S+SVY2;i3{JT48T^s`kPh1PMtdJP+wP+oN%e~b&i5e6I((+A{qAY1I5-o4 zX~Kqv6EPXepmB6@aWM$5;jIeZh~K30v)8fI~J>y3^AUS zkbrI_IyM9yi|cVff88_12kA2UG6FjOhtAdQ0bt_8l0?u>A}ooBOjRz$vs?c`W&-8Q zmt#;reGsMX0@DwHnI}Z#oQO#H2?N18RpdAV%n?M6Sr@>SF9 zPhyz^GKBOYwe1$MDV&(SnHaZ_Sh$}!{ERsFHE}Ucf+rZMvg&|Y_^Iw489PT?p0rxW zZ|XJ?*`w=v#!;#K4Dk9okvgej8OH>XOy4Kn2?4`=z}ThG%FqA#z0jWpVqnOREus3b z+Wmmo|3sI~W1kQwKO;_mL7e%Dw#Q;Q$iu53ytk(tAI@~rU*@ZolLru!*640iIwCT{ zPVJ15opQWg*rMxw@_Oh-ZgJr~kn*jN`DwIKpmpokiIDIS0*$zDB_NFtpET3 literal 0 HcmV?d00001 diff --git a/ktorrent/icons/48-actions-kt-speed-limits.png b/ktorrent/icons/48-actions-kt-speed-limits.png new file mode 100644 index 0000000000000000000000000000000000000000..d241b2254ba51f45395c0f9593a58e6c43eef908 GIT binary patch literal 3485 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 LNkvXXu0mjfssgzG literal 0 HcmV?d00001 diff --git a/ktorrent/icons/48-actions-kt-start-all.png b/ktorrent/icons/48-actions-kt-start-all.png new file mode 100644 index 0000000000000000000000000000000000000000..a73aeabe56ac039e6ed70ba0adb1d19a001d24e0 GIT binary patch literal 1834 zcmV+_2i5qAP)mknIuV?0h`UfjDt=jQiMjM0j*Yx&}=p# zmCB(0w^1yXux$zs2=#ovsME2Ws-r=jUM^RNDNz)^4A_=Sr3%Di2|_3o(h-fuQUBWr zhr_z|Ba!H*9?|KTqmIw@dKi+)Gzh|{0gJ`DTqu;(2T=r1H@I9bg4gR+D@9QM+hAE9 z+oo_Fk2(%Wrw4;9(M3@Px7$s1zdr!+cmjA{_%dKw&gTmt%VC66xVgDiD|xUGYJWRE zpC23!hdP*`P7eeaqCY+PbUMSZs_uk`hbNFEZCCI-uNg3#Ez5Ya1wk?lxGOl0(+rqQ z=H*N#3ulgSkBN5g=tz%Yzv;OOY+5I-RG zdfhPK2UeZ*G%?_Exvs!~Q7VNBBCG7w2*L5`*=Cl@Z*J35=ny^kg3y|=!JrKcND+^1x+L-Jz|z=8a;ps2OlL(15{0Z@{7ZnQM0kP;Yhr6QYR} zT;JTm&h8%k@Z(QlGF#y2_!J&}0VtFz^dp8*D0~{Yx}pYvm6QL;PnzqV`;8cDMvxec z!pGem+xE0+$F}Y1<`#bypW=&?BjlleCYqfA(&;p!(J1_Wzsf$J55Zs%u~-a+LJ_Oo z*<9vWmNg7`*}BXZ|f;xD9v7c?*wauuuIh`}<)(G$ICUbqiZ_e2>vnRVB0DRZX)i$nfLbgCO)vhb~%#9{-^3p2({**ZOXP{L_`Dy z1_n}nefU{=(?-fv|#8Rty{I@Teoh}zJ2@1 z&CLyRdb@o2@}-*Q<`$ipx%7-oa&>hDlVD!9)R8Cv?AWnGW(BOP0U%GGJ{4JIWo414 zrzbf&I>s}AW(DB=;-eiiht7f|knsKc_hCDP@dF1A$ajzy0P?nve6EZp8h>?~vYDLFYA2NgI-Q2@jnty{N_7A#mm)22BLIPGLq_OaAmK3pXx+?}l$1aMm;nKR&Z7X7m6g%lxpQgEm@zbb_;8vr zV}^h|fBw93xTqP-06ss>0Gf1S=Fowhot^2>p+lcZsIsz>mMvRGlP6E6Ns}g_pp`v` zgpd=q*2&DwjBIRdiWLB@kG*^MlBuaFH8wUv)6ljMR99Cgk})zel6p`>74gG|4`p?> zw6r8+W8-8^QBhF|>0rPrSXfw)pPwI{J9iFsAT!775@lv)N}+!;01oU!mPj5Fe7j`H zlGPes46GS-EgvcMXgjzD?taJ)lz|VQ3%H0w{bJ8ygGRpvCSRHEPswzQ%7FBGTZ6 zu7c&Th2y%Rp&@yAco0h(huC81uo7dP;KO*bwzeh{6B8ug&%d9Dyb*ZNd2n*$Ik5_J zb8{(PeSLi-j3UQtu#S3p#lK17&;P)GeklMdIKfTKZYoCuCwzMzCV_|S* zE^l&Yo9;Xs000LINkl##d#%0qIp>~pt|wTmJ==^PS%(f3)qrt~h31W7-(%&)yiY2yrBycJ zW~@g}07%@eX0sT<9>Z$Nq=F(|PGb9|{6N0Q+?G33%=kB@B22PtKfzz~-d%q91Jm zLiof2EiefZ6G^0yBoYctqTd^Q%KuLS{YBY@PEq1{Vvd+cGqSWC!n%}Ep{0=_(gcMl zi$mU~{_#Zxp6Vifdw>Ncu8{h4oQ3m|w`kckmuVcqQT##rtO}*V-zL0Gn-88;;K{pW zP_n+pJxg1OD;RZ_XtFq!p7QKLo5n%>K>D2K!rv=i@6$v7D)7YJcumx~j1uGS!g5C5 zLi!Xu1t2ORDpEy-_B%ddE(I1at^IJ%me)@x@OYPeB-yYuGjzNf7tv`MElI#;OxT1_ z0TY>J?=LXQF!EX|X3_o3!3KfHigH-meLBq0aXsT-ffhO>6h=>zxP)Pr_z8R2E3c*W zCcHJzjvpuRNK!sUXDxakx1ts8SfUfr=xMMFOv0ux3rbYzH-Q~US{9s9S3T!LGuIaM zN~a&zp4dS8B9Q<+=-V9nPU3>CGnPTn7qt00T<2gF&M^9w5ovIB85N)>Jls`(nikcjb5|_U*dUu1-5z_%Qj6k<4mm9^&dbAvQY*6 zjjW=s%PuClBr1S=ER&w7;!h0XRZQ1O5SB4Bp+vX#UkFF%79jaYv%DOn!vS7_OVNhB z8{VXo_(^odtXQdJweXb5jc$SY4wB znb#$P4qxf>C{TTY?txK9ZNWyt=(SvbVrwi96R^@QLtZ5)(vgrNb%>l*-2;cbPJn+Q zC}8A-X7w>1#bMVg5z8ZQ`=1mO(otKHd7G9B+Zy8n^KKNBobZV$yo6UUZ8>{xJPPn3 zAv#jE3mGqL>>be#IYkIdG@ABwVxgDM)A`C`O#lDcX&AGf(0#vb9(S32#)_F5SMfsTVjIYxca)Dw=6c zxB+j(>5ZCDhA)w_vGPV!vQK7_3oCpC*I*7!CLysRW%L^hrK`M^v0o3=SKjCYa(rtK zN;2W<<};^8{UJj7M(DR83btuMXkyT_OS85laI0&D{eT^G`K z)aOyqJ<#Tr;BpqG6H-rYU{7~&8O2v*(J4MjR+zw>Gt$qx>8AZIe!z^5g^H-%YmeUs z*zdMmFub?)2&^N}|K^Mn0K7r(kQXi|+8eB62^D)V6IL*Z5sMW?q2v`D#WrK`K(@iU z!|xqM=;4>g7k0T5s~C0GJro*`zwT^G(1&w>kl*N`hhMDO5kLHzgdX3+npSBWu@*Td zoN^Lg!vUW3iZb@&EmAkp5_){!QFnZ|cDr6cYS*VSbGQlZl*mMp6zq_!`zv zFCfE>7Z4}GD-^PHB&sCn#0D%whB$!$$*1rhKI1Pp?2>kI>r!wI zF2-dz&F*NPJsAGN7x)H0!$T(8AttbC_+*zrlPFT6)9x@Pz2z|{MsPg4C| z@cx3PE)u_=JbCik)2Gj#3xzv(?%eP0?*4=1{!7=HGiPps0AqZ7Ttf`}+Vz^m!omV% zvpK=gXf(j-blQl4-@2qy>Fa#H0MTenFeDO5a5(;AAeYNSI2@&9GFh0Ln+IH#LZOJ- zni7r1p;oIyv$+J-YK@jrk3?cvCVCU<^*Y323D%}uuAq)HkICaTV!+WdAdyI4W72^@ zh*BsNpj0YRip3&?LJ`#0jASy!`bRJ!)Z_6aN5^`o6N4N*oz75GcDsGmfaG;5l>whG zKykTT9K2pX>T8DE?dJ6F@pxBs4@bu`>Uf>2yCE11gUz;LKqL~sP9##~f)~Nl4Jwt2 zqSb0iBM<6phS_Xo{Ua8O6?IIAqZ^H8s@v^OP^;BU*Xs@7_XoggwXGTuy^hBd;B>kv z*kE*Yj5N|q2oKV2?g4?QP;KhrVuY9vj>)!m)jfA+q|Tbdn<#h(B6Q}6$G=x&-nDMAKrZU+7Z>r zG_60R9ObRoJEWrZMkh@i{qy$$)c+ApJ_DXW6rA2LqkmWh-+Bi-GNCim`r0uaZ#{*2 zoYvEs92|XMScUpO0ly>K8wf>{5C|u>G|eR*1KE5LV#&;wC7msFd=B6q!_UdB2HX38 zGoVxo`ay;Ulx+>vYBi`-DvZU&MU?U58p~~&o0|iS`T2RK@qy#~djqM?2JjQEr>BR3 zFH#sfoes-w@pwFd5ex>I#)n3uSsw$lv$HtcjHT8c0B0D>ZNWW@fh)#z9&nt08_8s{ zWk8`o140Xq0R`t}u3T+EzEs^5ynzsmjE=*XuixPDlc(_Vl?ZzK24G5OfJ7?89%2}Y z#EJpAoEZSKGq@=)X{reeJbfXC)2C0v(W6IU|Ni}yef##o!Gj0k*s)`9;lf4u^sSG* z%q0?uRRi)(eVH3pjl-2|*IB~1Z{LPbpFYulQuseeBoe{3Yu8}cu3d2W@L>>(#q`|7 z<6Hxq@-ioW^8rqrI04(YZ-ttVHF{n?x~z}OW9(O27TP`h2*w*A+(ZEsKq z>y2I8wrxAAZBFOh~%Yb0>FXGbf(UX67?8GKdEjI4&1KKY#vQRwh91 zIVhXrSh2LUl=AZOXw#-mFGK>{w{KUancKc|59Q?K2u9DJKaV{|mSB`?0q7m~vK&8p z^oWifJ4VUL$&k}m3kwS~W!tvxsx)>!0pK4zI`iX)!yDtX=!N`7Z+E<32gg$AK+}Y(#)ZlE zQ+RkdMMg&AEL1dfkkgSPM<_Nn76dGk325~K+lh&ZqUxMHc~Zy+GC6qgAT>5NQg(JW zX*3%0^z48*R@u7sp9w&BXtX52fNU}F{GR}uxWM#Z z-UpjlerjlFz@P$y6p;Xr>nI=~fE*ni$;ilv#*ZIQQ>RWPb8~a_U$Q}jj8M5i!fPa; z^=5AQ^5u{KRzM_x$qN$TaTmF`xR9Qn9*r6`iu(8OPh-Z6ArljmpA&#*umT|AGAFQA zm1YhVC?O$1NJC0!$&w}H?d?rmU1<9B=`?B5B+}8*AzNEpNDUG~OgLJnkdP2~W0_1q z>(_z>3n(}^n6_-$0!>5P*nx#R6CI<%x^7ZwlGiS~S3BP>#61%hTNMK-K zgR*q#(&ePWg;pUfEKC^o+O=!&0sMiA^z)Dr_p(ZXeaI5QLxNvTO-*eSUap9Z8#iv? zkMzCV_|S* zE^l&Yo9;Xs000K!Nkl=}8J&_`hl*lPCi?L*4cjrF|jMe2ZhD3>%i6vqgWt3^`m7`U(VwcPU8>KmsBX9)tyPUKY#3B1$I3q6H@Bv zxEE*}aR(|W5j{3ePv6!S%al2WpGjX)Us%7G?AWt!Re_z4<1Nw7O*A;}VXQ|LeWcGL zQ2?S6q9QFU(tgJm^reO}3fc}&yzur#1)dw0&!m)vW}Y4I#!c+Bj+PP#D@G;61iZ)s zTOY?1N~mh6=*5;7k6j?}Y+b&Ufeu=vcNpRLuS6d^)N4}yeW!VD;}$bM&V0IBzaE9%adJ5bNHxE(Uo z$zo63OZqx4LqTI2kgj|xbfJhIqKsblxlj8qQ&xuD4nz;0uHuyIZ?p0&w%|6U&l5TI zApBxuiC%ORwx$-jroP4}>|4(I&f>;b=Di1Q)b)3Cx~|2|xXRO~CP9Qb$Xx*XqgGT; zs@R0<$kL*y>vyBxgQqsgZ?2Y2RRfRV8lo7s0UBcTFDPM$YI`4AWH^o2Q9~sgTqC*e z)ww(%4@k}FMBR$@M8U|F{tY-{NeS8-4rk#pY0^0C;xnwPGOlKbPm+T^*RTz5QlPVq zW65kK<}9ATk`5qwfC5jJWyC2>Kk3)|;3Pio z@XK(?qpc8vB`yM$GJzu2VE}XX3mSQ{oO4@UC0n2^Tu=3yCyv`%J4$3*1JP3RUJ;2R zRhYy^v~;n$irboVn}sx)#72^~;&Gy|Et>)KXa#yB*Ra92&6!6OxXtK6=FNFGC`wXA zjp}W7PZ3wW)0oFqvaE8>6Q&_pLBKETb*QpkA^UbPFLmk=HNSQeCG3F})ym< zI&l(0qy|5jyq;%>Wrd*^vf=?eqPtxZOO&uPm`^pH&1!tAbS$xd#(t`@E(4De$Nf^$BPt$psF{kErDx-u= zA&J5%V4wXno)u}JiuB}Zm}m4LFVh+dYMwfNj%6@FIl9=p8wJ()j_%d^ESWDU;B+p2j)9Uv?Bi^w?QNl!$VGrDDETx(?4a zL;aiON07Zsa&S1Mo+62)poJlZP))A$-C3s3{^>4Cd%PR2E^v`PK7+MdY{ak^Kfw@fwU zM@_u%nE)BzHy84vam)fKia1Gqrh?&knLezEDmOcV$b zn!Y=~=WoMZsAv)xB$Dm~-slxge9qu-E2QN#eLv8hzK8O#UO?>n)T9shVSp0Fn59Mm zrMH&TT<$-Fc6tFhc)IKvsU2f@^ULZpY=8kP)^q{E6fE5x*j#ILWQ<47ML!^%58*>XaBFO-x$&>_ZKG z1aOXM&{89|!05^${hHTUli)=>8Iyh!i4X$aiF_cR6<)*-b}r#n!b4RVO-7{#$O_;E zXa@VgA$*l^=n`HfL=nCWN$!^sQ~+Eoe^^K+g)c)6UGmG2ZW;eNC#hpl>g+OuY4NXf d4mGhx`5X6w*m7OqHn0Ey002ovPDHLkV1ghGZo&Wn literal 0 HcmV?d00001 diff --git a/ktorrent/icons/48-actions-kt-upnp.png b/ktorrent/icons/48-actions-kt-upnp.png new file mode 100644 index 0000000000000000000000000000000000000000..e35e5b3ed60cd8df2ce5519643ab18c675328816 GIT binary patch literal 2408 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 literal 0 HcmV?d00001 diff --git a/ktorrent/icons/CMakeLists.txt b/ktorrent/icons/CMakeLists.txt new file mode 100644 index 0000000..f35b5ef --- /dev/null +++ b/ktorrent/icons/CMakeLists.txt @@ -0,0 +1,62 @@ +ecm_install_icons(ICONS + 16-apps-ktorrent.png + 22-apps-ktorrent.png + 32-apps-ktorrent.png + 48-apps-ktorrent.png + 64-apps-ktorrent.png + 128-apps-ktorrent.png + 16-actions-kt-stop-all.png + 16-actions-kt-stop.png + 16-actions-kt-upnp.png + 22-actions-kt-magnet.png + 22-actions-kt-pause.png + 22-actions-kt-remove.png + 22-actions-kt-set-max-download-speed.png + 22-actions-kt-set-max-upload-speed.png + 22-actions-kt-speed-limits.png + 22-actions-kt-start-all.png + 22-actions-kt-start.png + 22-actions-kt-stop-all.png + 22-actions-kt-stop.png + 32-actions-kt-info-widget.png + 32-actions-kt-magnet.png + 32-actions-kt-pause.png + 32-actions-kt-queue-manager.png + 32-actions-kt-remove.png + 32-actions-kt-set-max-download-speed.png + 32-actions-kt-set-max-upload-speed.png + 32-actions-kt-speed-limits.png + 32-actions-kt-start-all.png + 32-actions-kt-start.png + 32-actions-kt-stop-all.png + 32-actions-kt-stop.png + 32-actions-kt-upnp.png + 48-actions-kt-bandwidth-scheduler.png + 48-actions-kt-change-tracker.png + 48-actions-kt-check-data.png + 48-actions-kt-chunks.png + 48-actions-kt-info-widget.png + 48-actions-kt-magnet.png + 48-actions-kt-pause.png + 48-actions-kt-plugins.png + 48-actions-kt-queue-manager.png + 48-actions-kt-remove.png + 48-actions-kt-restore-defaults.png + 48-actions-kt-set-max-download-speed.png + 48-actions-kt-set-max-upload-speed.png + 48-actions-kt-show-hide.png + 48-actions-kt-show-statusbar.png + 48-actions-kt-speed-limits.png + 48-actions-kt-start-all.png + 48-actions-kt-start.png + 48-actions-kt-stop-all.png + 48-actions-kt-stop.png + 48-actions-kt-upnp.png + 64-actions-kt-magnet.png + sc-actions-kt-magnet.svgz + sc-actions-kt-set-max-download-speed.svgz + sc-actions-kt-set-max-upload-speed.svgz + sc-actions-kt-speed-limits.svgz + DESTINATION ${ICON_INSTALL_DIR} + THEME hicolor +) diff --git a/ktorrent/icons/sc-actions-kt-magnet.svgz b/ktorrent/icons/sc-actions-kt-magnet.svgz new file mode 100644 index 0000000000000000000000000000000000000000..9ef2fddcbcf4cbf832dd09e255e9c3d0a0de20fc GIT binary patch literal 12976 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 z%0A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 literal 0 HcmV?d00001 diff --git a/ktorrent/icons/sc-actions-kt-set-max-download-speed.svgz b/ktorrent/icons/sc-actions-kt-set-max-download-speed.svgz new file mode 100644 index 0000000000000000000000000000000000000000..bb321d479ad3cbad465ff3942161267f05084791 GIT binary patch literal 10612 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 OKmC7)PvYP1>;M2YyyK_< literal 0 HcmV?d00001 diff --git a/ktorrent/icons/sc-actions-kt-set-max-upload-speed.svgz b/ktorrent/icons/sc-actions-kt-set-max-upload-speed.svgz new file mode 100644 index 0000000000000000000000000000000000000000..0bde800faf6e4fbcda76569a2f8ac346659bbdfe GIT binary patch literal 9869 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 V01N#1;msfT@86bkOHQzW000_r1PlNG literal 0 HcmV?d00001 diff --git a/ktorrent/ipfilterlist.cpp b/ktorrent/ipfilterlist.cpp new file mode 100644 index 0000000..d961601 --- /dev/null +++ b/ktorrent/ipfilterlist.cpp @@ -0,0 +1,208 @@ +/* + SPDX-FileCopyrightText: 2008 Joris Guisson + SPDX-FileCopyrightText: 2008 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include +#include + +#include "ipfilterlist.h" +#include +#include +#include +#include + +using namespace bt; + +namespace kt +{ +IPFilterList::IPFilterList() + : bt::BlockListInterface() +{ +} + +IPFilterList::~IPFilterList() +{ +} + +bool IPFilterList::blocked(const net::Address &addr) const +{ + quint32 ip = 0; + if (addr.isIPv4Mapped()) + ip = addr.convertIPv4Mapped().toIPv4Address(); + else if (addr.ipVersion() != 4) + return false; + else + ip = addr.toIPv4Address(); + + for (const Entry &e : qAsConst(ip_list)) { + if (e.start <= ip && ip <= e.end) + return true; + } + + return false; +} + +bool IPFilterList::parseIPWithWildcards(const QString &str, bt::Uint32 &start, bt::Uint32 &end) +{ + QStringList ip_comps = str.split(QLatin1Char('.')); + if (ip_comps.count() != 4) + return false; + + bt::Uint32 ip = 0; + bt::Uint32 mask = 0; + for (int i = 0; i < 4; i++) { + if (ip_comps[i] == QStringLiteral("*")) { + mask |= 0xFF000000 >> (8 * i); + } else { + bool ok = false; + bt::Uint32 n = ip_comps[i].toUInt(&ok); + if (!ok || n > 255) + return false; + + ip |= (n & 0x000000FF) << (8 * (3 - i)); + } + } + + if (mask == 0xFFFFFFFF || mask == 0x00FFFFFF || mask == 0x0000FFFF || mask == 0x000000FF || mask == 0) { + start = ip; + end = ip | mask; + return true; + } else { + return false; + } +} + +bool IPFilterList::addIP(const QString &str) +{ + Entry e; + e.string_rep = str; + if (!parseIPWithWildcards(str, e.start, e.end)) + return false; + + ip_list.append(e); + return true; +} + +bool IPFilterList::addIPRange(const QString &str) +{ + QStringList range = str.split(QLatin1Char('-')); + if (range.count() != 2) + return false; + + net::Address start; + net::Address end; + if (!start.setAddress(range[0]) || !end.setAddress(range[1])) + return false; + + Entry e = {str, start.toIPv4Address(), end.toIPv4Address()}; + ip_list.append(e); + insertRow(ip_list.count()); + return true; +} + +bool IPFilterList::add(const QString &str) +{ + int pos = ip_list.count(); + beginInsertRows(QModelIndex(), pos, pos + 1); + bool ret = addIPRange(str) || addIP(str); + endInsertRows(); + return ret; +} + +void IPFilterList::remove(int row, int count) +{ + if (row < 0 || row + count > rowCount()) + return; + + removeRows(row, count, QModelIndex()); +} + +void IPFilterList::clear() +{ + ip_list.clear(); + endResetModel(); +} + +int IPFilterList::rowCount(const QModelIndex &parent) const +{ + if (!parent.isValid()) + return ip_list.count(); + else + return 0; +} + +QVariant IPFilterList::data(const QModelIndex &index, int role) const +{ + if (!index.isValid() || index.row() >= ip_list.count() || index.row() < 0) + return QVariant(); + + const Entry &e = ip_list.at(index.row()); + switch (role) { + case Qt::DisplayRole: + case Qt::EditRole: + return e.string_rep; + default: + return QVariant(); + } +} + +bool IPFilterList::setData(const QModelIndex &index, const QVariant &value, int role) +{ + if (!index.isValid() || index.row() >= ip_list.count() || index.row() < 0 || role != Qt::EditRole) + return false; + + Entry &e = ip_list[index.row()]; + QString str = value.toString(); + QStringList range = str.split(QLatin1Char('-')); + if (range.count() != 2) { + if (!parseIPWithWildcards(str, e.start, e.end)) + return false; + + e.string_rep = str; + } else { + net::Address start; + net::Address end; + if (!start.setAddress(range[0]) || !end.setAddress(range[1])) + return false; + + e.start = start.toIPv4Address(); + e.end = end.toIPv4Address(); + e.string_rep = str; + } + + Q_EMIT dataChanged(index, index); + return true; +} + +bool IPFilterList::insertRows(int row, int count, const QModelIndex &parent) +{ + if (parent.isValid()) + return false; + + beginInsertRows(QModelIndex(), row, row + count - 1); + endInsertRows(); + return true; +} + +bool IPFilterList::removeRows(int row, int count, const QModelIndex &parent) +{ + if (parent.isValid()) + return false; + + beginRemoveRows(QModelIndex(), row, row + count - 1); + for (int i = 0; i < count; i++) + ip_list.removeAt(row); + endRemoveRows(); + return true; +} + +Qt::ItemFlags IPFilterList::flags(const QModelIndex &index) const +{ + if (!index.isValid() || index.row() >= ip_list.count() || index.row() < 0) + return QAbstractItemModel::flags(index); + else + return QAbstractItemModel::flags(index) | Qt::ItemIsEditable; +} +} diff --git a/ktorrent/ipfilterlist.h b/ktorrent/ipfilterlist.h new file mode 100644 index 0000000..4880982 --- /dev/null +++ b/ktorrent/ipfilterlist.h @@ -0,0 +1,62 @@ +/* + SPDX-FileCopyrightText: 2008 Joris Guisson + SPDX-FileCopyrightText: 2008 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KTIPFILTERLIST_H +#define KTIPFILTERLIST_H + +#include +#include + +#include +#include + +namespace kt +{ +/** + Blocklist for the IPFilterWidget +*/ +class IPFilterList : public QAbstractListModel, public bt::BlockListInterface +{ +public: + IPFilterList(); + ~IPFilterList() override; + + bool blocked(const net::Address &addr) const override; + + /// Add an IP address with a mask. + bool add(const QString &ip); + + /// Remove the IP address at a given row and count items following that + void remove(int row, int count); + + /// Clear the list + void clear(); + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role) const override; + bool setData(const QModelIndex &index, const QVariant &value, int role) override; + bool insertRows(int row, int count, const QModelIndex &parent) override; + bool removeRows(int row, int count, const QModelIndex &parent) override; + Qt::ItemFlags flags(const QModelIndex &index) const override; + +private: + bool addIP(const QString &str); + bool addIPRange(const QString &str); + bool parseIPWithWildcards(const QString &str, bt::Uint32 &start, bt::Uint32 &end); + +private: + struct Entry { + QString string_rep; + bt::Uint32 start; + bt::Uint32 end; + }; + + QList ip_list; +}; + +} + +#endif diff --git a/ktorrent/ipfilterwidget.cpp b/ktorrent/ipfilterwidget.cpp new file mode 100644 index 0000000..ba39858 --- /dev/null +++ b/ktorrent/ipfilterwidget.cpp @@ -0,0 +1,194 @@ +/* + SPDX-FileCopyrightText: 2007 Ivan Vasić + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "ipfilterwidget.h" + +#include +#include +#include +#include +#include +#include + +#include + +#include +#include + +#include +#include +#include + +#include "ipfilterlist.h" + +#define MAX_RANGES 500 + +using namespace bt; + +namespace kt +{ +IPFilterList *IPFilterWidget::filter_list = nullptr; + +IPFilterWidget::IPFilterWidget(QWidget *parent) + : QDialog(parent) +{ + setAttribute(Qt::WA_DeleteOnClose); + setupUi(this); + setWindowTitle(i18n("IP Filter List")); + + KGuiItem::assign(m_add, KStandardGuiItem::add()); + KGuiItem::assign(m_clear, KStandardGuiItem::clear()); + KGuiItem::assign(m_save_as, KStandardGuiItem::saveAs()); + KGuiItem::assign(m_open, KStandardGuiItem::open()); + KGuiItem::assign(m_remove, KStandardGuiItem::remove()); + KGuiItem::assign(m_close, KStandardGuiItem::close()); + + registerFilterList(); + + m_ip_list->setModel(filter_list); + m_ip_list->setSelectionMode(QAbstractItemView::ContiguousSelection); + + setupConnections(); +} + +IPFilterWidget::~IPFilterWidget() +{ +} + +void IPFilterWidget::registerFilterList() +{ + if (!filter_list) { + filter_list = new IPFilterList(); + AccessManager::instance().addBlockList(filter_list); + loadFilter(kt::DataDir() + QLatin1String("ip_filter")); + } +} + +void IPFilterWidget::setupConnections() +{ + connect(m_add, &QPushButton::clicked, this, &IPFilterWidget::add); + connect(m_close, &QPushButton::clicked, this, &IPFilterWidget::accept); + connect(m_clear, &QPushButton::clicked, this, &IPFilterWidget::clear); + connect(m_save_as, &QPushButton::clicked, this, &IPFilterWidget::save); + connect(m_open, &QPushButton::clicked, this, &IPFilterWidget::open); + connect(m_remove, &QPushButton::clicked, this, &IPFilterWidget::remove); +} + +void IPFilterWidget::add() +{ + try { + std::regex rx( + "(([*]|[0-9]{1,3}).([*]|[0-9]{1,3}).([*]|[0-9]{1,3}).([*]|[0-9]{1,3}))" + "|(([0-9]{1,3}).([0-9]{1,3}).([0-9]{1,3}).([0-9]{1,3})-" + "([0-9]{1,3}).([0-9]{1,3}).([0-9]{1,3}).([0-9]{1,3}))"); + + QString ip = m_ip_to_add->text(); + + if (!regex_match(ip.toStdString(), rx) || !filter_list->add(ip)) { + KMessageBox::sorry(this, + i18n("Invalid IP address %1. IP addresses must be in the format 'XXX.XXX.XXX.XXX'." + "

You can also use wildcards like '127.0.0.*' or specify ranges like '200.10.10.0-200.10.10.40'.") + .arg(ip)); + + return; + } + } catch (bt::Error &err) { + KMessageBox::sorry(this, err.toString()); + } +} + +void IPFilterWidget::remove() +{ + QModelIndexList idx = m_ip_list->selectionModel()->selectedRows(); + if (idx.count() == 0) + return; + + filter_list->remove(idx.at(0).row(), idx.count()); +} + +void IPFilterWidget::clear() +{ + filter_list->clear(); +} + +void IPFilterWidget::open() +{ + QString lf = QFileDialog::getOpenFileName(this, i18n("Choose a file"), i18n("Text files") + QLatin1String(" (*.txt)")); + + if (lf.isEmpty()) + return; + + clear(); + + loadFilter(lf); +} + +void IPFilterWidget::save() +{ + QString sf = QFileDialog::getSaveFileName(this, i18n("Choose a filename to save under"), i18n("Text files") + QStringLiteral(" (*.txt)")); + + if (sf.isEmpty()) + return; + + saveFilter(sf); +} + +void IPFilterWidget::accept() +{ + saveFilter(kt::DataDir() + QStringLiteral("ip_filter")); + QDialog::accept(); +} + +void IPFilterWidget::saveFilter(const QString &fn) +{ + QFile fptr(fn); + + if (!fptr.open(QIODevice::WriteOnly)) { + Out(SYS_GEN | LOG_NOTICE) << QStringLiteral("Could not open file %1 for writing.").arg(fn) << endl; + return; + } + + QTextStream out(&fptr); + + for (int i = 0; i < filter_list->rowCount(); ++i) { + out << filter_list->data(filter_list->index(i, 0), Qt::DisplayRole).toString() << Qt::endl; + } + + fptr.close(); +} + +void IPFilterWidget::loadFilter(const QString &fn) +{ + QFile dat(fn); + dat.open(QIODevice::ReadOnly); + + QTextStream stream(&dat); + QString line; + std::regex rx( + "(([*]|[0-9]{1,3}).([*]|[0-9]{1,3}).([*]|[0-9]{1,3}).([*]|[0-9]{1,3}))" + "|(([0-9]{1,3}).([0-9]{1,3}).([0-9]{1,3}).([0-9]{1,3})-" + "([0-9]{1,3}).([0-9]{1,3}).([0-9]{1,3}).([0-9]{1,3}))"); + + bool err = false; + + while (!stream.atEnd()) { + line = stream.readLine(); + if (!regex_match(line.toStdString(), rx)) { + err = true; + } else { + try { + filter_list->add(line); + } catch (...) { + err = true; + } + } + } + + if (err) + Out(SYS_IPF | LOG_NOTICE) << "Some lines could not be loaded. Check your filter file..." << endl; + + dat.close(); +} +} diff --git a/ktorrent/ipfilterwidget.h b/ktorrent/ipfilterwidget.h new file mode 100644 index 0000000..662b2e3 --- /dev/null +++ b/ktorrent/ipfilterwidget.h @@ -0,0 +1,49 @@ +/* + SPDX-FileCopyrightText: 2007 Ivan Vasić + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef IPFILTERWIDGET_H +#define IPFILTERWIDGET_H + +#include "ui_ipfilterwidget.h" +#include + +namespace kt +{ +class IPFilterList; + +/** + * @author Ivan Vasic + * @brief Integrated IPFilter GUI class. + * Used to show, add and remove banned peers from blacklist. + */ +class IPFilterWidget : public QDialog, public Ui_IPFilterWidget +{ + Q_OBJECT +public: + IPFilterWidget(QWidget *parent); + ~IPFilterWidget() override; + + /// Register the filter list + static void registerFilterList(); + + void saveFilter(const QString &fn); + static void loadFilter(const QString &fn); + +public Q_SLOTS: + void save(); + void open() override; + void clear(); + void remove(); + void add(); + void accept() override; + +private: + void setupConnections(); + + static IPFilterList *filter_list; +}; +} + +#endif diff --git a/ktorrent/ipfilterwidget.ui b/ktorrent/ipfilterwidget.ui new file mode 100644 index 0000000..4278dd8 --- /dev/null +++ b/ktorrent/ipfilterwidget.ui @@ -0,0 +1,116 @@ + + + IPFilterWidget + + + + 0 + 0 + 632 + 323 + + + + KTorrent Blacklisted Peers + + + + + + + + Add peer: + + + + + + + IP addresses must be in the format 'XXX.XXX.XXX.XXX'. + +You can also use wildcards like '127.0.0.*' or specify ranges like '200.10.10.0-200.10.10.40'. + + + 127.0.0.1 + + + true + + + + + + + + + Add + + + + + + + true + + + QAbstractItemView::ContiguousSelection + + + + + + + + + Remove + + + + + + + Clear + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Open... + + + + + + + Save As... + + + + + + + Close + + + + + + + + + + diff --git a/ktorrent/ktorrent.notifyrc b/ktorrent/ktorrent.notifyrc new file mode 100644 index 0000000..cb7205a --- /dev/null +++ b/ktorrent/ktorrent.notifyrc @@ -0,0 +1,754 @@ +[Global] +IconName=ktorrent +DesktopEntry=org.kde.ktorrent +Comment=KTorrent +Comment[ar]=سيولك +Comment[ast]=KTorrent +Comment[bg]=KTorrent +Comment[bs]=KTorent +Comment[ca]=KTorrent +Comment[ca@valencia]=KTorrent +Comment[cs]=KTorrent +Comment[da]=KTorrent +Comment[de]=KTorrent +Comment[el]=KTorrent +Comment[en_GB]=KTorrent +Comment[es]=KTorrent +Comment[et]=KTorrent +Comment[fi]=KTorrent +Comment[fr]=KTorrent +Comment[ga]=KTorrent +Comment[gl]=KTorrent +Comment[hi]=केटोरेंट +Comment[hne]=केटोरेंट +Comment[hr]=KTorrent +Comment[hu]=KTorrent +Comment[ia]=KTorrent +Comment[is]=KTorrent +Comment[it]=KTorrent +Comment[ja]=KTorrent +Comment[kk]=KTorrent +Comment[km]=KTorrent +Comment[ko]=KTorrent +Comment[lt]=KTorrent +Comment[lv]=KTorrent +Comment[mr]=के-टोरंट +Comment[nb]=KTorrent +Comment[nds]=KTorrent +Comment[nl]=KTorrent +Comment[nn]=KTorrent +Comment[pl]=KTorrent +Comment[pt]=KTorrent +Comment[pt_BR]=KTorrent +Comment[ro]=KTorrent +Comment[ru]=KTorrent +Comment[si]=KTorrent +Comment[sk]=KTorrent +Comment[sl]=KTorrent +Comment[sq]=KTorrent +Comment[sr]=К‑торент +Comment[sr@ijekavian]=К‑торент +Comment[sr@ijekavianlatin]=KTorrent +Comment[sr@latin]=KTorrent +Comment[sv]=Ktorrent +Comment[th]=โปรแกรม KTorrent +Comment[tr]=KTorrent +Comment[ug]=KTorrent +Comment[uk]=KTorrent +Comment[x-test]=xxKTorrentxx +Comment[zh_CN]=KTorrent +Comment[zh_TW]=KTorrent + +[Event/TorrentSilentlyOpened] +Name=Torrent silently opened +Name[ar]=فُتح سيل بهدوء +Name[bs]=Torrent tiho otvoren +Name[ca]=El torrent s'ha obert en silenci +Name[ca@valencia]=El torrent s'ha obert en silenci +Name[cs]=Torrent byl tiše otevřen +Name[da]=Torrent åbnet tavst +Name[de]=Torrent ohne Nachfrage geöffnet +Name[el]=Το torrent άνοιξε σιωπηρά +Name[en_GB]=Torrent silently opened +Name[es]=Torrent abierto silenciosamente +Name[et]=Torren avati vaikselt +Name[fi]=Torrent avattiin hiljaisesti +Name[fr]=Torrent ouvert silencieusement +Name[ga]=Osclaíodh an torrent go ciúin +Name[gl]=Abriuse un torrente silandeiramente. +Name[hu]=A torrent csendesen megnyitva +Name[it]=Torrent aperto senza avvisi +Name[kk]=Торрент қосымша мәліметсіз жүктелмейді +Name[ko]=토렌트가 조용히 열림 +Name[lt]=Torentas tyliai įkeltas +Name[nb]=Torrent åpnet stille +Name[nds]=Torrent still opmaakt +Name[nl]=Torrent stilletjes geopend +Name[pl]=Torrent został otworzony po cichu +Name[pt]=A torrente foi aberta silenciosamente +Name[pt_BR]=O torrent foi aberto silenciosamente +Name[ro]=Torent deschis silențios +Name[ru]=Торрент открыт без подтверждения +Name[sk]=Torrent ticho otvorený +Name[sl]=Torrent je bil potiho odprt +Name[sr]=Торент тихо отворен +Name[sr@ijekavian]=Торент тихо отворен +Name[sr@ijekavianlatin]=Torent tiho otvoren +Name[sr@latin]=Torent tiho otvoren +Name[sv]=Dataflödet öppnades utan meddelanden +Name[tr]=Torrent sessizce açıldı +Name[uk]=Торент відкрито без додаткових запитів +Name[x-test]=xxTorrent silently openedxx +Name[zh_CN]=种子已静默打开 +Name[zh_TW]=Torrent 無聲地開啟 +Action=Sound|Popup + +[Event/TorrentStoppedByError] +Name=Torrent stopped by error +Name[ar]=توقّف سيل بسبب خطأ +Name[bg]=Торентът е прекъснат от грешка +Name[bs]=Greška je zaustavila torent +Name[ca]=El torrent s'ha aturat per un error +Name[ca@valencia]=El torrent s'ha aturat per un error +Name[cs]=Torrent byl zastaven kvůli chybě +Name[da]=Torrent stoppet af fejl +Name[de]=Torrent aufgrund eines Fehlers angehalten +Name[el]=Το torrent σταμάτησε με σφάλμα +Name[en_GB]=Torrent stopped by error +Name[es]=Torrent detenido por errores +Name[et]=Torrent peatatud vea tõttu +Name[fi]=Torrent pysähtyi virheeseen +Name[fr]=Torrent arrêté par erreur +Name[ga]=Bhí an torrent ag earráid +Name[gl]=Un erro provocou que se detivese un torrente +Name[hr]=Torrent je zaustavljen zbog pogreške +Name[hu]=A torrent hiba miatt leállt +Name[is]=Torrent straumurinn stöðvaðist við villu +Name[it]=Torrent interrotto da un errore +Name[ja]=torrent がエラーで停止しました +Name[kk]=Торрент қатемен тоқталды +Name[km]=Torrent ត្រូវ​បាន​បញ្ឈប់ ដោយ​កំហុស +Name[ko]=토렌트가 오류로 중단됨 +Name[lt]=Torrent failas sustojo dėl klaidos +Name[lv]=Ktorrent apturēts kļūdas dēļ +Name[nb]=Torrent stoppet på grunn av feil +Name[nds]=Torrent na Fehler anhollen +Name[nl]=Torrent gestopt vanwege een fout +Name[nn]=Torrenten vart stoppa ved ein feil +Name[pl]=Torrent zatrzymany przez błąd +Name[pt]=Torrente parada por erros +Name[pt_BR]=Torrent parado por erro +Name[ro]=Torentul a fost oprit de o eroare +Name[ru]=Торрент остановлен из-за ошибки +Name[si]=දෝශයක් මගින් ටොරෙන්ට් නැවතිනි +Name[sk]=Torrent bol zastavený chybou +Name[sl]=Torrent se je ustavil z napako +Name[sq]=Torrenti ndali nga një gabim +Name[sr]=Грешка заустави торент +Name[sr@ijekavian]=Грешка заустави торент +Name[sr@ijekavianlatin]=Greška zaustavi torent +Name[sr@latin]=Greška zaustavi torent +Name[sv]=Dataflöde stoppat av fel +Name[tr]=Torrent bir hata tarafından durduruldu +Name[uk]=Торент зупинено через помилку +Name[x-test]=xxTorrent stopped by errorxx +Name[zh_CN]=种子因错误停止 +Name[zh_TW]=Torrent 因錯誤而中止 +Action=Sound|Popup + +[Event/TorrentFinished] +Name=Torrent has finished downloading +Name[ar]=انتهى تنزيل السّيل +Name[bg]=Торентът завърши изтеглянето си +Name[bs]=Završeno preuzimanje torenta +Name[ca]=El torrent ha finalitzat la baixada +Name[ca@valencia]=El torrent ha finalitzat la baixada +Name[cs]=Torrent dokončil stahování +Name[da]=Torrent er færdig med at downloade +Name[de]=Download eines Torrents abgeschlossen +Name[el]=Η λήψη του torrent ολοκληρώθηκε +Name[en_GB]=Torrent has finished downloading +Name[es]=El torrent ha terminado de descargarse +Name[et]=Torrent lõpetas allalaadimise +Name[fi]=Torrentin lataus on valmistunut +Name[fr]=Le torrent a terminé le téléchargement +Name[ga]=Tá an t-íosluchtú críochnaithe +Name[gl]=Rematou a descarga dun torrente +Name[hr]=Torrent je završio skidanje +Name[hu]=A torrent letöltése befejeződött +Name[is]=Búið er að ná í allan torrent strauminn +Name[it]=Scaricamento del torrent terminato +Name[ja]=torrent のダウンロードが完了しました +Name[kk]=Торрент жүктеуі аяқталды +Name[km]=Torrent បាន​បញ្ចប់​ការ​ទាញយក +Name[ko]=토렌트 다운로드가 완료됨 +Name[lt]=Torrent failas atsiųstas +Name[lv]=Torrents pabeigts +Name[nb]=Torrenten er ferdig med nedlasting +Name[nds]=Torrent hett Daalladen afslaten +Name[nl]=Torrent is klaar met downloaden +Name[nn]=Torrenten er ferdig nedlasta +Name[pl]=Torrent zakończył pobieranie +Name[pt]=A torrente terminou a sua transferência +Name[pt_BR]=O download do torrent foi concluído +Name[ro]=Descărcarea torentului s-a încheiat +Name[ru]=Загрузка завершена +Name[si]=ටොරෙන්ට් බාගැනීම අවසානයි +Name[sk]=Torrent skončil sťahovanie +Name[sl]=Prejem torrenta je bil zaključen +Name[sq]=Torrenti përfundoi shkarkimin +Name[sr]=Завршено преузимање торента +Name[sr@ijekavian]=Завршено преузимање торента +Name[sr@ijekavianlatin]=Završeno preuzimanje torenta +Name[sr@latin]=Završeno preuzimanje torenta +Name[sv]=Nerladdning av dataflöde är klar +Name[tr]=Torrent indirme işlemi tamamlandı +Name[uk]=Отримання торента завершено +Name[x-test]=xxTorrent has finished downloadingxx +Name[zh_CN]=种子已下载完成 +Name[zh_TW]=Torrent 已完成下載 +Action=Sound|Popup + +[Event/MaxShareRatioReached] +Name=Maximum share ratio reached +Name[ar]=وصلتُ أقصى نسبة للمشاركة +Name[bg]=Достигнато е максималното съотношение +Name[bs]=Dostignut najveći odnos dijeljenja +Name[ca]=S'ha assolit la velocitat màxima de compartició +Name[ca@valencia]=S'ha assolit la velocitat màxima de compartició +Name[cs]=Bylo dosaženo maximálního sdílecího poměru +Name[da]=Maksimalt delingsforhold nået +Name[de]=Maximales Verteilungsverhältnis erreicht +Name[el]=Όριο της μέγιστης κοινόχρηστης αναλογίας +Name[en_GB]=Maximum share ratio reached +Name[es]=Se ha alcanzado la proporción máxima de compartición +Name[et]=Saavutati maksimaalne jagamissuhe +Name[fi]=Enimmäisjakosuhde saavutettu +Name[fr]=Le taux de partage maximal a été atteint +Name[ga]=Sroicheadh cóimheas uasta comhroinnte +Name[gl]=Acadouse a taxa máxima de compartición +Name[hr]=Maksimalni omjer dijeljenja je postignut +Name[hu]=Maximális megosztási arány elérve +Name[is]=Hámarks deilihlutfalli hefur verið náð +Name[it]=Livello massimo di condivisione raggiunto +Name[ja]=最大負担率に達しました +Name[kk]=Максимум ортақтасу ара-қатынасына жетті +Name[km]=សមាមាត្រ​ចែករំលែក​​បាន​ដល់​ចំនួន​អតិបរមា +Name[ko]=최대 공유 비율에 도달함 +Name[lt]=Didžiausias dalinimo santykis pasiektas +Name[lv]=Sasniegta maksimālā došanas attiecība +Name[nb]=Nådde grense for delingsforhold +Name[nds]=Bi hööchst Deelrelatschoon anlangt +Name[nl]=Maximale deelverhouding bereikt +Name[nn]=Høgste deleforhold er nådd +Name[pl]=Osiągnięto maksymalny współczynnik udziału +Name[pt]=Foi atingida a quota máxima de partilha +Name[pt_BR]=Taxa máxima de compartilhamento alcançada +Name[ro]=A fost atinsă rata maximă de partajare +Name[ru]=Достигнуто ограничение раздачи по коэффициенту +Name[si]=උපරිම හවුල් ප්‍රතිශතය ලඟාවිය +Name[sk]=Dosiahnutý maximálny pomer zdieľania +Name[sl]=Doseženo je bilo največje delilno razmerje +Name[sq]=Shkalla maksimale e ndarjes u arrit +Name[sr]=Достигнут највећи однос дељења +Name[sr@ijekavian]=Достигнут највећи однос дијељења +Name[sr@ijekavianlatin]=Dostignut najveći odnos dijeljenja +Name[sr@latin]=Dostignut najveći odnos deljenja +Name[sv]=Maximalt delningsförhållande uppnått +Name[tr]=En yüksek paylaşım oranına ulaşıldı +Name[uk]=Досягнути максимального відношення поширення +Name[x-test]=xxMaximum share ratio reachedxx +Name[zh_CN]=已达到的最高分享率 +Name[zh_TW]=已達到最大的分享比率 +Action=Sound|Popup + +[Event/MaxSeedTimeReached] +Name=Maximum seed time reached +Name[ar]=وصلتُ أقصى زمن للتّزويد +Name[bg]=Достигнато е максималното време за разпръскване +Name[bs]=Dostignuto najduže vrijeme sijanja +Name[ca]=S'ha assolit el temps màxim de sembrat +Name[ca@valencia]=S'ha assolit el temps màxim de sembrat +Name[cs]=Bylo dosaženo maximální doby sdílení +Name[da]=Maksimal seed-tid nået +Name[de]=Maximale Uploadzeit erreicht +Name[el]=Όριο του μέγιστου χρόνου προσφοράς γόνου +Name[en_GB]=Maximum seed time reached +Name[es]=Se ha alcanzado el tiempo máximo de siembra +Name[et]=Saavutati maksimaalne levitamise aeg +Name[fi]=Enimmäisjakoaika saavutettu +Name[fr]=Le temps de semence maximal a été atteint +Name[ga]=Sroicheadh tréimhse uasta síolaithe +Name[gl]=Acadouse o prazo máximo de compartición +Name[hr]=Maksimalno vrijeme sijanja je postignuto +Name[hu]=Maximális megosztási idő elérve +Name[is]=Hámarks sáðtíma hefur verið náð +Name[it]=Tempo massimo di distribuzione seme raggiunto +Name[ja]=最長シード時間に達しました +Name[kk]=Максимум тарату уақытына жетті +Name[km]=បាន​ដល់​ពេលវេលា​អតិបរមា​ហើយ +Name[ko]=최대 시드 시간에 도달함 +Name[lt]=Ilgiausias platinimo laikas pasiektas +Name[lv]=Sasniegts maksimālais došanas laiks +Name[nb]=Nådde tidsgrensen for opplasting +Name[nds]=Bi hööchst Verdeeltiet anlangt +Name[nl]=Maximale seed-tijd bereikt +Name[nn]=Lengste deletid er nådd +Name[pl]=Osiągnięto maksymalny czas rozsiewania +Name[pt]=Foi atingida a quota máxima de publicação +Name[pt_BR]=Taxa máxima de semeação alcançada +Name[ro]=A fost atinsă durata maximă de încărcare +Name[ru]=Достигнуто максимальное время сидирования +Name[si]=උපරිම සීඩ් කාලය ලඟාවිය +Name[sk]=Dosiahnutý maximálny čas zdieľania +Name[sl]=Dosežen je bil največji čas sejanja +Name[sq]=Shkalla maksimale e dërgimit u arrit +Name[sr]=Достигнуто најдуже време сејања +Name[sr@ijekavian]=Достигнуто најдуже време сијања +Name[sr@ijekavianlatin]=Dostignuto najduže vreme sijanja +Name[sr@latin]=Dostignuto najduže vreme sejanja +Name[sv]=Maximal erbjudningstid uppnådd +Name[tr]=En yüksek paylaşım süresine ulaşıldı +Name[uk]=Досягнуто максимального часу поширення +Name[x-test]=xxMaximum seed time reachedxx +Name[zh_CN]=已达到的最大做种时间 +Name[zh_TW]=已達到最大播種時間 +Action=Sound|Popup + +[Event/LowDiskSpace] +Name=Disk space is running low +Name[ar]=مساحة القرص منخفضة +Name[ast]=L'espaciu en discu ta escosando +Name[bg]=Дисковото пространство намалява +Name[bs]=Ponestaje prostora na disku +Name[ca]=Queda poc espai al disc +Name[ca@valencia]=Queda poc espai al disc +Name[cs]=Dochází místo na disku +Name[da]=Diskplads er ved at løbe tør +Name[de]=Nur noch wenig Speicherplatz verfügbar +Name[el]=Ο χώρος στο δίσκο τελειώνει +Name[en_GB]=Disk space is running low +Name[es]=Va quedando poco espacio en disco +Name[et]=Kettaruumi hakkab nappima +Name[fi]=Levytila on käymässä vähiin +Name[fr]=Vous allez manquer d'espace disque +Name[ga]=Tá an diosca ag éirí lán +Name[gl]=O espazo de almacenamento está esgotándose +Name[hr]=Ponestaje prostora na disku +Name[hu]=Az elérhető szabad lemezterület fogyóban +Name[is]=Diskpláss er að verða af skornum skammti +Name[it]=Lo spazio sul disco sta terminando +Name[ja]=ディスクの空き領域が少なくなりました +Name[kk]=Дискіде орын тапшылығы +Name[km]=ជិត​អស់​ទំហំ​ថាស​ហើយ +Name[ko]=디스크 공간이 부족함 +Name[lt]=Diske mažai laisvos vietos +Name[lv]=Paliek maz vietas uz diska +Name[nb]=Det er lite diskplass igjen +Name[nds]=Fastplaatruum geiht to Enn +Name[nl]=Beschikbare schijfruimte wordt te laag +Name[nn]=Lite diskplass att +Name[pl]=Wyczerpuje się wolne miejsce na dysku +Name[pt]=Está a faltar espaço em disco +Name[pt_BR]=O espaço em disco está acabando +Name[ro]=Spațiul pe disc se termină +Name[ru]=Заканчивается место на диске +Name[si]=තැටි ඉඩ අඩුවෙමින් පවතී +Name[sk]=Je málo miesta na disku +Name[sl]=Zmanjkuje prostora na disku +Name[sq]=Hapësira në disk është e pakët +Name[sr]=Понестаје простора на диску +Name[sr@ijekavian]=Понестаје простора на диску +Name[sr@ijekavianlatin]=Ponestaje prostora na disku +Name[sr@latin]=Ponestaje prostora na disku +Name[sv]=Diskutrymme håller på att ta slut +Name[tr]=Disk alanı azalıyor +Name[uk]=Вільне місце на диску закінчується +Name[x-test]=xxDisk space is running lowxx +Name[zh_CN]=磁盘空间紧张 +Name[zh_TW]=磁碟空間快不夠了 +Action=Sound|Popup + +[Event/CorruptedData] +Name=Corrupted data has been found +Name[ar]=وُجدت بيانات معطوبة +Name[ast]=Atopáronse datos toyíos +Name[bg]=Открити са повредени данни +Name[bs]=Pronađeni su oštećeni podaci +Name[ca]=S'han trobat dades corruptes +Name[ca@valencia]=S'han trobat dades corruptes +Name[cs]=Byla nalezena vadná data +Name[da]=Defekt data er blevet fundet +Name[de]=Fehlerhafte Daten gefunden +Name[el]=Βρέθηκαν κατεστραμμένα δεδομένα +Name[en_GB]=Corrupted data has been found +Name[es]=Se han encontrado datos dañados +Name[et]=Leiti vigaseid andmeid +Name[fi]=On löytynyt vioittunutta dataa +Name[fr]=Des données corrompues ont été détectées +Name[ga]=Fuarthas sonraí truaillithe +Name[gl]=Atopáronse datos corrompidos +Name[hr]=Nađeni su pokvareni podaci +Name[hu]=Sérült adatok találhatóak +Name[is]=Skennd gögn fundust +Name[it]=Sono stati trovati dati danneggiati +Name[ja]=壊れたデータが見つかりました +Name[kk]=Бүлінген деректер табылды +Name[km]=បាន​រកឃើញ​ទិន្នន័យ​ដែល​ខូច +Name[ko]=잘못된 데이터를 찾음 +Name[lt]=Aptikti sugadinti duomenys +Name[lv]=Atrasti bojāti dati +Name[nb]=Fant skadet data +Name[nds]=Schaadhaftig Daten funnen +Name[nl]=Beschadigde gegevens gevonden +Name[nn]=Øydelagde data vart funne +Name[pl]=Wykryto uszkodzone dane +Name[pt]=Foram encontrados dados corrompidos +Name[pt_BR]=Foram encontrados dados corrompidos +Name[ro]=Au fost găsite date corupte +Name[ru]=Обнаружены повреждённые данные +Name[si]=දූශිත දත්ත හමුවිය +Name[sk]=Našli sa poškodené dáta +Name[sl]=Najdeni so bili pokvarjeni podatki +Name[sq]=U gjetën të dhëna të dëmtuara +Name[sr]=Нађени су оштећени подаци +Name[sr@ijekavian]=Нађени су оштећени подаци +Name[sr@ijekavianlatin]=Nađeni su oštećeni podaci +Name[sr@latin]=Nađeni su oštećeni podaci +Name[sv]=Skadad data har hittats +Name[tr]=Bozuk veri bulundu +Name[ug]=‫بۇزۇلغان سانلىق-مەلۇمات تېپىلدى +Name[uk]=Знайдено пошкоджені дані +Name[x-test]=xxCorrupted data has been foundxx +Name[zh_CN]=发现损坏的数据 +Name[zh_TW]=找到損毀的資料 +Action=Sound|Popup + +[Event/QueueNotPossible] +Name=Torrent cannot be enqueued +Name[bg]=Торентът не може да бъде поставен на опашка +Name[bs]=Ne mogu da stavim torent u red +Name[ca]=No s'ha pogut posar el torrent a la cua +Name[ca@valencia]=No s'ha pogut posar el torrent a la cua +Name[cs]=Torrent nelze zařadit do fronty +Name[da]=Torrent kan ikke sættes i kø +Name[de]=Torrent kann nicht in die Warteschlange eingereiht werden +Name[el]=Το torrent δεν μπορεί να μπει σε αναμονή +Name[en_GB]=Torrent cannot be enqueued +Name[es]=No se puede encolar el torrent +Name[et]=Torrenti panek järjekorda nurjus +Name[fi]=Torrentia ei voi panna jonoon +Name[fr]=Impossible de mettre le torrent en file d'attente +Name[ga]=Ní féidir an torrent a chur sa chiú +Name[gl]=Non é posíbel pór o torrente na fila +Name[hr]=Torrent ne može biti postavljen na listu +Name[hu]=A torrent nem állítható sorba +Name[is]=Ekki hægt að setja torrent strauminn í biðröð +Name[it]=Impossibile accodare il torrent +Name[ja]=torrent をキューに追加できません +Name[kk]=Торрент кезекке тұрмайды +Name[km]=មិនអាច​ដាក់ Torrent ជា​ជួរ​បានទេ +Name[ko]=토렌트를 대기열에 추가할 수 없음 +Name[lt]=Torrent failo įtraukti į eilę nepavyko +Name[lv]=Neizdevās torrentu ielikt rindā +Name[nb]=Strømmen kan ikke legges til i køen +Name[nds]=Torrent lett sik nich inregen +Name[nl]=Torrent kan niet in wachtrij worden geplaatst +Name[nn]=Klarar ikkje leggja til torrenten i køen +Name[pl]=Nie można dodać torrenta do kolejki +Name[pt]=A torrente não pode ser colocada em espera +Name[pt_BR]=O torrent não pode ser enfileirado +Name[ro]=Torentul nu poate fi pus în coadă +Name[ru]=Торрент не может быть поставлен в очередь +Name[si]=ටොරෙන්ට් පෙළගැසිය නොහැක +Name[sk]=Torrent sa nedá zaradiť +Name[sl]=Torrenta ni mogoče postaviti v vrsto +Name[sq]=Torrenti nuk mund të vendoset në pritje +Name[sr]=Не могу да ставим торент у ред +Name[sr@ijekavian]=Не могу да ставим торент у ред +Name[sr@ijekavianlatin]=Ne mogu da stavim torent u red +Name[sr@latin]=Ne mogu da stavim torent u red +Name[sv]=Dataflödet kan inte köas +Name[tr]=Torrent bekletilemez +Name[uk]=Торент неможливо поставити в чергу +Name[x-test]=xxTorrent cannot be enqueuedxx +Name[zh_CN]=无法将种子排入队列 +Name[zh_TW]=Torrent 無法加入佇列 +Action=Sound|Popup + +[Event/CannotStart] +Name=Torrent cannot be started +Name[ar]=تعذّر بدء السّيل +Name[bg]=Торентът не може да бъде пуснат +Name[bs]=Torent se ne može pokrenuti +Name[ca]=No s'ha pogut engegar el torrent +Name[ca@valencia]=No s'ha pogut engegar el torrent +Name[cs]=Torrent nemůže být spuštěn +Name[da]=Torrent kan ikke startes +Name[de]=Torrent kann nicht gestartet werden +Name[el]=Το torrent δεν μπορεί να εκκινήσει +Name[en_GB]=Torrent cannot be started +Name[es]=No se puede iniciar el torrent +Name[et]=Torrenti alustamine nurjus +Name[fi]=Torrentia ei voi käynnistää +Name[fr]=Impossible de démarrer le torrent +Name[ga]=Ní féidir an torrent a thosú +Name[gl]=Non é posíbel iniciar o torrente +Name[hr]=Torrent ne može biti pokrenut +Name[hu]=A torrent nem indítható el +Name[is]=Ekki hægt að ræsa torrent strauminn +Name[it]=Impossibile avviare il torrent +Name[ja]=torrent を開始できません +Name[kk]=Торрент басталмайды +Name[km]=មិនអាច​ចាប់ផ្ដើម Torrent បានទេ +Name[ko]=토렌트를 시작할 수 없음 +Name[lt]=Torrent failo pradėti nepavyko +Name[lv]=Neizdevās sākt torrentu +Name[nb]=Torrenten kan ikke startes +Name[nds]=Torrent lett sik nich starten +Name[nl]=Torrent kan niet worden gestart +Name[nn]=Klarar ikkje starta torrenten +Name[pl]=Nie można uruchomić torrenta +Name[pt]=A torrente não pode ser iniciada +Name[pt_BR]=O torrent não pode ser iniciado +Name[ro]=Torentul nu poate fi pornit +Name[ru]=Торрент не может быть запущен +Name[si]=ටොරෙන්ටය ආරම්භ කල නොහැක +Name[sk]=Torrent sa nedá spustiť +Name[sl]=Torrenta ni mogoče zagnati +Name[sq]=Torrenti nuk mund të niset +Name[sr]=Не могу да покренем торент +Name[sr@ijekavian]=Не могу да покренем торент +Name[sr@ijekavianlatin]=Ne mogu da pokrenem torent +Name[sr@latin]=Ne mogu da pokrenem torent +Name[sv]=Dataflödet kan inte startas +Name[tr]=Torrent başlatılamadı +Name[uk]=Неможливо запустити торент +Name[x-test]=xxTorrent cannot be startedxx +Name[zh_CN]=无法启动种子 +Name[zh_TW]=Torrent 無法被啟始 +Action=Sound|Popup + +[Event/CannotLoadSilently] +Name=Torrent cannot be loaded silently +Name[ar]=تعذّر تحميل سيل بهدوء +Name[bg]=Торентът не може да бъде автоматично зареден +Name[bs]=Torent se ne može učitati tiho +Name[ca]=No s'ha pogut carregar silenciosament el torrent +Name[ca@valencia]=No s'ha pogut carregar silenciosament el torrent +Name[cs]=Torrent nemůže být tiše spuštěn +Name[da]=Torrent kan ikke indlæses tavst +Name[de]=Torrent kann nicht ohne Nachfrage geladen werden +Name[el]=Το Torrent δεν μπορεί να φορτωθεί σιωπηλά +Name[en_GB]=Torrent cannot be loaded silently +Name[es]=No se puede cargar silenciosamente el torrent +Name[et]=Torrenti vaikne laadimine nurjus +Name[fi]=Torrentia ei voi ladata hiljaisesti +Name[fr]=Impossible de charger le torrent silencieusement +Name[ga]=Ní féidir an torrent a luchtú go ciúin +Name[gl]=Non é posíbel cargar silandeiramente o torrente +Name[hr]=Torrent ne može biti potiho učitan +Name[hu]=A torrent nem tölthető be jóváhagyás nélkül +Name[is]=Ekki hægt að hlaða óséð inn torrent straumnum +Name[it]=Impossibile caricare senza avvisi il torrent +Name[ja]=torrent を自動でロードできません +Name[kk]=Торрент үндемей жүктелмейді +Name[km]=មិនអាច​ផ្ទុក Torrent ដោយ​ស្ងាត់ៗ​បានទេ +Name[ko]=토렌트를 조용히 열 수 없음 +Name[lt]=Torrent failo tyliai įkelti nepavyko +Name[lv]=Torrentu nevar ielādēt klusām +Name[nb]=Torrenten kan ikke lastes stille +Name[nds]=Torrent lett sik nich still laden +Name[nl]=Torrent kan niet stil worden geladen +Name[nn]=Klarar ikkje lasta inn torrenten stille +Name[pl]=Nie można wczytać torrenta bez potwierdzenia +Name[pt]=A torrente não pode ser carregada silenciosamente +Name[pt_BR]=O torrent não pode ser carregado silenciosamente +Name[ro]=Torentul nu poate fi încărcat silențios +Name[ru]=Торрент не может быть загружен без выдачи сообщений +Name[si]=ටොරෙන්ට් නිහඬච පූර්ණය කල නොහැක +Name[sk]=Torrent sa nedá načítať potichu +Name[sl]=Torrenta ni mogoče naložiti potiho +Name[sq]=Torrenti nuk mund të ngarkohet në heshtje +Name[sr]=Не могу да тихо учитам торент +Name[sr@ijekavian]=Не могу да тихо учитам торент +Name[sr@ijekavianlatin]=Ne mogu da tiho učitam torent +Name[sr@latin]=Ne mogu da tiho učitam torent +Name[sv]=Dataflödet kan inte laddas tyst +Name[tr]=Torrent sessizce yüklenemedi +Name[uk]=Неможливо завантажити торент без додаткової інформації +Name[x-test]=xxTorrent cannot be loaded silentlyxx +Name[zh_CN]=无法静默地加载种子 +Name[zh_TW]=Torrent 無法安靜地被載入 +Action=Sound|Popup + +[Event/DHTNotEnabled] +Name=DHT is not enabled +Name[ar]=‏DHT ليس مفعّلًا +Name[ast]=DHT nun ta activáu +Name[bg]=Не е включено DHT +Name[bs]=DHT nije uključen +Name[ca]=El DHT no està actiu +Name[ca@valencia]=El DHT no està actiu +Name[cs]=DHT není povoleno +Name[da]=DHT er ikke aktiveret +Name[de]=VHT ist nicht aktiviert +Name[el]=Το DHT δεν είναι ενεργοποιημένο +Name[en_GB]=DHT is not enabled +Name[es]=DHT no está habilitado +Name[et]=DHT pole lubatud +Name[fi]=DHT ei ole käytössä +Name[fr]=DHT n'est pas activé +Name[ga]=Níl DHT cumasaithe +Name[gl]=O DHT non está activado +Name[hr]=DHT nije omogućen +Name[hu]=A DHT nincs engedélyezve +Name[is]=DHT er óvirkt +Name[it]=Il DHT non è attivo +Name[kk]=DHT рұқсат етілмеген +Name[km]=DHT មិន​ត្រូវ​បាន​បើក​ទេ +Name[ko]=DHT가 비활성화됨 +Name[lt]=DHT neįgalintas +Name[nb]=DHT er ikke slått på +Name[nds]=VPT is nich anmaakt +Name[nl]=DHT is niet ingeschakeld +Name[nn]=DHT er ikkje slått på +Name[pl]=DHT nie zostało właczone +Name[pt]=O DHT não está activo +Name[pt_BR]=O DHT não está habilitado +Name[ro]=DHT nu este activat +Name[ru]=DHT отключён +Name[si]=DHT සක්‍රීයව නොමැත +Name[sk]=DHT nie je povolené +Name[sl]=DHT ni omogočen +Name[sr]=ДХТ није укључен +Name[sr@ijekavian]=ДХТ није укључен +Name[sr@ijekavianlatin]=DHT nije uključen +Name[sr@latin]=DHT nije uključen +Name[sv]=DHT är inte aktiverat +Name[tr]=DHT etkin değil +Name[uk]=DHT не увімкнено +Name[x-test]=xxDHT is not enabledxx +Name[zh_CN]=未启用 DHT +Name[zh_TW]=DHT 未開啟 +Action=Sound|Popup + +[Event/PluginEvent] +Name=Event generated by plugin +Name[ar]=ولّدت ملحقة حدثًا +Name[ast]=Eventu xeneráu por un plugin +Name[bs]=Događaj generisan priključkom +Name[ca]=Esdeveniment generat pel connector +Name[ca@valencia]=Esdeveniment generat pel connector +Name[cs]=Událost generovaná modulem +Name[da]=Hændelse genereret af plugin +Name[de]=Ereignis von Modul erzeugt +Name[el]=Γεγονός που δημιουργήθηκε από πρόσθετο +Name[en_GB]=Event generated by plugin +Name[es]=Evento generado por el complemento +Name[et]=Plugina genereeritud sündmus +Name[fi]=Liitännäisen tuottama tapahtuma +Name[fr]=Évènement généré par un module externe +Name[ga]=Teagmhas cruthaithe ag breiseán +Name[gl]=Suceso xerado por un complemento +Name[hu]=A bővítmény által létrehozott esemény +Name[it]=Evento generato da un'estensione +Name[kk]=Плагин жасаған оқиға +Name[km]=ព្រឹត្តិការណ៍​បាន​បង្កើត​ដោ​កម្មវិធី​ជំនួយ +Name[ko]=플러그인에서 생성한 이벤트 +Name[lt]=Įvykis sugeneruotas priedo +Name[nb]=Programtillegg genererte en hendelse +Name[nds]=Vun Moduul opstellt Begeefnis +Name[nl]=Door plugin gegenereerde gebeurtenis +Name[pl]=Zdarzenie wygenerowane przez wtyczkę +Name[pt]=Evento gerado pelo 'plugin' +Name[pt_BR]=Evento gerado pelo plugin +Name[ro]=Eveniment generat de extensie +Name[ru]=Событие получено от модуля +Name[si]=ප්ලගිනය මගින් ජනිත අවස්ථාවක් +Name[sk]=Udalosť generovaná pluginom +Name[sl]=Dogodek je ustvaril vstavek +Name[sr]=Догађај генерисан прикључком +Name[sr@ijekavian]=Догађај генерисан прикључком +Name[sr@ijekavianlatin]=Događaj generisan priključkom +Name[sr@latin]=Događaj generisan priključkom +Name[sv]=Händelse skapad av insticksprogram +Name[tr]=Olay eklenti tarafından oluşturuldu +Name[uk]=Додатком створено повідомлення про подію +Name[x-test]=xxEvent generated by pluginxx +Name[zh_CN]=插件产生的事件 +Name[zh_TW]=外掛程式產生的事件 +Action=Sound|Popup + +[Event/MagnetLinkDownloadStarted] +Name=Magnet link download started +Name[ar]=بدأ تنزيل الوصلة الممغنطة +Name[bs]=Magnetno preuzimanje datoteka započelo +Name[ca]=S'ha iniciat la baixada de l'enllaç «magnet» +Name[ca@valencia]=S'ha iniciat la baixada de l'enllaç «magnet» +Name[cs]=Stahování odkazu Magnet bylo zahájeno +Name[da]=Download af magnet-link startet +Name[de]=Download der Magnet-Verknüpfung wurde gestartet +Name[el]=Η λήψη συνδέσμου μαγνήτη ξεκίνησε +Name[en_GB]=Magnet link download started +Name[es]=Se ha iniciado la descarga del enlace magnético +Name[et]=Magnetlingi allalaadimine on alanud +Name[fi]=Magnet-linkkilataus aloitettiin +Name[fr]=Le téléchargement du lien « Magnet » a démarré +Name[ga]=Íosluchtú naisc maighnéid tosaithe +Name[gl]=Comezou a descarga dunha ligazón Magnet +Name[hu]=A magnethivatkozás letöltése elkezdődött +Name[it]=Avviato scaricamento di collegamento magnet +Name[kk]=Магнит-сілтемені жүктеп алу басталды +Name[km]=តំណ Magnet ដែល​បាន​ចាប់ផ្ដើម​ទាញ​យក +Name[ko]=마그넷 링크 다운로드 시작됨 +Name[lt]=Magnet nuorodos atsiuntimas pradėtas +Name[nb]=Magnetlenke-nedlasting startet +Name[nds]=Daalladen vun en Magnetlink anfungen +Name[nl]=Download van Magneetlink gestart +Name[pl]=Rozpoczęto pobieranie z odnośnika magnet +Name[pt]=Transferência de ligação Magnet iniciada +Name[pt_BR]=Download do link magnético iniciado +Name[ro]=Descărcarea legăturii magnet a început +Name[ru]=Загрузка magnet-ссылки начата +Name[sk]=Spustené sťahovanie Magnet odkazu +Name[sl]=Prejem povezave Magnet je bil začet +Name[sr]=Покренуто преузимање магнетске везе +Name[sr@ijekavian]=Покренуто преузимање магнетске везе +Name[sr@ijekavianlatin]=Pokrenuto preuzimanje magnetske veze +Name[sr@latin]=Pokrenuto preuzimanje magnetske veze +Name[sv]=Nerladdning med magnetlänk startad +Name[tr]=Magnet bağlantı indirmesi başlatıldı +Name[uk]=Розпочато отримання за маґнет-посиланням +Name[x-test]=xxMagnet link download startedxx +Name[zh_CN]=已开始磁力链下载 +Name[zh_TW]=Magnet 連結下載已開始 +Action=Sound|Popup + +[Event/MagnetLinkCopied] +Name=Magnet link copied to clipboard +Name[ca]=L'enllaç magnet s'ha copiat al porta-retalls +Name[ca@valencia]=L'enllaç magnet s'ha copiat al porta-retalls +Name[cs]=Magnet link zkopírován do schránky +Name[da]=Magnet-link kopieret til udklipsholderen +Name[de]=Magnet-Verknüpfung in die Zwischenablage kopiert +Name[el]=Έγινε αντιγραφή του σύνδεσμου μαγνήτη στο πρόχειρο +Name[en_GB]=Magnet link copied to clipboard +Name[es]=Enlace de Magnet copiado al portapapeles +Name[et]=Magnetlink kopeeriti lõikepuhvrisse +Name[fi]=Magnet-linkki kopioitu leikepöydälle +Name[fr]=Lien « Magnet » copié dans le presse-papier +Name[it]=Collegamento magnet copiato negli appunti +Name[ko]=마그넷 링크가 클립보드에 복사됨 +Name[nl]=Magneet-koppeling gekopieerd naar klembord +Name[pl]=Łacze magnet skopiowano do schowka +Name[pt]=Ligação Magnet copiada para a área de transferência +Name[pt_BR]=Link magnético copiado para a área de transferência +Name[sk]=Odkaz Magnet skopírovaný do schránky +Name[sl]=Povezava s spletiščem Magnet je bila skopirana v odložišče +Name[sv]=Magnetisk länk kopierad till klippbordet +Name[uk]=Маґнет-посилання скопійовано до буфера обміну +Name[x-test]=xxMagnet link copied to clipboardxx +Name[zh_CN]=磁力链接已复制到剪贴板 +Name[zh_TW]=已複製 Magnet 連結到剪貼簿 +Action=Popup diff --git a/ktorrent/ktorrentui.rc b/ktorrent/ktorrentui.rc new file mode 100644 index 0000000..5788cec --- /dev/null +++ b/ktorrent/ktorrentui.rc @@ -0,0 +1,33 @@ + + + + + &File + + + + + + + + + + + &View + + + + + + + + +Main + + + + + + + + diff --git a/ktorrent/kttorrentactivityui.rc b/ktorrent/kttorrentactivityui.rc new file mode 100644 index 0000000..5b9aa12 --- /dev/null +++ b/ktorrent/kttorrentactivityui.rc @@ -0,0 +1,97 @@ + + + + + + + + Torrents + + + + + + + + + + + + + + + + + + +Groups + + + + + + + + + + + + +View + + + + + + + + + + + + + + + Advanced + + + + + + + + Open Directory + + + + + + Add to Group + + + + + + + + Configure Columns + Columns + + + + + +Torrents + + + + + + + + + + + + + diff --git a/ktorrent/main.cpp b/ktorrent/main.cpp new file mode 100644 index 0000000..ee230a9 --- /dev/null +++ b/ktorrent/main.cpp @@ -0,0 +1,245 @@ +/* + SPDX-FileCopyrightText: 2007 Joris Guisson + SPDX-FileCopyrightText: 2007 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "gui.h" +#include +#include +#include + +#include "ktversion.h" +#include "version.h" +#include +#include +#include +#ifndef Q_OS_WIN +#include +#endif + +using namespace bt; + +#ifndef Q_WS_WIN +bool GrabPIDLock() +{ + // open the PID file in the /tmp directory and attempt to lock it + QString pid_file = QDir::tempPath() + QStringLiteral("/.ktorrent_kf5_%1.lock").arg(getuid()); + + int fd = open(QFile::encodeName(pid_file).data(), O_RDWR | O_CREAT, 0640); + if (fd < 0) { + fprintf(stderr, "Failed to open KT lock file %s : %s\n", pid_file.toLatin1().constData(), strerror(errno)); + return false; + } + + if (lockf(fd, F_TLOCK, 0) < 0) { + fprintf(stderr, "Failed to get lock on %s : %s\n", pid_file.toLatin1().constData(), strerror(errno)); + return false; + } + + char str[20]; + sprintf(str, "%d\n", getpid()); + write(fd, str, strlen(str)); /* record pid to lockfile */ + + // leave file open, so nobody else can lock it until KT exists + return true; +} +#endif + +int main(int argc, char **argv) +{ +#ifndef Q_WS_WIN + // ignore SIGPIPE and SIGXFSZ + signal(SIGPIPE, SIG_IGN); + signal(SIGXFSZ, SIG_IGN); +#endif + + if (!bt::InitLibKTorrent()) { + fprintf(stderr, "Failed to initialize libktorrent\n"); + return -1; + } + + bt::SetClientInfo(QStringLiteral("KTorrent"), kt::MAJOR, kt::MINOR, kt::RELEASE, kt::VERSION_TYPE, QStringLiteral("KT")); + + QGuiApplication::setAttribute(Qt::AA_UseHighDpiPixmaps); + QCoreApplication::setAttribute(Qt::AA_ShareOpenGLContexts); + KLocalizedString::setApplicationDomain("ktorrent"); + + QApplication app(argc, argv); + app.setWindowIcon(QIcon::fromTheme(QStringLiteral("ktorrent"))); + KCrash::initialize(); + + QCommandLineParser parser; + KAboutData about(QStringLiteral("ktorrent"), + i18nc("@title", "KTorrent"), + QStringLiteral(VERSION), + i18n("Bittorrent client by KDE"), + KAboutLicense::GPL, + i18nc("@info:credit", "(C) 2005 - 2011 Joris Guisson and Ivan Vasic"), + QString(), + QStringLiteral("http://www.kde.org/applications/internet/ktorrent/")); + + about.setOrganizationDomain(QByteArray("kde.org")); + about.setDesktopFileName(QStringLiteral("org.kde.ktorrent")); + + about.addAuthor(i18n("Joris Guisson"), + QString(), + QStringLiteral("joris.guisson@gmail.com"), + QStringLiteral("http://kde.org/applications/internet/ktorrent")); + about.addAuthor(i18n("Ivan Vasic"), QString(), QStringLiteral("ivasic@gmail.com")); + about.addAuthor(i18n("Alan Jones"), i18n("BitFinder Plugin"), QStringLiteral("skyphyr@gmail.com")); + about.addCredit(i18n("Diego Rosario Brogna"), i18n("Webinterface Plugin, global max share ratio patch"), QStringLiteral("dierbro@gmail.com")); + about.addAuthor(i18n("Krzysztof Kundzicz"), i18n("Statistics Plugin"), QStringLiteral("athantor@gmail.com")); + about.addAuthor(i18n("Christian Weilbach"), i18n("kio-magnet"), QStringLiteral("christian_weilbach@web.de")); + about.addCredit(i18n("Mladen Babic"), i18n("Application icon and a couple of others"), QStringLiteral("bmladen@EUnet.yu")); + about.addCredit(i18n("Adam Treat"), QString(), QStringLiteral("treat@kde.org")); + about.addCredit(i18n("Danny Allen"), i18n("1.0 application icon"), QStringLiteral("danny@dannyallen.co.uk")); + about.addCredit(i18n("Vincent Wagelaar"), QString(), QStringLiteral("vincent@ricardis.tudelft.nl")); + about.addCredit(i18n("Knut Morten Johansson"), QString(), QStringLiteral("knut@johansson.com")); + about.addCredit(i18n("Felix Berger"), i18n("ChunkBar's tooltip and IWFileTreeItem sorting"), QStringLiteral("bflat1@gmx.net")); + about.addCredit(i18n("Andreas Kling"), QString(), QStringLiteral("kling@impul.se")); + about.addCredit(i18n("Felipe Sateler"), QString(), QStringLiteral("fsateler@gmail.com")); + about.addCredit(i18n("Maxmind"), + i18n("Country locator for InfoWidget plugin. Flags are taken from http://flags.blogpotato.de/ so thanks to them too."), + QString(), + QStringLiteral("http://www.maxmind.com/")); + about.addCredit(i18n("Adam Forsyth"), i18n("File prioritization and some other patches"), QStringLiteral("agforsyth@gmail.com")); + about.addCredit(i18n("Thomas Bernard"), + i18n("Miniupnp was used as an example for our own UPnP implementation"), + QString(), + QStringLiteral("http://miniupnp.free.fr/")); + about.addCredit(i18n("Lesly Weyts"), i18n("Zeroconf enhancements")); + about.addCredit(i18n("Kevin Andre"), i18n("Zeroconf enhancements"), QString(), QStringLiteral("http://users.edpnet.be/hyperquantum/")); + about.addCredit(i18n("Dagur Valberg Johannsson"), i18n("Coldmilk webgui"), QStringLiteral("dagurval@pvv.ntnu.no")); + about.addCredit(i18n("Alexander Dymo"), i18n("IDEAl code from KDevelop"), QStringLiteral("adymo@kdevelop.org")); + about.addCredit(i18n("Scott Wolchok"), i18n("Conversion speed improvement in ipfilter plugin"), QStringLiteral("swolchok@umich.edu")); + about.addCredit(i18n("Bryan Burns of Juniper Networks"), i18n("Discovered 2 security vulnerabilities (both are fixed)")); + about.addCredit(i18n("Goten Xiao"), i18n("Patch to load silently with a save location")); + about.addCredit(i18n("Rapsys"), i18n("Fixes in PHP code of webinterface")); + about.addCredit(i18n("Athantor"), i18n("XFS specific disk preallocation")); + about.addCredit(i18n("twisted_fall"), i18n("Patch to not show very low speeds"), QStringLiteral("twisted.fall@gmail.com")); + about.addCredit(i18n("Lucke"), i18n("Patch to show potentially firewalled status")); + about.addCredit(i18n("Modestas Vainius"), i18n("Several patches"), QStringLiteral("modestas@vainius.eu")); + about.addCredit(i18n("Stefan Monov"), i18n("Patch to hide menu bar"), QStringLiteral("logixoul@gmail.com")); + about.addCredit(i18n("The_Kernel"), i18n("Patch to change file priorities in the webgui"), QStringLiteral("kernja@cs.earlham.edu")); + about.addCredit(i18n("Rafał Miłecki"), i18n("Several webgui patches"), QStringLiteral("zajec5@gmail.com")); + about.addCredit(i18n("Ozzi"), i18n("Fixes for several warnings"), QStringLiteral("ossi@masiina.no-ip.info")); + about.addCredit(i18n("Markus Brueffer"), i18n("Patch to fix free diskspace calculation on FreeBSD"), QStringLiteral("markus@brueffer.de")); + about.addCredit(i18n("Lukas Appelhans"), i18n("Patch to fix a crash in ChunkDownloadView"), QStringLiteral("l.appelhans@gmx.de")); + about.addCredit(i18n("Rickard Närström"), i18n("A couple of bugfixes"), QStringLiteral("rickard.narstrom@gmail.com")); + about.addCredit(i18n("caruccio"), i18n("Patch to load torrents silently from the command line"), QStringLiteral("mateus@caruccio.com")); + about.addCredit(i18n("Lee Olson"), i18n("New set of icons"), QStringLiteral("leetolson@gmail.com")); + about.addCredit(i18n("Aaron J. Seigo"), i18n("Drag and drop support for Plasma applet"), QStringLiteral("aseigo@kde.org")); + about.addCredit(i18n("Ian Higginson"), i18n("Patch to cleanup the plugin list"), QStringLiteral("xeriouxi@fastmail.fm")); + about.addCredit(i18n("Amichai Rothman"), i18n("Patch to make the Plasma applet a popup applet"), QStringLiteral("amichai@amichais.net")); + about.addCredit(i18n("Leo Trubach"), i18n("Patch to add support for IP ranges in IP filter dialog"), QStringLiteral("leotrubach@gmail.com")); + about.addCredit(i18n("Andrei Barbu"), i18n("Feature which adds the date a torrent was added"), QStringLiteral("andrei@0xab.com")); + about.addCredit(i18n("Jonas Lundqvist"), i18n("Feature to disable authentication in the webinterface"), QStringLiteral("jonas@gannon.se")); + about.addCredit(i18n("Jaroslaw Swierczynski"), i18n("Exclusion patterns in the syndication plugin"), QStringLiteral("swiergot@gmail.com")); + about.addCredit(i18n("Alexey Shildyakov "), i18n("Patch to rename single file torrents to the file inside"), QStringLiteral("ashl1future@gmail.com")); + about.addCredit(i18n("Maarten De Meyer"), i18n("Fix for bug 305379"), QStringLiteral("de.meyer.maarten@gmail.com")); + about.addCredit(i18n("Rex Dieter"), i18n("Add support for x-scheme-handler/magnet mimetype"), QStringLiteral("rdieter@gmail.com")); + about.addCredit(i18n("Leszek Lesner"), i18n("Fix for bug 339584"), QStringLiteral("leszek.lesner@web.de")); + about.addCredit(i18n("Andrius Štikonas"), i18n("KF5 porting"), QStringLiteral("andrius@stikonas.eu")); + about.addCredit(i18n("Nick Shaforostoff"), i18n("KF5 porting"), QStringLiteral("shaforostoff@gmail.com")); + about.addCredit(i18n("Alexander Trufanov"), i18n("Bugfixes, cleanups and optimizations"), QStringLiteral("trufanovan@gmail.com")); + + KAboutData::setApplicationData(about); + about.setupCommandLine(&parser); + parser.addOption(QCommandLineOption(QStringList() << QStringLiteral("verbose"), i18n("Enable logging to standard output"))); + parser.addOption(QCommandLineOption(QStringList() << QStringLiteral("silent"), i18n("Silently open torrent given on URL"))); + parser.addOption(QCommandLineOption(QStringList() << QStringLiteral("+[URL]"), i18n("Document to open"))); + parser.process(app); + about.processCommandLine(&parser); + + const KDBusService dbusService(KDBusService::Unique); + +#if 0 // ndef Q_WS_WIN + // need to grab lock after the fork call in start, otherwise this will not work properly + if (!GrabPIDLock()) { + fprintf(stderr, "ktorrent is already running !\n"); + return 0; + + } +#endif + + try { +#ifndef Q_WS_WIN + bt::SignalCatcher catcher; + catcher.catchSignal(SIGINT); + catcher.catchSignal(SIGTERM); + QObject::connect(&catcher, &bt::SignalCatcher::triggered, &app, &QApplication::quit); +#endif + + const bool logToStdout = parser.isSet(QStringLiteral("verbose")); + bt::InitLog(kt::DataDir(kt::CreateIfNotExists) + QLatin1String("log"), true, true, logToStdout); + + kt::GUI widget; + + auto handleCmdLine = [&widget, &parser](const QStringList &arguments, const QString &workingDirectory) { + if (!arguments.isEmpty()) { + parser.parse(arguments); +#if KWINDOWSYSTEM_VERSION >= QT_VERSION_CHECK(5, 62, 0) + widget.setAttribute(Qt::WA_NativeWindow, true); + KStartupInfo::setNewStartupId(widget.windowHandle(), KStartupInfo::startupId()); +#else + KStartupInfo::setNewStartupId(&widget, KStartupInfo::startupId()); +#endif + KWindowSystem::forceActiveWindow(widget.winId()); + } + QString oldCurrent = QDir::currentPath(); + if (!workingDirectory.isEmpty()) + QDir::setCurrent(workingDirectory); + + bool silent = parser.isSet(QStringLiteral("silent")); + auto loadMethod = silent ? &kt::GUI::loadSilently : &kt::GUI::load; + const auto positionalArguments = parser.positionalArguments(); + for (const QString &filePath : positionalArguments) { + QUrl url = QFile::exists(filePath) ? QUrl::fromLocalFile(filePath) : QUrl(filePath); + (widget.*loadMethod)(url); + } + + if (!workingDirectory.isEmpty()) + QDir::setCurrent(oldCurrent); + }; + QObject::connect(&dbusService, &KDBusService::activateRequested, handleCmdLine); + QObject::connect(&dbusService, &KDBusService::activateRequested, &widget, &kt::GUI::show); + handleCmdLine(QStringList(), QString()); + + app.setQuitOnLastWindowClosed(false); + app.exec(); + } catch (bt::Error &err) { + Out(SYS_GEN | LOG_IMPORTANT) << "Uncaught exception: " << err.toString() << endl; + } catch (std::exception &err) { + Out(SYS_GEN | LOG_IMPORTANT) << "Uncaught exception: " << err.what() << endl; + } catch (...) { + Out(SYS_GEN | LOG_IMPORTANT) << "Uncaught unknown exception " << endl; + } + bt::Globals::cleanup(); + return 0; +} diff --git a/ktorrent/org.kde.ktorrent.appdata.xml b/ktorrent/org.kde.ktorrent.appdata.xml new file mode 100644 index 0000000..091d95e --- /dev/null +++ b/ktorrent/org.kde.ktorrent.appdata.xml @@ -0,0 +1,814 @@ + + + org.kde.ktorrent.desktop + FSFAP + GPL-2.0+ + KTorrent + سيولك + KTorrent + KTorrent + KTorrent + KTorrent + KTorrent + KTorrent + KTorrent + KTorrent + KTorrent + KTorrent + KTorrent + KTorrent + KTorrent + KTorrent + KTorrent + KTorrent + KTorrent + KTorrent + KTorrent + KTorrent + KTorrent + KTorrent + KTorrent + KTorrent + К‑торент + KTorrent + К‑торент + KTorrent + Ktorrent + KTorrent + KTorrent + xxKTorrentxx + KTorrent + KTorrent + BitTorrent Client + عميل بِت‌تورنت + Client de BitTorrent + Client de BitTorrent + Klient pro BitTorrent + BitTorrent-klient + BitTorrent-Programm + Εφαρμογή BitTorrent + BitTorrent Client + Cliente de BitTorrent + BitTorrenti klient + BitTorrent-asiakas + Client BitTorrent + Cliente de BitTorrent + Cliente de BitTorrent + BitTorrent Client + Client BitTorrent + 비트토렌트 클라이언트 + BitTorrent-cliënt + Bittorrent-klient + Klient BitTorrent + Cliente de BitTorrent + Cliente BitTorrent + Клиент BitTorrent + BitTorrent Klient + Odjemalec BitTorrent + Битторент клијент + Bittorrent klijent + Битторент клијент + Bittorrent klijent + Dataflödesprogram + BitTorrent İstemcisi + Клієнт BitTorrent + xxBitTorrent Clientxx + BitTorrent 客户端 + BitTorrent 用戶端 + +

+ KTorrent is a BitTorrent application by KDE which allows you to download files using + the BitTorrent protocol. It enables you to run multiple torrents at the same time and + comes with extended features to make it a full-featured client for BitTorrent. +

+

يتيح لك «سيولك» (وهو تطبيق ”بِت‌تورنت“ من «كدي») تنزيل الملفات مستخدما ميفاق ”بِت‌تورنت“. يسمح لك التطبيق بتشغيل عدد من السيول في الوقت نفسه وهو يأتي مع مزايا عدّة تجعله عميلًا كامل المواصفات لميفاق ”بِت‌تورنت“.

+

El KTorrent és una aplicació de BitTorrent creada per la comunitat KDE que us permetrà descarregar fitxers utilitzant el protocol BitTorrent. Permet executar múltiples torrent alhora i ve amb característiques esteses per a fer-lo un client complet per a BitTorrent.

+

El KTorrent és una aplicació de BitTorrent creada per la comunitat KDE que vos permetrà descarregar fitxers utilitzant el protocol BitTorrent. Permet executar múltiples torrent alhora i ve amb característiques esteses per a fer-lo un client complet per a BitTorrent.

+

KTorrent er et BitTorrent-program fra KDE, som lader dig downloade filer med BitTorrent-protokollen. Den lader dig køre flere torrents på samme tid og har udvidet funktionalitet som gør det til en komplet BitTorrent-klient.

+

KTorrent ist ein BitTorrent-Programm von KDE. Sie können damit Dateien über das BitTorrent-Protokoll herunterladen. Mehrere Torrents lassen sich gleichzeitig ausführen. KTorrent hat zusätzliche Fähigkeiten und ist damit ein mit allen Funktionen ausgestattetes Dienstprogramm für BitTorrent.

+

Το KTorrent είναι μια εφαρμογή BitTorrent από το KDE την οποία σας επιτρέπει να κάνετε λήψεις αρχείων με χρήσητ του πρωτοκόλλου BitTorrent. Σας δίνει τη δυνατότητα να εκετελείτε πολλά torrents ταυτόχρονα και συνοδεύεται με εκτεταμένες λειτουργίες για μια πλήρη χαρακτηριστικών εφαρμογή BitTorrent.

+

KTorrent is a BitTorrent application by KDE which allows you to download files using the BitTorrent protocol. It enables you to run multiple torrents at the same time and comes with extended features to make it a full-featured client for BitTorrent.

+

KTorrent es una aplicación bitTorrent de KDE que le permite descargar archivos usando el protocolo BitTorrent. Permite ejecutar varios torrents al mismo tiempo e incluye características extendidas para convertilo en un cliente completo para BitTorrent.

+

KTorrent on KDE BitTorrenti rakendus, mis võimaldab faile alla laadida BitTorrenti protokolli kasutades. Korraga saab käitada mitut torrentit ning rakendus pakub ohtralt võimalusi, olles niisiis igati täiuslik BitTorrenti klient.

+

KTorrent on KDE:n BitTorrent-sovellus, jolla voit ladata tiedostoja BitTorrent-yhteyskäytännöllä. Sovelluksella voit suorittaa useita torrentteja samanaikaisesti, ja siinä on erityisominaisuuksia, jotka tekevät siitä täydellisen BitTorrent-asiakkaan.

+

KTorrent est une application BitTorrent par KDE qui vous permet de télécharger des fichiers en utilisant le protocole BitTorrent. Il peut gérer plusieurs torrents en même temps et possède des fonctionnalités étendues qui en font un client complet pour BitTorrent.

+

KTorrent é unha aplicación de BitTorrent por KDE que permítelle descargar ficheiros usando o protocolo BitTorrent. Permítelle executar varios torrents ao mesmo tempo e inclúe funcionalidades adicionais que o converten nun cliente de BitTorrent completo.

+

KTorrent adalah sebuah aplikasi BitTorrent oleh KDE yang mana memungkinkan kamu untuk mengunduh file menggunakan protokol BitTorrent. Yang sanggup saat kamu menjalankan torrent secara multiple di waktu yang bersamaan dan hadir dengan fitur yang diperluas untuk membuat sebuah klien yang berfitur penuh buat BitTorrent.

+

KTorrent è un'applicazione BitTorrent della comunità KDE, che ti permette di scaricare i file usando il protocollo BitTorrent. Ti consente di avviare contemporaneamente torrent multipli, e ti arriva con funzionalità estese, che lo rendono un client di BitTorrent ricco di funzionalità.

+

KTorrent는 KDE의 비트토렌트 클라이언트입니다. 비트토렌트 프로토콜을 사용하여 파일을 다운로드할 수 있습니다. 토렌트 여러 개를 동시에 다운로드할 수 있으며 다양한 추가 기능을 제공합니다.

+

KTorrent is een BitTorrent-toepassing door KDE die u in staat stelt bestanden te downloaden met het BitTorrent-protocol. U kunt meerdere torrents tegelijk doen en het komt met uitgebreide functies om het een client voor BitTorrent te laten zijn met alle mogelijkheden.

+

KTorrent jest aplikacją BitTorrent stworzoną w ramach KDE, która umożliwia ci pobieranie przy użyciu protokołu BitTorrent. Umożliwia ci uruchamianie wielu torrentów w tym samym czasie i jest wyposażona w dodatkowe możliwości tworzące z niej pełnego klienta dla BitTorrent.

+

O KTorrent é uma aplicação de BitTorrent do KDE que lhe permite obter ficheiros pelo protocolo BitTorrent. Ele permite-lhe executar várias torrentes ao mesmo tempo e vem com funcionalidades alargadas que o tornam um cliente pleno de funcionalidades para o BitTorrent.

+

O KTorrent é um aplicativo BitTorrent do KDE que permite a você baixar arquivos usando o protocolo BitTorrent. Ele permite baixar múltiplos torrents ao mesmo tempo e vem com recursos estendidos para torná-lo cliente com recursos completos para o BitTorrent.

+

KTorrent je BitTorrentová aplikácia od KDE, ktorá vám umožní stiahnuť súbory pomocou protokolu BitTorrent. Umožní vám spustiť viac torrentov súčasne a obsahuje rôzne funkcie, čo z nej robí plnohodnotného klienta pre BitTorrent.

+

KTorrent je program za BitTorrent v KDE, ki omogoča prenos datoteke s protokolom BitTorrent. Omogoča vam zagon več sočasnih tokovhkrati pa ima razširjene funkcije, da postane popoln odjemalec za BitTorrent.

+

К‑торент је програм из КДЕ‑а којим можете да преузимате фајлове преко протокола битторент. Омогућава покретање више торената истовремено и пружа разноврсне проширене могућности, што га чини га свеобухватним битторент клијентом.

+

KTorrent je program iz KDE‑a kojim možete da preuzimate fajlove preko protokola Bittorrent. Omogućava pokretanje više torenata istovremeno i pruža raznovrsne proširene mogućnosti, što ga čini ga sveobuhvatnim bittorrent klijentom.

+

К‑торент је програм из КДЕ‑а којим можете да преузимате фајлове преко протокола битторент. Омогућава покретање више торената истовремено и пружа разноврсне проширене могућности, што га чини га свеобухватним битторент клијентом.

+

KTorrent je program iz KDE‑a kojim možete da preuzimate fajlove preko protokola Bittorrent. Omogućava pokretanje više torenata istovremeno i pruža raznovrsne proširene mogućnosti, što ga čini ga sveobuhvatnim bittorrent klijentom.

+

Ktorrent är ett dataflödesprogram av KDE som låter dig ladda ner filer med protokollet BitTorrent. Det möjliggör att hantera flera dataflöden samtidigt och levereras med utökade funktioner som gör det till en fullfjädrad klient för BitTorrent.

+

KTorrent, KDE tarafından geliştirilen, BitTorrent protokolünü kullanarak dosyaları indirmenize izin veren bir BitTorrent uygulamasıdır. Aynı anda birden fazla torrent çalıştırabilmenizi ve BitTorrent için tam özellikli bir istemci yapabilmeniz için genişletilmiş özellikler ile birlikte geliştirilmiştir.

+

KTorrent — програма для обміну даними торентів, створена розробниками KDE, за допомогою якої ви можете отримувати і розповсюджувати дані на основі протоколу BitTorrent. Ця програма надає вам змогу одночасно обмінюватися даними декількох торентів. Також передбачено додаткові можливості, які роблять програму повноцінною клієнтською частиною для обміну даними BitTorrent.

+

xxKTorrent is a BitTorrent application by KDE which allows you to download files using the BitTorrent protocol. It enables you to run multiple torrents at the same time and comes with extended features to make it a full-featured client for BitTorrent.xx

+

KTorrent 是 KDE 提供的 BT 下载应用。支持同时运行多个种子,并具有多种扩展特性,是一个全功能的 BT 客户端。

+

Features:

+

المزايا

+

Característiques:

+

Característiques:

+

Vlastnosti:

+

Funktioner:

+

Funktionen:

+

Χαρακτηριστικά:

+

Features:

+

Características:

+

Omadused:

+

Ominaisuuksia:

+

Fonctionnalités :

+

Funcionalidades:

+

Characteristicas:

+

Fitur:

+

Funzionalità:

+

기능:

+

Mogelijkheden:

+

Funksjonar:

+

Możliwości:

+

Funcionalidades:

+

Recursos:

+

Возможности:

+

Funkcie:

+

Lastnosti:

+

Могућности:

+

Mogućnosti:

+

Могућности:

+

Mogućnosti:

+

Funktioner:

+

Özellikler:

+

Можливості:

+

xxFeatures:xx

+

功能:

+

功能:

+
    +
  • Queuing of torrents
  • +
  • وضع السيول في طوابير
  • +
  • Cua dels torrent
  • +
  • Cua dels torrent
  • +
  • Řazení torrentů do fronty
  • +
  • Sæt torrents i kø
  • +
  • Verwaltung von Warteschlangen für Torrents
  • +
  • Ουρές από torrents
  • +
  • Queuing of torrents
  • +
  • Encolado de torrents
  • +
  • Torrentide seadmine järjekorda
  • +
  • Torrenttien jonotus
  • +
  • Mise en file d'attente des torrents
  • +
  • Consulta de torrents.
  • +
  • Pengatrean torrent
  • +
  • Accodamento dei torrent
  • +
  • 토렌트 대기열
  • +
  • Torrents in de wachtrij zetten
  • +
  • Kolejkowanie torrentów
  • +
  • Fila de espera de torrentes
  • +
  • Enfileiramento de torrents
  • +
  • Загрузка торрентов по очереди
  • +
  • Zaraďovanie torrentov
  • +
  • Čakalne vrste podatkovnih tokov
  • +
  • Стављање торената у ред.
  • +
  • Stavljanje torenata u red.
  • +
  • Стављање торената у ред.
  • +
  • Stavljanje torenata u red.
  • +
  • Köa dataflöden
  • +
  • Torrent kuyruğu
  • +
  • Черга торентів
  • +
  • xxQueuing of torrentsxx
  • +
  • 队列种子
  • +
  • Global and per torrent speed limits
  • +
  • حدود سرعة عمومية وخاصة لكل سيل
  • +
  • Límits de la velocitat global i per torrent
  • +
  • Límits de la velocitat global i per torrent
  • +
  • Hastighedsgrænser globalt og pr. torrent
  • +
  • Geschwindigkeitsbegrenzungen global und für den einzelnen Torrent
  • +
  • Καθολικά και ανά torrent όρια ταχύτητας
  • +
  • Global and per torrent speed limits
  • +
  • Límites de velocidad globales e individuales
  • +
  • Kiiruse üldine ja torrentipõhine piiramine
  • +
  • Yleinen tai torrenttikohtainen nopeusrajoitus
  • +
  • Limites de vitesse globales et par torrent
  • +
  • Límites de velocidade globais e por torrent.
  • +
  • Batas kecepatan per torrent dan global
  • +
  • Limiti di velocità globali e per torrent
  • +
  • 전역 및 토렌트별 속도 제한
  • +
  • Globale en per torrent snelheidslimieten
  • +
  • Ograniczenie prędkości globalne i na torrenta
  • +
  • Limites de velocidades globais e por torrentes
  • +
  • Limites de velocidade global e por torrent
  • +
  • Общие и отдельные для каждого торрента ограничения скорости
  • +
  • Globálne a torrentové rýchlostné limity
  • +
  • Omejitve hitrosti prenosa za celoto in po posameznem toku
  • +
  • Ограничења брзине, глобална и по торенту.
  • +
  • Ograničenja brzine, globalna i po torentu.
  • +
  • Ограничења брзине, глобална и по торенту.
  • +
  • Ograničenja brzine, globalna i po torentu.
  • +
  • Hastighetsbegränsningar, allmänna och per dataflöde
  • +
  • Evrensel ve torrent'e özel hız sınırı
  • +
  • Обмеження на швидкість на загальному рівні і для окремих торентів
  • +
  • xxGlobal and per torrent speed limitsxx
  • +
  • 全局限速和单个种子限速
  • +
  • Previewing of certain file types, build in (video and audio)
  • +
  • معاينة مضمّنة لبعض أنواع الملفات (المرئية والمسموعة)
  • +
  • Vista prèvia de certs tipus de fitxer, integrada en construir (vídeo i àudio)
  • +
  • Vista prèvia de certs tipus de fitxer, integrada en construir (vídeo i àudio)
  • +
  • ForhÃ¥ndsvisning af visse filtyper indbygget (video og lyd)
  • +
  • Eingebaute Vorschau für bestimmte Dateitypen (Video und Audio)
  • +
  • Προεπισκόπηση συγκεκριμένων τύπων αρχείων, ενσωματωμένη (με βίντεο και ήχο)
  • +
  • Previewing of certain file types, build in (video and audio)
  • +
  • Vista previa de ciertos tipos de archivo, vídeo y audio incorporados
  • +
  • Teatavate failitüüpide eelvaatlus (video ja audio)
  • +
  • Joidenkin (ääni- ja video-) tiedostotyyppien sisään rakennettu esikatselu
  • +
  • Aperçu en interne de certains types de fichiers (vidéo et audio)
  • +
  • Previsualización de certos tipos de ficheiros, incluída (vídeo e son).
  • +
  • Mempratinjau tipe-tipe file tertentu, yang dibawakannya ( video dan audio)
  • +
  • Anteprima integrata per alcuni tipi di file (video e audio)
  • +
  • 비디오 및 오디오 등 일부 파일 형식 미리 보기
  • +
  • Voorbeelden van bepaalde bestandstypen bekijken, ingebouwd (video en geluid)
  • +
  • Podgląd pewnych rodzajów plików, wbudowany odtwarzacz filmów i dźwięku
  • +
  • Antevisão incorporada de alguns tipos de ficheiros (vídeo e áudio)
  • +
  • Pré-visualização embutida de certos tipos de arquivos (áudio e vídeo)
  • +
  • Предварительный просмотр файлов некоторых типов (видео, звук)
  • +
  • Náhľady určitých typov súborov (video a audio)
  • +
  • Vgrajen predogled določenih zvrsti datotek (video in avdio)
  • +
  • Преглед појединих типова фајлова, уграђено (видео и аудио).
  • +
  • Pregled pojedinih tipova fajlova, ugrađeno (video i audio).
  • +
  • Преглед појединих типова фајлова, уграђено (видео и аудио).
  • +
  • Pregled pojedinih tipova fajlova, ugrađeno (video i audio).
  • +
  • Förhandsgranskning av vissa filtyper inbyggd (video och ljud)
  • +
  • Belli dosya tipleri için ön izleme, gömülü (video ve ses)
  • +
  • Перегляд даних файлів деяких типів, вбудований (відео і звукові дані)
  • +
  • xxPreviewing of certain file types, build in (video and audio)xx
  • +
  • 内置的文件预览功能 (视频和音频)
  • +
  • Importing of partially or fully downloaded files
  • +
  • استيراد الملفات المنزّلة جزئيا أو كليا
  • +
  • Importació de fitxers descarregats parcialment o del tot
  • +
  • Importació de fitxers descarregats parcialment o del tot
  • +
  • Import af delvist eller helt downloadede filer
  • +
  • Teilweise oder vollständig heruntergeladene Dateien werden importiert
  • +
  • Εισαγωγή αποσπασματικώς ή πλήρως ληφθέντων αρχείων
  • +
  • Importing of partially or fully downloaded files
  • +
  • Importación de archivos descargados parcial o totalmente
  • +
  • Osaliselt või täielikult alla laaditud failide import
  • +
  • Osittain tai täysin ladattujen tiedostojen tuonti
  • +
  • Import de fichiers partiellement ou totalement téléchargés
  • +
  • Importación de ficheiros descargados total ou parcialmente.
  • +
  • Mengimpor file yang diunduh secara penuh atau secara parsial
  • +
  • Importazione di file parzialmente o totalmente scaricati
  • +
  • 일부분 및 완전히 다운로드된 파일 가져오기
  • +
  • Gedeeltelijk of volledig gedownloade bestanden importeren
  • +
  • Impot częściowo lub w pełni pobranych plików
  • +
  • Importação de ficheiros transferidos parcialmente ou por completo
  • +
  • Importação de arquivos baixados completamente ou parcialmente
  • +
  • Импорт частично или полностью загруженных файлов
  • +
  • Importovanie čiastočne alebo plne stiahnutých súborov
  • +
  • Uvoz delno ali v celoti prenesenih datotek
  • +
  • Увоз делимично или потпуно преузетих фајлова.
  • +
  • Uvoz delimično ili potpuno preuzetih fajlova.
  • +
  • Увоз делимично или потпуно преузетих фајлова.
  • +
  • Uvoz delimično ili potpuno preuzetih fajlova.
  • +
  • Import av partiellt eller fullständigt nerladdade filer
  • +
  • Kısmi veya tam indirilmiş dosyaları içe aktarma
  • +
  • Імпортування частково або повністю звантажених файлів
  • +
  • xxImporting of partially or fully downloaded filesxx
  • +
  • 导入部分或完全下载的文件
  • +
  • File prioritization for multi-file torrents
  • +
  • تحديد أولويات الملفات للسيول التي فيها عدة ملفات
  • +
  • Priorització de fitxers per als torrent amb múltiples fitxers
  • +
  • Priorització de fitxers per als torrent amb múltiples fitxers
  • +
  • Filprioritering for torrents med flere filer
  • +
  • Dateipriorisierung für Torrents mit mehreren Dateien
  • +
  • Παραχώρηση προτεραιότητας σε αρχείο σε torrent με πολλά αρχεία
  • +
  • File prioritisation for multi-file torrents
  • +
  • Priorización de archivos para torrents multiarchivo
  • +
  • Paljude failidega torrentide failide seadmine tähtsuse järjekorda
  • +
  • Useampitiedostoisten torrenttien eri tiedostojen ensisijaistus
  • +
  • Ordre de priorité des fichiers et pour les torrents multi-fichiers
  • +
  • Priorización de ficheiros para os torrents con varios ficheiros.
  • +
  • Pemprioritasan file untuk torrent multi file
  • +
  • Priorità dei file per i torrent multi-file
  • +
  • 파일이 여러 개 있는 토렌트 내부 우선 순위 부여
  • +
  • Bestandsprioriteit instellen multi-bestand-torrents
  • +
  • Priorytety dla wieloplikowych torrentów
  • +
  • Prioritização de torrentes multi-ficheiros
  • +
  • Priorização de arquivos para torrents multi-arquivos
  • +
  • Приоритеты файлов для торрентов с несколькими файлами
  • +
  • Prioritizácia súborov cez viacsúborové torrenty
  • +
  • Upravljanje s prednostmi za tokove več datotek
  • +
  • Задавање приоритета по фајлу у торентима са више фајлова.
  • +
  • Zadavanje prioriteta po fajlu u torentima sa viÅ¡e fajlova.
  • +
  • Задавање приоритета по фајлу у торентима са више фајлова.
  • +
  • Zadavanje prioriteta po fajlu u torentima sa viÅ¡e fajlova.
  • +
  • Filprioritering för dataflöden med flera filer
  • +
  • Birden çok dosya barındıran torrentler için dosya önceliklendirme
  • +
  • Визначення пріоритетів файлів у торентах, які складаються з декількох файлів
  • +
  • xxFile prioritization for multi-file torrentsxx
  • +
  • 多文件种子设定文件优先级
  • +
  • Selective downloading for multi-file torrents
  • +
  • Descàrrega selectiva per als torrent amb múltiples fitxers
  • +
  • Descàrrega selectiva per als torrent amb múltiples fitxers
  • +
  • Selektiv download ved torrents med flere filer
  • +
  • In Torrents mit mehreren Dateien können einzelne Dateien zum Herunterladen ausgewählt werden.
  • +
  • Επιλεκτική λήψη για torrent με πολλά αρχεία
  • +
  • Selective downloading for multi-file torrents
  • +
  • Descarga selectiva de torrents multiarchivo
  • +
  • Paljude failidega torrentide valikuline allalaadimine
  • +
  • Useampitiedostoisten torrenttien valikoiva lataus
  • +
  • Téléchargement sélectif pour les torrents multi-fichiers
  • +
  • Descarga selectiva para torrents con varios ficheiros.
  • +
  • Pengunduhan selektif untuk torrent-torrent multi file
  • +
  • Scaricamento selettivo per i torrent multi-file
  • +
  • 파일이 여러 개 있는 토렌트의 일부만 다운로드
  • +
  • Selectief downloaden van multi-bestand-torrents
  • +
  • Wybiórcze pobieranie dla wieloplikowych torrentów
  • +
  • Transferência selectiva para torrentes multi-ficheiros
  • +
  • Download seletivo para torrents multi-arquivo
  • +
  • Выборочная загрузка файлов из торрентов с несколькими файлами
  • +
  • Selektívne sÅ¥ahovanie pre viacsúborové torrenty
  • +
  • Selektivni prenos za tokove več datotek
  • +
  • Селективно преузимање у торентима са више фајлова.
  • +
  • Selektivno preuzimanje u torentima sa viÅ¡e fajlova.
  • +
  • Селективно преузимање у торентима са више фајлова.
  • +
  • Selektivno preuzimanje u torentima sa viÅ¡e fajlova.
  • +
  • Selektiv nerladdning för dataflöden med flera filer
  • +
  • Birden çok dosya barındıran torrentler için seçimli indirme
  • +
  • Вибіркове отримання даних торентів, які складаються з декількох файлів
  • +
  • xxSelective downloading for multi-file torrentsxx
  • +
  • 多文件种子可选下载指定文件
  • +
  • Kick/ban peers with an additional IP Filter dialog for list/edit purposes
  • +
  • Expulsa/veta els parells amb un diàleg de filtre d'IP addicional per a finalitats de llista/edició
  • +
  • Expulsa/veta els parells amb un diàleg de filtre d'IP addicional per a finalitats de llista/edició
  • +
  • Afbryd/bandlys peers med en yderligere IP-filterdialog til liste- og redigeringsformÃ¥l
  • +
  • Gegenstellen zwangsweise trennen oder verbannen mit Hilfe eines zusätzlichen IP-Filterdialogs. Die IP-Einträge können in diesem Dialog angezeigt und bearbeitet werden.
  • +
  • Αποκλεισμός κόμβων με επιπλέον διάλογο IP φίλτρου για σκοπούς λίστας/επεξεργασίας
  • +
  • Kick/ban peers with an additional IP Filter dialogue for list/edit purposes
  • +
  • Expulsión/bloqueo de pares con diálogo de filtro IP adicional para propósitos de listar/editar
  • +
  • Masinate pagendamine/keelamine täiendavas IP filtrite dialoogis
  • +
  • Torju/estä käyttäjiä IP-lisäsuodatinikkunassa (luettelu- tai muokkaustarkoituksiin)
  • +
  • Exclusion/bannissement de pairs avec une fenêtre additionnelle pour lister/modifier les filtres d'IP
  • +
  • Expulsar e prohibir parceiros cun diálogo adicional de filtraxe por IP para listar e editar.
  • +
  • Tendang/cegah rekan (peer) dengan sebuah dialog Filter IP tambahan untuk tujuan daftar/edit
  • +
  • Caccia/metti al bando i nodi con un'ulteriore finestra di dialogo di filtro IP per elencarli e modificarli
  • +
  • IP 필터 대화 상자에서 피어 목록 관리/차단
  • +
  • Start/stop peers met een extra dialoog voor IP-filteren voor weergeven/bewerken
  • +
  • Wykopywanie/banowanie uczestników z dodatkowym filtrem IP do celów edycji i wyświetlania
  • +
  • Banir/expulsar as máquinas com uma janela adicional de filtros de IP's para fins de listagem/edição
  • +
  • Banir/expulsar as máquinas com uma janela adicional de filtros de IPs para fins de listagem/edição
  • +
  • Возможность отключения о блокирования пиров. Список заблокированных пиров можно редактировать в диалоге IP-фильтра.
  • +
  • Vykopnutie/ban klientov s dodatočným dialógom IP filtra pre dôvody zoznamu/úpravy
  • +
  • Preklop/prepoved vrstnikov z dodatnim pogovornim oknom filtra IP za namene urejanja/seznama
  • +
  • Избацивање/забрањивање вршњака преко дијалога за филтрирање ИП‑ова (листање и уређивање).
  • +
  • Izbacivanje/zabranjivanje vrÅ¡njaka preko dijaloga za filtriranje IP‑ova (listanje i uređivanje).
  • +
  • Избацивање/забрањивање вршњака преко дијалога за филтрирање ИП‑ова (листање и уређивање).
  • +
  • Izbacivanje/zabranjivanje vrÅ¡njaka preko dijaloga za filtriranje IP‑ova (listanje i uređivanje).
  • +
  • Sparka ut eller förbjud deltagare med en ytterligare IP-filtreringsdialogruta i listnings- och redigeringssyfte
  • +
  • Listeleme/Düzenleme amaçları için ilave bir IP Süzgeci penceresin ile eşleri atma/engelleme
  • +
  • Викидання/Блокування вузлів за допомогою додаткового діалогового вікна фільтрування за IP (список і редагування записів)
  • +
  • xxKick/ban peers with an additional IP Filter dialog for list/edit purposesxx
  • +
  • 踢掉/屏蔽对等点,并可以用 IP 过滤器查看和编辑列表
  • +
  • UDP tracker support
  • +
  • Suport per al seguidor UDP
  • +
  • Suport per al seguidor UDP
  • +
  • Podpora trackerů UDP
  • +
  • Understøttelse af UDP-tracker
  • +
  • Unterstützung für UDP-Tracker
  • +
  • Υποστήριξη ανιχνευτή UDP
  • +
  • UDP tracker support
  • +
  • Uso de rastreador UDP
  • +
  • UDP tracker'i toetus
  • +
  • UDP-seurantapalvelintuki
  • +
  • Gestion des traqueurs « UDP »
  • +
  • Compatibilidade con localizadores UDP.
  • +
  • Supporto de traciator UDP
  • +
  • Mendukung UDP tracker
  • +
  • Supporto ai server traccia UDP
  • +
  • UDP 트래커 지원
  • +
  • Ondersteuning van UDP-tracker
  • +
  • Obsługa serwera śledzącego UDP
  • +
  • Suporte a localizadores por UDP
  • +
  • Suporte a rastreadores UDP
  • +
  • Поддержка трекеров UDP
  • +
  • Podpora trackerov UDP
  • +
  • Podpora za sledilnik UDP
  • +
  • Подршка за УДП пратиоце.
  • +
  • PodrÅ¡ka za UDP pratioce.
  • +
  • Подршка за УДП пратиоце.
  • +
  • PodrÅ¡ka za UDP pratioce.
  • +
  • Stöd för UDP-spÃ¥rare
  • +
  • UDP izleme desteği
  • +
  • Підтримка координування UDP
  • +
  • xxUDP tracker supportxx
  • +
  • 支持 UDP 追踪器
  • +
  • 支援 UDP tracker
  • +
  • Support for private trackers and torrents
  • +
  • Suport per als seguidors i torrent privats
  • +
  • Suport per als seguidors i torrent privats
  • +
  • Podpora pro soukromé trackery a torrenty
  • +
  • Understøtter private trackere og torrents
  • +
  • Unterstützung für private Tracker und Torrents
  • +
  • Υποστήριξη για ιδιωτικούς ανιχνευτές και torrent
  • +
  • Support for private trackers and torrents
  • +
  • Implementación de rastreadores y torrents privados
  • +
  • Privaatsete tracker'ite ja torrentide toetus
  • +
  • Yksityisten seurantapalvelinten ja torrenttien tuki
  • +
  • Gestion des torrents et traqueurs privés
  • +
  • Compatibilidade con localizadores e torrents privados.
  • +
  • Dukungan untuk torrent dan tracker privat
  • +
  • Supporto per server traccia e torrent privati
  • +
  • 비밀 트래커 및 토렌트 지원
  • +
  • Ondersteuning voor privé-trackers en -torrents
  • +
  • Obsługa prywatnych serwerów śledzących i torrentów
  • +
  • Suporte para localizadores e torrentes privadas
  • +
  • Suporte para rastreadores e torrents privados
  • +
  • Поддержка частных трекеров и торрентов
  • +
  • Podpora pre súkromné trackery a torrenty
  • +
  • Podpora zasebnim sledilcem in tokovom
  • +
  • Подршка за приватне пратиоце и торенте.
  • +
  • PodrÅ¡ka za privatne pratioce i torente.
  • +
  • Подршка за приватне пратиоце и торенте.
  • +
  • PodrÅ¡ka za privatne pratioce i torente.
  • +
  • Stöd för privata spÃ¥rare och dataflöden
  • +
  • Özel izleyici ve torrent desteği
  • +
  • Підтримка приватних координаторів та торентів
  • +
  • xxSupport for private trackers and torrentsxx
  • +
  • 支持私人追踪器和种子
  • +
  • 支援私密 tracker 和種子檔
  • +
  • Support for µTorrent's peer exchange
  • +
  • Suport per a l'intercanvi entre parells de µTorrent
  • +
  • Suport per a l'intercanvi entre parells de µTorrent
  • +
  • Podpora výměny protějÅ¡ků µTorrent
  • +
  • Understøttelse af µTorrent's peer-udveksling
  • +
  • Unterstützung für Austausch von Gegenstellen bei µTorrents
  • +
  • Υποστήριξη για ανταλλαγή µTorrent κόμβων
  • +
  • Support for µTorrent's peer exchange
  • +
  • Implementación de intercambio de pares de µTorrent
  • +
  • µTorrentiga ühilduva masinate suhtlemise toetus
  • +
  • µTorrentin käyttäjävaihdon tuki
  • +
  • Gestion de l'échange de pairs de µTorrent.
  • +
  • Compatibilidade co intercambio de parceiros de µTorrent.
  • +
  • Dukungan untuk pertukaran para rekan µTorrent
  • +
  • Supporto per lo scambio di nodi con µTorrent
  • +
  • µTorrent 피어 교환 지원
  • +
  • Ondersteuning voor uitwisseling van peers van µTorrent
  • +
  • Obsługa wymiany uczestników µTorrenta
  • +
  • Suporte para a troca de máquinas do µTorrent
  • +
  • Suporte a troca de pares do µTorrent
  • +
  • Поддержка протокола обмена списками участников, используемого µTorrent
  • +
  • Podpora pre výmenu peerov µTorrent
  • +
  • Podpora za medsebojno izmenjavo µTorrent
  • +
  • Подршка за Микроторентову размену вршњака.
  • +
  • PodrÅ¡ka za µTorrentovu razmenu vrÅ¡njaka.
  • +
  • Подршка за Микроторентову размену вршњака.
  • +
  • PodrÅ¡ka za µTorrentovu razmenu vrÅ¡njaka.
  • +
  • Stöd för µTorrents utbyte mellan deltagare
  • +
  • µTorrent eş değişimi desteği
  • +
  • Підтримка обміну вузлами у форматі µTorrent
  • +
  • xxSupport for µTorrent's peer exchangexx
  • +
  • 支持 µTorrent 对等点交换
  • +
  • Support for protocol encryption (compatible with Azureus)
  • +
  • Suport per a l'encriptatge del protocol (compatible amb Azureus)
  • +
  • Suport per a l'encriptatge del protocol (compatible amb Azureus)
  • +
  • Podpora pro Å¡ifrování protokolu (kompatibilní s Azureus)
  • +
  • Understøttelse af protokolkryptering (kompatibel med Azureus)
  • +
  • Unterstützung für Protokollverschlüsselung (kompatibel mit Azureus)
  • +
  • Υποστήριξη για κρυπτογράφηση πρωτοκόλλου (συμβατό με Azureus)
  • +
  • Support for protocol encryption (compatible with Azureus)
  • +
  • Implementación de cifrado de protocolo (compatible con Azureus)
  • +
  • Protokolli krüptimise toetus (ühilduv Azureusega)
  • +
  • Yhteyskäytännön salaustuki (Azureuksen kanssa yhteensopiva)
  • +
  • Gestion du chiffrement du protocole (compatible avec Azureus)
  • +
  • Funcionalidade de cifrado do protocolo (compatíbel con Azureus).
  • +
  • Dukungan untuk enkripsi protokol (kompatibel dengan Azureus)
  • +
  • Supporto per la cifratura del protocollo (compatibile con Azureus)
  • +
  • 프로토콜 암호화 지원(Azureus 호환)
  • +
  • Ondersteuning voor protocolversleuteling (compatibel met Azureus)
  • +
  • Obsługa szyfrowania protokołu (zgodna z Azureus)
  • +
  • Suporte para a encriptação do protocolo (compatível com o Azureus)
  • +
  • Suporte para criptografia de protocolo (compatível com Azureus)
  • +
  • Поддержка шифрования на уровне протокола (совместимо с Azureus)
  • +
  • Podpora pre Å¡ifrovanie protokolu (kompatibilné s Azureus)
  • +
  • Podpora za protokol Å¡ifriranja (združljivo z Azureusom)
  • +
  • Подршка за шифровање протокола (сагласна са Азуреусом).
  • +
  • PodrÅ¡ka za Å¡ifrovanje protokola (saglasna sa Azureusom).
  • +
  • Подршка за шифровање протокола (сагласна са Азуреусом).
  • +
  • PodrÅ¡ka za Å¡ifrovanje protokola (saglasna sa Azureusom).
  • +
  • Stöd för protokollkryptering (kompatibel med Azureus)
  • +
  • Protok şifreleme desteği (Azureus ile uyumlu)
  • +
  • Підтримка шифрування даних протоколів (сумісна з Azureus)
  • +
  • xxSupport for protocol encryption (compatible with Azureus)xx
  • +
  • 支持协议加密 (兼容 Azureus)
  • +
  • Support for creating trackerless torrents
  • +
  • Suport per a la creació de torrent sense seguidors
  • +
  • Suport per a la creació de torrent sense seguidors
  • +
  • Podpora pro vytvoření torrentů bez trackerů
  • +
  • Understøttelse af oprettelse af trackerløse torrents
  • +
  • Unterstützung für das Erstellen von Torrents ohne Tracker
  • +
  • Υποστήριξη για δημιουργία torrent χωρίς ανιχνευτές
  • +
  • Support for creating trackerless torrents
  • +
  • Admite la creación de torrents sin rastreadores
  • +
  • Tracker'ivabade torrentide loomise toetus
  • +
  • Seurantapalvelimettomien torrenttien luontituki
  • +
  • Gestion de la création de torrents sans traqueur
  • +
  • Funcionalidade de creación de torrents sen localizadores.
  • +
  • Dukungan untuk menciptakan ke-tracker-an torrent
  • +
  • Supporto per la creazione di torrent senza server traccia
  • +
  • 트래커 없는 토렌트 생성 지원
  • +
  • Ondersteuning voor aanmaken van torrents zonder tracker
  • +
  • Obsługa tworzenia torrentów bez serwera śledzącego
  • +
  • Suporte para a criação de torrentes sem localizador
  • +
  • Suporte a criação de torrents sem rastreadores
  • +
  • Поддержка создания торрентов, не привязанных к торрент-трекеру.
  • +
  • Podpora pre vytváranie beztracerových torrentov
  • +
  • Podpora za ustvarjanje tokov brez sledi
  • +
  • Подршка за стварање торената без пратилаца.
  • +
  • PodrÅ¡ka za stvaranje torenata bez pratilaca.
  • +
  • Подршка за стварање торената без пратилаца.
  • +
  • PodrÅ¡ka za stvaranje torenata bez pratilaca.
  • +
  • Stöd för att skapa dataflöden utan spÃ¥rare
  • +
  • İzleyicisiz torrentler için destek
  • +
  • Підтримка створення торентів без координації
  • +
  • xxSupport for creating trackerless torrentsxx
  • +
  • 支持创建无追踪器种子
  • +
  • Support for distributed hash tables (DHT, the Mainline version)
  • +
  • Suport per a taules de resum distribuïdes (DHT, la versió de la línia principal)
  • +
  • Suport per a taules de resum distribuïdes (DHT, la versió de la línia principal)
  • +
  • Podpora pro distribuované tabulky hash (DHT, hlavní verze)
  • +
  • Understøttelse af distribuerede hast-tabeller (DHT, mainline-versionen)
  • +
  • Unterstützung für Verteilte Hashtabellen (Distributed Hash Table, DHT in der Mainline-Version)
  • +
  • Υποστήιριξη για κατανεμημένους πίνακες κατακερματισμού (DHT, η κύρια έκδοση)
  • +
  • Support for distributed hash tables (DHT, the Mainline version)
  • +
  • Implementación de tablas de resumen distribuidas (DHT, la versión principal)
  • +
  • Jagatavate räsitabelite toetus (DHT, Mainline versioon)
  • +
  • Hajautettujen tiivistetaulukoiden tuki (DHT, Mainline-versio)
  • +
  • Gestion des tables de hachage distribuées (DHT, la version principale)
  • +
  • Compatibilidade con táboas de sumas distribuídas (DHT, a versión principal).
  • +
  • Dukungan untuk tabel-tabel hash yang distribusikan (DHT, versi Mainline)
  • +
  • Supporto per le tabelle di hash distribuite (DHT, la versione principale)
  • +
  • 분산 해시 테이블(DHT, 메인라인 버전) 지원
  • +
  • Ondersteuning voor gedistribueerde hash-tabellen (DHT, het hoofdversie)
  • +
  • Obsługa rozproszonych tablic haszów (DHT, wersja głównoliniowa)
  • +
  • Suporte para tabelas de dispersão distribuídas (DHT)
  • +
  • Suporte para tabelas de dispersão distribuídas (DHT)
  • +
  • Поддержка распределённых хеш-таблиц (DHT).
  • +
  • Podpora pre distribuované haÅ¡ovacie tabuľky (DHT, mainline verzia)
  • +
  • Podpora za porazdeljene razprÅ¡ene tabele (DHT, verzija glavne linije)
  • +
  • Подршка за дистрибуиране дисперзионе табеле (мејнлајн ДХТ).
  • +
  • PodrÅ¡ka za distribuirane disperzione tabele (Mainline DHT).
  • +
  • Подршка за дистрибуиране дисперзионе табеле (мејнлајн ДХТ).
  • +
  • PodrÅ¡ka za distribuirane disperzione tabele (Mainline DHT).
  • +
  • Stöd för distribuerade kondensattabeller (DHT, huvudversionen)
  • +
  • Dağıtık özet tabloları için destek (DHT, Ana sürümü)
  • +
  • Підтримка розподілених таблиць хешів (distributed hash tables або DHT, основної версії)
  • +
  • xxSupport for distributed hash tables (DHT, the Mainline version)xx
  • +
  • 支持分布式哈希表 (DHT,主线版本)
  • +
  • Support for UPnP to automatically forward ports on a LAN with dynamic assigned hosts
  • +
  • Suport per a UPnP per a desviar automàticament els ports de la LAN amb assignació dinàmica de màquines
  • +
  • Suport per a UPnP per a desviar automàticament els ports de la LAN amb assignació dinàmica de màquines
  • +
  • Podpora automatické přeposílání portů na LAN s dynamicky přidělovanými hostiteli v UPnP
  • +
  • Understøttelse af UPnP til automatisk at Ã¥bne porte pÃ¥ et LAN med dynamisk tildelte værter
  • +
  • Unterstützung für UPnP zur automatischen Port-Weiterleitung in Netzwerken mit dynamisch zugewiesenen Rechnern
  • +
  • Υποστήριξη UPnP για αυτόματη προώθηση θυρών σε τοπικό δίκτυο με δυναμική αντιστοίχιση υπολογιστών
  • +
  • Support for UPnP to automatically forward ports on a LAN with dynamic assigned hosts
  • +
  • Implementación de UPnP para redireccionar puertos automáticamente en una red local con servidores asignados dinámicamente
  • +
  • UPnP toetus portide automaatseks edastamiseks dünaamiliselt omistatud masinatega kohtvõrgus
  • +
  • UPnP-tuki LAN-porttien automaattiohjaukseen dynaamisesti verkkoon yhdistetyillä koneilla
  • +
  • Gestion du protocole UPnP pour rediriger automatiquement des ports sur un réseau dont les hôtes sont gérés dynamiquement
  • +
  • Compatibilidade con UPnP para redirixir automaticamente portos na rede local con servidores asignados dinamicamente.
  • +
  • Dukungan buat UPnP untuk memportkan maju secara otomatis pada sebuah LAN dengan host tertandatangani yang dinamis
  • +
  • Supporto UPnP per mappare automaticamente le porte sulla LAN con host dinamici assegnati
  • +
  • UPnP를 통한 자동 포트 포워딩 지원
  • +
  • Ondersteuning voor UPnP om automatisch poorten te 'forwarden' op een LAN met dynamisch toegekende hosts
  • +
  • Obsługa UPnP do przekierowywania portów w LANie z dynamicznie przypisanymi gospodarzami
  • +
  • Suporte de UPnP para encaminhar automaticamente os portos numa LAN para as máquinas atribuídas de forma dinâmica
  • +
  • Suporte ao UPnP para encaminhar automaticamente portas em uma LAN com host atribuídos dinamicamente
  • +
  • Поддержка UPnP для автоматического перенаправления портов в локальной сети с динамическим выделением IP-адресов.
  • +
  • Podpora pre UPnP na automatické presmerovanie portov na LAN s dynaicky priradenými hostami
  • +
  • Podpora za UPnP za samodejno posredovanje vrat v omrežju LAN z dinamično dodeljenimi gostitelji
  • +
  • Подршка за аутоматско прослеђивање портова кроз УПнП у ЛАН‑у са динамички додељиваним домаћинима.
  • +
  • PodrÅ¡ka za automatsko prosleđivanje portova kroz UPnP u LAN‑u sa dinamički dodeljivanim domaćinima.
  • +
  • Подршка за аутоматско прослеђивање портова кроз УПнП у ЛАН‑у са динамички додељиваним домаћинима.
  • +
  • PodrÅ¡ka za automatsko prosleđivanje portova kroz UPnP u LAN‑u sa dinamički dodeljivanim domaćinima.
  • +
  • Stöd för UPnP för att automatiskt vidarebefordra portar pÃ¥ ett lokalt nätverk med dynamiskt tilldelade värddatorer
  • +
  • Dinamik olarak atanmış makineler ile LAN üzerinde otomatik port yönlendirme için UPnP desteği
  • +
  • Підтримка UPnP з метою автоматичного переспрямування портів у локальній мережі з динамічним призначенням назв вузлів
  • +
  • xxSupport for UPnP to automatically forward ports on a LAN with dynamic assigned hostsxx
  • +
  • 支持 UPnP 局域网自动转发端口的动态主机名
  • +
  • Support for webseeds
  • +
  • Suport per a sembradors web
  • +
  • Suport per a sembradors web
  • +
  • Podpora pro webové sdílení
  • +
  • Understøttelse af webseeds
  • +
  • Unterstützung für Webseeds
  • +
  • Υποστήριξη για πηγές ιστού
  • +
  • Support for webseeds
  • +
  • Implementación de semillas web
  • +
  • Veebilevituse toetus
  • +
  • Webseed-tuki
  • +
  • Gestion des sources web
  • +
  • Compatibilidade con sementes web.
  • +
  • Supporto per webseeds
  • +
  • Dukungan untuk webseeds
  • +
  • Supporto per distribuzione web
  • +
  • 웹 시드 지원
  • +
  • Ondersteuning voor webseeds
  • +
  • Obsługa rozsiewania w sieci
  • +
  • Suporte para fontes Web
  • +
  • Suporte para fontes Web
  • +
  • Поддержка веб-сидов
  • +
  • Podpora pre webové seedy
  • +
  • Podpora za spletne brste
  • +
  • Подршка за веб сејања.
  • +
  • PodrÅ¡ka za veb sejanja.
  • +
  • Подршка за веб сејања.
  • +
  • PodrÅ¡ka za veb sejanja.
  • +
  • Stöd för webberbjudanden
  • +
  • Web tohumları için destek
  • +
  • Підтримка вебпоширення
  • +
  • xxSupport for webseedsxx
  • +
  • 支持 Webseed
  • +
  • Scripting support via Kross, and interprocess control via D-Bus interface
  • +
  • Suport per a crear scripts mitjançant el Kross i control entre processos a través de la interfície de D-Bus
  • +
  • Suport per a crear scripts mitjançant el Kross i control entre processos a través de la interfície de D-Bus
  • +
  • Scripting-understøttelse via Kross og interproceskontrol via D-Bus-grænseflade
  • +
  • Skript-Unterstützung mit Kross und Interprozess-Steuerung über die D-Bus-Schnittstelle
  • +
  • Υποστήριξη συγγραφής σεναρίων με Kross και έλεγχος επικοινωνίας διεργασιών με D-Bus διεπαφή
  • +
  • Scripting support via Kross, and interprocess control via D-Bus interface
  • +
  • Admite el uso de guiones vía Kross y control de interprocesos vía la interfaz D-Bus
  • +
  • Skriptimise toetus Krossi kaudu ja protsesside juhtimine D-Busi liidese abil
  • +
  • Skriptaustuki Krossin kautta sekä prosessienvälinen hallinta D-Bus-liitännän kautta
  • +
  • Scriptable par Kross et contrôle inter-processus par l'interface D-Bus
  • +
  • Funcionalidade de scripting mediante Kross, e control entre procesos mediante unha interface de D-Bus.
  • +
  • Dukungan penskripan via Kross, dan kendali interproses via D-Bus interface
  • +
  • Supporto per lo scripting tramite Kross, e per il controllo dei processi tramite interfaccia D-Bus
  • +
  • Kross를 통한 스크립팅 및 D-Bus 인터페이스를 통한 원격 제어 지원
  • +
  • Ondersteuning voor scripts via Kross en interprocescontrole via het D-Bus-interface
  • +
  • Obsługa skryptów przez Kross i sterowania międzyprocesowego poprzez interfejs D-Bus
  • +
  • Suporte de programação através do Kross e controlo entre processos por uma interface de D-Bus
  • +
  • Suporte a scripts via Kross e controle de interprocessos via interface D-Bus
  • +
  • Поддержка сценариев при помощи Kross и внешнего управления через интерфейс D-Bus
  • +
  • Podpora skriptovania cez Kross a medziprocesové ovládanie cez rozhrani D-Bus
  • +
  • Podpora za skripte prek Kross-a in medprocesni nadzor prek vmesnika D-Bus
  • +
  • Подршка за скриптовање преко Кроса и међупроцесно управљање преко д‑бус сучеља.
  • +
  • PodrÅ¡ka za skriptovanje preko Krossa i međuprocesno upravljanje preko D‑Bus sučelja.
  • +
  • Подршка за скриптовање преко Кроса и међупроцесно управљање преко д‑бус сучеља.
  • +
  • PodrÅ¡ka za skriptovanje preko Krossa i međuprocesno upravljanje preko D‑Bus sučelja.
  • +
  • Skriptstöd via Kross och styrning mellan processer via D-Bus gränssnitt
  • +
  • Kross üzerinden betik yazma desteği ve D-Bus arayüzü ile süreçler arası denetim
  • +
  • Підтримка керування за допомогою скриптів на Kross та обміну даними між процесами за допомогою D-Bus
  • +
  • xxScripting support via Kross, and interprocess control via D-Bus interfacexx
  • +
  • 通过 Kross 支持脚本编程,通过 D-Bus 接口实现进程间控制
  • +
  • System tray integration
  • +
  • Integració amb la safata del sistema
  • +
  • Integració amb la safata del sistema
  • +
  • Integrace do systémové části panelu
  • +
  • Integration med statusomrÃ¥de
  • +
  • Integration in den Systemabschnitt der Kontrollleiste
  • +
  • Ενσωμάτωση στο πλαίσιο συστήματος
  • +
  • System tray integration
  • +
  • Integración con la bandeja del sistema
  • +
  • Lõimimine süsteemisalve
  • +
  • Ilmoitusalueintegrointi
  • +
  • Intégration dans la barre de notifications du système
  • +
  • Integración coa área de notificación.
  • +
  • Integration de tabuliero de systema
  • +
  • Integrasi system tray
  • +
  • Integrazione nel vassoio di sistema
  • +
  • 시스템 트레이 통합
  • +
  • Integratie in het systeemvak
  • +
  • Integracja z tacką systemową
  • +
  • Integração na bandeja do sistema
  • +
  • Integração na área de notificação
  • +
  • Значок в системном лотке
  • +
  • Integrácia do systémovej liÅ¡ty
  • +
  • Integracija sistemskega pulta
  • +
  • Уклапање у системску касету.
  • +
  • Uklapanje u sistemsku kasetu.
  • +
  • Уклапање у системску касету.
  • +
  • Uklapanje u sistemsku kasetu.
  • +
  • Integrering med systembricka
  • +
  • Sistem çekmecesi tümleştirmesi
  • +
  • Можливість роботи у системному лотку
  • +
  • xxSystem tray integrationxx
  • +
  • 系统托盘整合
  • +
  • Tracker authentication support
  • +
  • Suport per a l'autenticació del seguidor
  • +
  • Suport per a l'autenticació del seguidor
  • +
  • Understøttelse af tracker-autentificering
  • +
  • Unterstützung für Tracker-Authentifizierung
  • +
  • Υποστήριξη ταυτοποίησης ανιχνευτή
  • +
  • Tracker authentication support
  • +
  • Implementa autenticación del rastreador
  • +
  • Trakcer'ite autentimise toetus
  • +
  • Seurantapalvelimen todennustuki
  • +
  • Gestion de l'authentification des traqueurs
  • +
  • Funcionalidade de autenticación de localizadores.
  • +
  • Supporto de authentication de traciator
  • +
  • Dukungan autentikasi tracker
  • +
  • Supporto per l'autenticazione sul server traccia
  • +
  • 트래커 인증 지원
  • +
  • Authenticatieondersteuning voor tracker
  • +
  • Obsluga uwierzytelniania na serwerze śledzącym
  • +
  • Suporte para localizadores autenticados
  • +
  • Suporte a autenticação de rastreadores
  • +
  • Поддержка аутентификации на трекере.
  • +
  • Podpora autentifikácie trackerov
  • +
  • Podpora za preverjanje pristnosti sledilcev
  • +
  • Подршка за аутентификацију пратилаца.
  • +
  • PodrÅ¡ka za autentifikaciju pratilaca.
  • +
  • Подршка за аутентификацију пратилаца.
  • +
  • PodrÅ¡ka za autentifikaciju pratilaca.
  • +
  • Stöd för behörighetskontroll av spÃ¥rare
  • +
  • İzleyici kimlik doğrulama desteği
  • +
  • Підтримка розпізнавання для координації
  • +
  • xxTracker authentication supportxx
  • +
  • 追踪器认证支持
  • +
  • Connection through a proxy
  • +
  • Connexió a través d'un servidor intermediari
  • +
  • Connexió a través d'un servidor intermediari
  • +
  • Připojení skrze proxy
  • +
  • Forbindelse via proxy
  • +
  • Verbindung über Proxys
  • +
  • Σύνδεση μέσω διαμεσολαβητή
  • +
  • Connection through a proxy
  • +
  • Conexión a través de un proxy
  • +
  • Ühendus läbi puhverserveri
  • +
  • Välityspalvelinyhteysmahdollisuus
  • +
  • Connexion à travers un serveur mandataire
  • +
  • Conexión mediante proxy.
  • +
  • Connexion per un proxy
  • +
  • Koneksi melalui sebuah proxy
  • +
  • Connessione attraverso un proxy
  • +
  • 프록시를 통한 연결
  • +
  • Verbinding via een proxy
  • +
  • Połączenia przez serwer pośredniczący
  • +
  • Ligação através de um 'proxy'
  • +
  • Conexão através de proxy
  • +
  • Подключение через прокси-сервер
  • +
  • Pripojenie cez proxy
  • +
  • Povezava prek posrednika
  • +
  • Повезивање кроз прокси.
  • +
  • Povezivanje kroz proksi.
  • +
  • Повезивање кроз прокси.
  • +
  • Povezivanje kroz proksi.
  • +
  • Anslutning via en proxy
  • +
  • Vekil sunucu üzerinden bağlantı
  • +
  • Встановлення з’єднання крізь проксі-сервер
  • +
  • xxConnection through a proxyxx
  • +
  • 通过代理连接
  • +
+

In addition to the built-in functionalities, there are some plugins available for KTorrent.

+

Addicionalment a les funcionalitats integrades en construir, hi ha alguns connectors disponibles per al KTorrent.

+

Addicionalment a les funcionalitats integrades en construir, hi ha alguns connectors disponibles per al KTorrent.

+

Udover den indbyggede funktionalitet er der nogle plugins tilgængelige til KTorrent.

+

Zusätzlich zu den eingebauten Funktionen gibt es weitere Module für KTorrent.

+

Επιπλέον των ενσωματωμένων λειτουργιών, υπάρχουν κάποια πρόσθετα διαθέσιμα για το KTorrent.

+

In addition to the built-in functionalities, there are some plugins available for KTorrent.

+

Además de las funcionalidades incluidas, hay varios complementos disponibles para KTorrent.

+

Lisaks on KTorrentil ka mitmeid pluginaid.

+

Sisään rakennettujen toimintojen lisäksi KTorrentiin saa myös liitännäisiä.

+

En plus de ces fonctionnalités internes, des extensions sont disponibles pour KTorrent.

+

Ademais das funcionalidades incluídas, compatibilidade con complementos instalábeis desde KTorrent.

+

Selain fungsionalitas bawaannya, ada beberapa plugin yang tersedia.

+

In aggiunta alle funzionalità integrate, ci sono alcune estensioni disponibili per KTorrent.

+

내장 기능 외에도 KTorrent 플러그인을 사용할 수 있습니다.

+

Naast de ingebouwde functionaliteiten zijn er enige plug-ins beschikbaar voor KTorrent.

+

Dodatkowo do wbudowanych możliwości, dostępne są także wtyczki dla KTorrenta.

+

Para além das funcionalidades incorporadas, existem alguns 'plugins' disponíveis para o KTorrent.

+

Adicionalmente as funcionalidades embutidas, existem alguns plugins disponíveis para o KTorrent.

+

Okrem zabudovaných funkcií sú k dispozícii aj niektoré pluginy pre KTorrent.

+

Poleg vgrajenih funkcionalnosti je nekaj vtičnikov na voljo za KTorrent.

+

Поред уграђених функција, доступни су и разни прикључци за К‑торент.

+

Pored ugrađenih funkcija, dostupni su i razni priključci za KTorrent.

+

Поред уграђених функција, доступни су и разни прикључци за К‑торент.

+

Pored ugrađenih funkcija, dostupni su i razni priključci za KTorrent.

+

Förutom inbyggd funktionalitet, finns några insticksprogram tillgängliga för Ktorrent.

+

Yerleşik işlevlere ek olarak, KTorrent için kullanılabilen bazı eklentiler de bulunmaktadır.

+

Окрім вбудованих функціональних можливостей, у KTorrent передбачено розширення можливостей за допомогою додатків.

+

xxIn addition to the built-in functionalities, there are some plugins available for KTorrent.xx

+

除了一些内置的功能,KTorrent 还有一些插件可用。

+
+ https://bugs.kde.org/enter_bug.cgi?format=guided&product=ktorrent + https://docs.kde.org/?application=ktorrent + https://kde.org/applications/internet/ktorrent/ + + + https://cdn.kde.org/screenshots/ktorrent/ktorrent.png + + + KDE + + ktorrent + + + + + + + + +
diff --git a/ktorrent/org.kde.ktorrent.desktop b/ktorrent/org.kde.ktorrent.desktop new file mode 100755 index 0000000..a19440e --- /dev/null +++ b/ktorrent/org.kde.ktorrent.desktop @@ -0,0 +1,162 @@ +[Desktop Entry] +Name=KTorrent +Name[ar]=سيولك +Name[ast]=KTorrent +Name[be]=KTorrent +Name[bg]=KTorrent +Name[bs]=KTorent +Name[ca]=KTorrent +Name[ca@valencia]=KTorrent +Name[cs]=KTorrent +Name[da]=KTorrent +Name[de]=KTorrent +Name[el]=KTorrent +Name[en_GB]=KTorrent +Name[es]=KTorrent +Name[et]=KTorrent +Name[fi]=KTorrent +Name[fr]=KTorrent +Name[ga]=KTorrent +Name[gl]=KTorrent +Name[hi]=केटोरेंट +Name[hne]=केटोरेंट +Name[hr]=KTorrent +Name[hu]=KTorrent +Name[ia]=KTorrent +Name[is]=KTorrent +Name[it]=KTorrent +Name[ja]=KTorrent +Name[kk]=KTorrent +Name[km]=KTorrent +Name[ko]=KTorrent +Name[lt]=KTorrent +Name[lv]=KTorrent +Name[mr]=के-टोरंट +Name[ms]=KTorrent +Name[nb]=KTorrent +Name[nds]=KTorrent +Name[ne]=केडीई टोरेन्ट +Name[nl]=KTorrent +Name[nn]=KTorrent +Name[pl]=KTorrent +Name[pt]=KTorrent +Name[pt_BR]=KTorrent +Name[ro]=KTorrent +Name[ru]=KTorrent +Name[si]=KTorrent +Name[sk]=KTorrent +Name[sl]=KTorrent +Name[sq]=KTorrent +Name[sr]=К‑торент +Name[sr@ijekavian]=К‑торент +Name[sr@ijekavianlatin]=KTorrent +Name[sr@latin]=KTorrent +Name[sv]=Ktorrent +Name[th]=โปรแกรม KTorrent +Name[tr]=KTorrent +Name[ug]=KTorrent +Name[uk]=KTorrent +Name[x-test]=xxKTorrentxx +Name[zh_CN]=KTorrent +Name[zh_TW]=KTorrent +GenericName=BitTorrent Client +GenericName[ar]=عميل بِت‌تورنت +GenericName[be]=Кліент BitTorrent +GenericName[bg]=Клиент за торент +GenericName[bs]=BitTorent klijent +GenericName[ca]=Client de BitTorrent +GenericName[ca@valencia]=Client de BitTorrent +GenericName[cs]=BitTorrent klient +GenericName[da]=BitTorrent-klient +GenericName[de]=BitTorrent-Programm +GenericName[el]=Πελάτης BitTorrent +GenericName[en_GB]=BitTorrent Client +GenericName[es]=Cliente de BitTorrent +GenericName[et]=BitTorrenti klient +GenericName[fi]=BitTorrent-ohjelma +GenericName[fr]=Client BitTorrent +GenericName[ga]=Cliant BitTorrent +GenericName[gl]=Cliente de BitTorrent +GenericName[hi]=बिटटोरेंट क्लाएंट +GenericName[hne]=बिटटोरेंट क्लायंट +GenericName[hr]=Klijent za BitTorrent +GenericName[hu]=BitTorrent kliens +GenericName[ia]=Cliente de BitTorrent +GenericName[is]=BitTorrent biðlari +GenericName[it]=Client BitTorrent +GenericName[ja]=BitTorrent クライアント +GenericName[kk]=BitTorrent клиенті +GenericName[km]=ម៉ាស៊ីន​ភ្ញៀវ BitTorrent +GenericName[ko]=비트토렌트 클라이언트 +GenericName[lt]=BitTorrent klientas +GenericName[lv]=BitTorrent klients +GenericName[mr]=बिट-टोरंट ग्राहक +GenericName[ms]=Klien BitTorrent +GenericName[nb]=BitTorrent-klient +GenericName[nds]=BitTorrent-Client +GenericName[ne]=बीट टोरेन्ट क्लाइन्ट +GenericName[nl]=BitTorrent-cliënt +GenericName[nn]=BitTorrent-klient +GenericName[pl]=Klient BitTorrent +GenericName[pt]=Cliente de BitTorrent +GenericName[pt_BR]=Cliente BitTorrent +GenericName[ro]=Client BitTorrent +GenericName[ru]=Клиент BitTorrent +GenericName[si]=BitTorrent වැඩසටහන +GenericName[sk]=BitTorrent Klient +GenericName[sl]=Odjemalec za BitTorrent +GenericName[sq]=BitTorrent Klient +GenericName[sr]=Битторент клијент +GenericName[sr@ijekavian]=Битторент клијент +GenericName[sr@ijekavianlatin]=Bittorrent klijent +GenericName[sr@latin]=Bittorrent klijent +GenericName[sv]=BitTorrent-klient +GenericName[th]=โปรแกรมสำหรับ BitTorrent +GenericName[tr]=BitTorrent İstemcisi +GenericName[ug]=بىتتوررېنت خېرىدارى +GenericName[uk]=Клієнт BitTorrent +GenericName[x-test]=xxBitTorrent Clientxx +GenericName[zh_CN]=BitTorrent 客户端 +GenericName[zh_TW]=BitTorrent 用戶端程式 +Exec=ktorrent %U +Icon=ktorrent +Type=Application +X-DocPath=ktorrent/index.html +MimeType=application/x-bittorrent;application/x-torrent;x-scheme-handler/magnet; +X-DBUS-StartupType=Unique +X-DBUS-ServiceName=org.kde.ktorrent +Comment=A BitTorrent program by KDE +Comment[ca]=Un programa de BitTorrent, creat per la comunitat KDE +Comment[ca@valencia]=Un programa de BitTorrent, creat per la comunitat KDE +Comment[cs]=BitTorrent od KDE +Comment[da]=Et BitTorrent-program fra KDE +Comment[de]=Ein BitTorrent-Programm von KDE +Comment[el]=Μία εφαρμογή BitTorrent από το KDE +Comment[en_GB]=A BitTorrent program by KDE +Comment[es]=Un programa de BitTorrent de KDE +Comment[et]=KDE BitTorrenti rakendus +Comment[fi]=KDE:n BitTorrent-ohjelma +Comment[fr]=Un programme BitTorrent par KDE +Comment[gl]=Un programa BitTorrent de KDE +Comment[it]=Un programma BitTorrent di KDE +Comment[ko]=KDE의 비트토렌트 프로그램 +Comment[nl]=Een BitTorrent-programma door KDE +Comment[nn]=Eit BitTorrent-program frå KDE +Comment[pl]=Klient bittorrent w ramach KDE +Comment[pt]=Um programa de BitTorrent do KDE +Comment[pt_BR]=Um programa BitTorrent do KDE +Comment[ru]=Клиент BitTorrent, разработанный KDE +Comment[sk]=BitTorrent klient pre KDE +Comment[sl]=Program za BitTorrent s strani KDE +Comment[sr]=Битторент програм из КДЕ‑а +Comment[sr@ijekavian]=Битторент програм из КДЕ‑а +Comment[sr@ijekavianlatin]=Bittorrent program iz KDE‑a +Comment[sr@latin]=Bittorrent program iz KDE‑a +Comment[sv]=Ett BitTorrent-program av KDE +Comment[tr]=KDE tarafından BitTorrent uygulaması +Comment[uk]=Програма BitTorrent від KDE +Comment[x-test]=xxA BitTorrent program by KDExx +Comment[zh_CN]=KDE 的 BitTorrent 程序 +Comment[zh_TW]=來自 KDE 的 BitTorrent 程式 +Terminal=false +Categories=Qt;KDE;Network;FileTransfer;P2P; diff --git a/ktorrent/pref/advancedpref.cpp b/ktorrent/pref/advancedpref.cpp new file mode 100644 index 0000000..b51252c --- /dev/null +++ b/ktorrent/pref/advancedpref.cpp @@ -0,0 +1,47 @@ +/* + SPDX-FileCopyrightText: 2007 Joris Guisson + SPDX-FileCopyrightText: 2007 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include + +#include "advancedpref.h" +#include "settings.h" + +namespace kt +{ +AdvancedPref::AdvancedPref(QWidget *parent) + : PrefPageInterface(Settings::self(), i18n("Advanced"), QStringLiteral("preferences-other"), parent) +{ + setupUi(this); + connect(kcfg_diskPrealloc, &QGroupBox::toggled, this, &AdvancedPref::onDiskPreallocToggled); + connect(kcfg_requeueMagnets, &QCheckBox::toggled, kcfg_requeueMagnetsTime, &QSpinBox::setEnabled); +} + +AdvancedPref::~AdvancedPref() +{ +} + +void AdvancedPref::loadSettings() +{ + kcfg_fullDiskPrealloc->setEnabled(Settings::diskPrealloc()); + kcfg_numMagnetDownloadingSlots->setValue(Settings::numMagnetDownloadingSlots()); + kcfg_requeueMagnets->setChecked(Settings::requeueMagnets()); + kcfg_requeueMagnetsTime->setEnabled(Settings::requeueMagnets()); + kcfg_requeueMagnetsTime->setValue(Settings::requeueMagnetsTime()); + kcfg_trackerListUrl->setText(Settings::trackerListUrl()); +} + +void AdvancedPref::loadDefaults() +{ + loadSettings(); +} + +void AdvancedPref::onDiskPreallocToggled(bool on) +{ + kcfg_fullDiskPrealloc->setEnabled(on); +} +} + +// kate: indent-mode cstyle; indent-width 4; replace-tabs on; mixed-indent off; diff --git a/ktorrent/pref/advancedpref.h b/ktorrent/pref/advancedpref.h new file mode 100644 index 0000000..502407e --- /dev/null +++ b/ktorrent/pref/advancedpref.h @@ -0,0 +1,32 @@ +/* + SPDX-FileCopyrightText: 2007 Joris Guisson + SPDX-FileCopyrightText: 2007 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KT_ADVNACEDPREF_HH +#define KT_ADVNACEDPREF_HH + +#include "ui_advancedpref.h" +#include + +namespace kt +{ +class AdvancedPref : public PrefPageInterface, public Ui_AdvancedPref +{ + Q_OBJECT +public: + AdvancedPref(QWidget *parent); + ~AdvancedPref() override; + + void loadSettings() override; + void loadDefaults() override; + +public Q_SLOTS: + void onDiskPreallocToggled(bool on); +}; +} + +#endif + +// kate: indent-mode cstyle; indent-width 4; replace-tabs on; mixed-indent off; diff --git a/ktorrent/pref/advancedpref.ui b/ktorrent/pref/advancedpref.ui new file mode 100644 index 0000000..e48f4df --- /dev/null +++ b/ktorrent/pref/advancedpref.ui @@ -0,0 +1,331 @@ + + + AdvancedPref + + + + 0 + 0 + 593 + 476 + + + + + + + Whether or not diskspace should be reserved before starting to download a torrent. + + + Reserve disk space before starting a torrent + + + true + + + false + + + + + + Instead of doing a quick reservation, do a full reservation. This is slower than the quick way, but avoids fragmentation on the disk. + + + Fully reserve disk space (avoids fragmentation) + + + + + + + + + + Performance + + + + + + + + GUI update interval: + + + + + + + + 100 + 0 + + + + <p>The interval in milliseconds between GUI updates.</p> +<p><span style=" font-weight:600;">Note: </span>Increasing this will decrease CPU usage.</p> + + + ms + + + 500 + + + 5000 + + + 1000 + + + + + + + Network sleep interval: + + + + + + + <p>Amount of time the network threads will sleep when they are speed limited. This has absolutely no effect when there are no speed limits.</p> +<p><span style=" font-weight:600;">Note:</span> The lower this is the more CPU is used. Setting it high can lead to lower speeds in high bandwidth situations. </p> +<p>For example on an 100 Mbit LAN, if you set a limit of 3000 KiB/s, you might not actually reach this speed when this value is too high. Without a limit you can easily get above 3000 KiB/s on a LAN.</p> + + + ms + + + 1 + + + 250 + + + 50 + + + + + + + + + Qt::Horizontal + + + + 111 + 20 + + + + + + + + + + + Magnets Queue + + + + + + + + Number of downloading slots: + + + + + + + Maximum number of concurrent downloading magnets at the same time. + + + 1 + + + 100 + + + 5 + + + + + + + Whether or not the magnets that are not downloaded after a maximum period of time must be pushed back at the end of the queue. + + + Requeue magnets after: + + + + + + + Maximum time that a magnet can occupy a downloading slot before it will be pushed back in the queue. + + + min + + + 1 + + + 60 + + + 5 + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + Miscellaneous + + + + + + + + + + Preview size for audio files: + + + + + + + <p>The amount of data at the beginning and end of an audio file, which will be prioritized for previewing.</p> + + + KiB + + + 16 + + + 100000000 + + + + + + + Preview size for video files: + + + + + + + <p>The amount of data at the beginning and end of a video file, which will be prioritized for previewing.</p> + + + KiB + + + 16 + + + 100000000 + + + + + + + + + Qt::Horizontal + + + + 111 + 20 + + + + + + + + + + + + Tracker list URL: + + + + + + + URL of the tracker list used when adding a torrent by infohash +NOTE: Each tracker must be on its own line. Blank lines are ignored. + + + + + + + + + Resolve the hostname of each peer. This will result in the hostname of a peer being displayed instead of the IP address. + +It can be disabled if you do not like the additional network traffic it generates + + + Resolve hostnames of peers + + + + + + + + + + Qt::Vertical + + + + 575 + 20 + + + + + + + + + diff --git a/ktorrent/pref/btpref.cpp b/ktorrent/pref/btpref.cpp new file mode 100644 index 0000000..5f8d7c7 --- /dev/null +++ b/ktorrent/pref/btpref.cpp @@ -0,0 +1,30 @@ +/* + SPDX-FileCopyrightText: 2009 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include + +#include "btpref.h" +#include "settings.h" + +namespace kt +{ +BTPref::BTPref(QWidget *parent) + : PrefPageInterface(Settings::self(), i18n("BitTorrent"), QStringLiteral("application-x-bittorrent"), parent) +{ + setupUi(this); +} + +BTPref::~BTPref() +{ +} + +void BTPref::loadSettings() +{ + kcfg_allowUnencryptedConnections->setEnabled(Settings::useEncryption()); + kcfg_dhtPort->setEnabled(Settings::dhtSupport()); + kcfg_customIP->setEnabled(Settings::useCustomIP()); +} + +} diff --git a/ktorrent/pref/btpref.h b/ktorrent/pref/btpref.h new file mode 100644 index 0000000..d5615dd --- /dev/null +++ b/ktorrent/pref/btpref.h @@ -0,0 +1,25 @@ +/* + SPDX-FileCopyrightText: 2009 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KT_BTPREF_H +#define KT_BTPREF_H + +#include "ui_btpref.h" +#include + +namespace kt +{ +class BTPref : public PrefPageInterface, public Ui_BTPref +{ + Q_OBJECT +public: + BTPref(QWidget *parent); + ~BTPref() override; + + void loadSettings() override; +}; +} + +#endif // KT_BTPREF_H diff --git a/ktorrent/pref/btpref.ui b/ktorrent/pref/btpref.ui new file mode 100644 index 0000000..c8c44b7 --- /dev/null +++ b/ktorrent/pref/btpref.ui @@ -0,0 +1,224 @@ + + + BTPref + + + + 0 + 0 + 628 + 477 + + + + + + + Features + + + + + + DHT is a trackerless protocol to find peers sharing the same torrents as you do. + + + Use DHT to get additional peers + + + + + + + + + UDP port for DHT communications: + + + + + + + <p>UDP port to use for the DHT protocol.</p> +<p><span style=" font-weight:600;">Attention:</span> If you are behind a router, this port needs to be forwarded to accept incoming DHT requests. The UPnP plugin can do this for you.</p> + + + 1 + + + 65535 + + + 4444 + + + + + + + + + Whether or not to use µTorrent compatible peer exchange. + + + Use peer exchange + + + + + + + Enable or disable the use of webseeds when they are present in a torrent. + + + Use webseeds + + + + + + + When a torrent has finished downloading, do a full data check on the torrent. + + + Check data when download is finished + + + + + + + + + + Encryption + + + + + + <p>Protocol encryption is useful when your ISP is slowing down bittorrent connections. </p> +<p>The encryption will prevent your bittorrent traffic to be flagged as bittorrent traffic, and so the ISP will not slow it down.</p> + + + Use protocol encryption + + + + + + + Not all clients support encryption, and some people have encryption disabled. If you want to connect to those peers, you need to have this option enabled. + + + Allow unencrypted connections + + + + + + + + + + Tracker + + + + + + Instead of allowing the tracker to determine your IP address, tell the tracker which IP address to use. Use this when you are behind a proxy. + + + Send the tracker a custom IP address or hostname + + + + + + + + + Custom IP address or hostname: + + + + + + + Custom IP address or hostname to send to the tracker. Hostnames will be resolved at runtime and the resolved IP address will be sent to the tracker. + + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + kcfg_dhtSupport + toggled(bool) + kcfg_dhtPort + setEnabled(bool) + + + 355 + 51 + + + 370 + 75 + + + + + kcfg_useEncryption + toggled(bool) + kcfg_allowUnencryptedConnections + setEnabled(bool) + + + 126 + 188 + + + 129 + 215 + + + + + kcfg_useCustomIP + toggled(bool) + kcfg_customIP + setEnabled(bool) + + + 93 + 273 + + + 272 + 304 + + + + + diff --git a/ktorrent/pref/colorpref.cpp b/ktorrent/pref/colorpref.cpp new file mode 100644 index 0000000..491be35 --- /dev/null +++ b/ktorrent/pref/colorpref.cpp @@ -0,0 +1,60 @@ +/* + SPDX-FileCopyrightText: 2020 Alexander Trufanov + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include + +#include "colorpref.h" +#include "settings.h" + +namespace kt +{ +ColorPref::ColorPref(QWidget *parent) + : PrefPageInterface(Settings::self(), i18n("Colors"), QStringLiteral("preferences-desktop-color"), parent) +{ + setupUi(this); + + connect(btnResetColors, &QPushButton::clicked, [=]() { + // set default values for current pref page only + kcfg_okTorrentColor->setColor(QColor(40, 205, 40)); + kcfg_stalledTorrentColor->setColor(QColor(255, 174, 0)); + kcfg_errorTorrentColor->setColor(QColor(Qt::red)); + + kcfg_highlightTorrentNameByTrackerStatus->setChecked(true); + kcfg_okTrackerConnectionColor->setColor(QColor(40, 205, 40)); + kcfg_warningsTrackerConnectionColor->setColor(QColor(255, 80, 0)); + kcfg_timeoutTrackerConnectionColor->setColor(QColor(0, 170, 110)); + kcfg_noTrackerConnectionColor->setColor(QColor(Qt::red)); + + kcfg_goodShareRatioColor->setColor(QColor(40, 205, 40)); + kcfg_lowShareRatioColor->setColor(QColor(Qt::red)); + }); +} + +ColorPref::~ColorPref() +{ +} + +void ColorPref::loadSettings() +{ + kcfg_okTorrentColor->setColor(Settings::okTorrentColor()); + kcfg_stalledTorrentColor->setColor(Settings::stalledTorrentColor()); + kcfg_errorTorrentColor->setColor(Settings::errorTorrentColor()); + + kcfg_highlightTorrentNameByTrackerStatus->setChecked(Settings::highlightTorrentNameByTrackerStatus()); + kcfg_okTrackerConnectionColor->setColor(Settings::okTrackerConnectionColor()); + kcfg_warningsTrackerConnectionColor->setColor(Settings::warningsTrackerConnectionColor()); + kcfg_timeoutTrackerConnectionColor->setColor(Settings::timeoutTrackerConnectionColor()); + kcfg_noTrackerConnectionColor->setColor(Settings::noTrackerConnectionColor()); + + kcfg_goodShareRatioColor->setColor(Settings::goodShareRatioColor()); + kcfg_lowShareRatioColor->setColor(Settings::lowShareRatioColor()); +} + +void ColorPref::loadDefaults() +{ + loadSettings(); +} + +} diff --git a/ktorrent/pref/colorpref.h b/ktorrent/pref/colorpref.h new file mode 100644 index 0000000..726cd94 --- /dev/null +++ b/ktorrent/pref/colorpref.h @@ -0,0 +1,26 @@ +/* + SPDX-FileCopyrightText: 2020 Alexander Trufanov + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KT_COLORPREF_HH +#define KT_COLORPREF_HH + +#include "ui_colorpref.h" +#include + +namespace kt +{ +class ColorPref : public PrefPageInterface, public Ui_ColorPref +{ + Q_OBJECT +public: + ColorPref(QWidget *parent); + ~ColorPref() override; + + void loadSettings() override; + void loadDefaults() override; +}; +} + +#endif diff --git a/ktorrent/pref/colorpref.ui b/ktorrent/pref/colorpref.ui new file mode 100644 index 0000000..39c90f3 --- /dev/null +++ b/ktorrent/pref/colorpref.ui @@ -0,0 +1,211 @@ + + + ColorPref + + + + 0 + 0 + 593 + 620 + + + + + + + + + Color to use for torrents when they are stalled or checking data. + + + + + + + Color to use for torrents while seeding or downloading. + + + + + + + Torrent is running: + + + + + + + Torrent is stalled or checking data: + + + + + + + Color to use for torrents with error status. + + + + + + + Torrent error: + + + + + + + + + Additionally highlight torrents based on the status of their trackers (if any) + + + true + + + + + + Color to use when at least one tracker is connected and replied no errors or warnings. + + + + + + + Some of the trackers are reachable: + + + + + + + Color to use when the best attempt to connect to tracker results in warning. + + + + + + + None of the trackers are reachable: + + + + + + + Reachable trackers report warnings: + + + + + + + Color to use when all trackers reply the errors or not found. + + + + + + + Trackers are timing out: + + + + + + + Color to use when the best attempt to connect to tracker results in timeout. Connection will be attempted again later. + + + + + + + + + + + + Low share ratio: + + + + + + + Good share ratio: + + + + + + + Color to display share ratio below minimal good value. + + + + + + + Color to display share ratio above minimal good value. + + + + + + + + + + + Reset colors to their default values (other settings won't change). + + + Reset Colors + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Qt::Vertical + + + + 575 + 20 + + + + + + + + + KColorButton + QPushButton +
kcolorbutton.h
+
+
+ + +
diff --git a/ktorrent/pref/generalpref.cpp b/ktorrent/pref/generalpref.cpp new file mode 100644 index 0000000..f79b79f --- /dev/null +++ b/ktorrent/pref/generalpref.cpp @@ -0,0 +1,69 @@ +/* + SPDX-FileCopyrightText: 2009 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "generalpref.h" +#include "settings.h" +#include + +namespace kt +{ +GeneralPref::GeneralPref(QWidget *parent) + : PrefPageInterface(Settings::self(), i18n("Application"), QStringLiteral("ktorrent"), parent) +{ + setupUi(this); + kcfg_tempDir->setMode(KFile::Directory | KFile::ExistingOnly | KFile::LocalOnly); + kcfg_saveDir->setMode(KFile::Directory | KFile::ExistingOnly | KFile::LocalOnly); + kcfg_torrentCopyDir->setMode(KFile::Directory | KFile::ExistingOnly | KFile::LocalOnly); + kcfg_completedDir->setMode(KFile::Directory | KFile::ExistingOnly | KFile::LocalOnly); +} + +GeneralPref::~GeneralPref() +{ +} + +void GeneralPref::loadSettings() +{ + kcfg_tempDir->setProperty("kcfg_property", QStringLiteral("text")); + kcfg_saveDir->setProperty("kcfg_property", QStringLiteral("text")); + kcfg_torrentCopyDir->setProperty("kcfg_property", QStringLiteral("text")); + kcfg_completedDir->setProperty("kcfg_property", QStringLiteral("text")); + + if (Settings::tempDir().isEmpty()) + kcfg_tempDir->setText(kt::DataDir()); + else + kcfg_tempDir->setText(Settings::tempDir()); + + kcfg_saveDir->setEnabled(Settings::useSaveDir()); + if (Settings::saveDir().isEmpty()) + kcfg_saveDir->setText(QDir::homePath()); + else + kcfg_saveDir->setText(Settings::saveDir()); + + kcfg_torrentCopyDir->setEnabled(Settings::useTorrentCopyDir()); + if (Settings::torrentCopyDir().isEmpty()) + kcfg_torrentCopyDir->setText(QDir::homePath()); + else + kcfg_torrentCopyDir->setText(Settings::torrentCopyDir()); + + kcfg_completedDir->setEnabled(Settings::useCompletedDir()); + if (Settings::completedDir().isEmpty()) + kcfg_completedDir->setText(QDir::homePath()); + else + kcfg_completedDir->setText(Settings::completedDir()); + + // kcfg_downloadBandwidth->setEnabled(Settings::showSpeedBarInTrayIcon()); + // kcfg_uploadBandwidth->setEnabled(Settings::showSpeedBarInTrayIcon()); +} + +void GeneralPref::loadDefaults() +{ + Settings::setTempDir(kt::DataDir()); + Settings::setSaveDir(QDir::homePath()); + Settings::setCompletedDir(QDir::homePath()); + Settings::setTorrentCopyDir(QDir::homePath()); + loadSettings(); +} + +} diff --git a/ktorrent/pref/generalpref.h b/ktorrent/pref/generalpref.h new file mode 100644 index 0000000..83074d3 --- /dev/null +++ b/ktorrent/pref/generalpref.h @@ -0,0 +1,26 @@ +/* + SPDX-FileCopyrightText: 2009 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KTGENERALPREF_H +#define KTGENERALPREF_H + +#include "ui_generalpref.h" +#include + +namespace kt +{ +class GeneralPref : public PrefPageInterface, public Ui_GeneralPref +{ + Q_OBJECT +public: + GeneralPref(QWidget *parent); + ~GeneralPref() override; + + void loadSettings() override; + void loadDefaults() override; +}; +} + +#endif diff --git a/ktorrent/pref/generalpref.ui b/ktorrent/pref/generalpref.ui new file mode 100644 index 0000000..5742190 --- /dev/null +++ b/ktorrent/pref/generalpref.ui @@ -0,0 +1,261 @@ + + + GeneralPref + + + + 0 + 0 + 427 + 423 + + + + + + + Folders + + + + + + Folder to store torrent information: + + + + + + + Directory to store information about all torrents currently opened in KTorrent. + + + + + + + Directory to use as a default save location for all data. + + + Default save location: + + + + + + + Directory to use as a default save location for all data. + + + + + + + Directory to move data to when a torrent has finished downloading. + + + Move completed downloads to: + + + + + + + Directory to move data to when a torrent has finished downloading. + + + + + + + Directory to copy all torrent files in, which are opened by KTorrent. + + + Copy torrent files to: + + + + + + + Directory to copy all torrent files in, which are opened by KTorrent. + + + + + + + + + + System Tray Icon + + + true + + + + + + Whether or not to always minimize to system tray on startup. + + + Always minimize to system tray on startup + + + + + + + Whether or not to show system tray popup messages. + + + Show system tray popup messages + + + + + + + + + + Miscellaneous + + + + + + When you select multiple torrents to be opened at the same time, open them silently. + + + Open multiple torrents silently + + + + + + + Open all torrents silently. + + + Open all torrents silently + + + + + + + Prevent computer from going into a sleep state when torrents are running. + + + Suppress sleep when torrents are running + + + + + + + <p>Rename a single file torrent with the name of its only file. </p> +<p>If for example the file is named <span style=" font-weight:600;">linux-desktop.iso</span>, the torrent will be displayed as <span style=" font-weight:600;">linux-desktop.</span></p> + + + Rename single file torrents with the name of its only file + + + + + + + Whether or not new torrents will be highlighted. + + + Highlight new torrents + + + + + + + Show total speed in the window title + + + + + + + + + + Qt::Vertical + + + + 382 + 20 + + + + + + + + + KUrlRequester + QFrame +
kurlrequester.h
+ 1 +
+
+ + + + kcfg_useCompletedDir + toggled(bool) + kcfg_completedDir + setEnabled(bool) + + + 160 + 100 + + + 318 + 100 + + + + + kcfg_useSaveDir + toggled(bool) + kcfg_saveDir + setEnabled(bool) + + + 160 + 72 + + + 318 + 72 + + + + + kcfg_useTorrentCopyDir + toggled(bool) + kcfg_torrentCopyDir + setEnabled(bool) + + + 160 + 128 + + + 318 + 128 + + + + +
diff --git a/ktorrent/pref/networkpref.cpp b/ktorrent/pref/networkpref.cpp new file mode 100644 index 0000000..65d2cf0 --- /dev/null +++ b/ktorrent/pref/networkpref.cpp @@ -0,0 +1,102 @@ +/* + SPDX-FileCopyrightText: 2007 Joris Guisson + SPDX-FileCopyrightText: 2007 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include + +#include +#include + +#include "networkpref.h" +#include "settings.h" +#include + +using namespace bt; + +namespace kt +{ +NetworkPref::NetworkPref(QWidget *parent) + : PrefPageInterface(Settings::self(), i18n("Network"), QStringLiteral("preferences-system-network"), parent) +{ + setupUi(this); + connect(m_recommended_settings, &QPushButton::clicked, this, &NetworkPref::calculateRecommendedSettings); + connect(kcfg_utpEnabled, &QCheckBox::toggled, this, &NetworkPref::utpEnabled); + connect(kcfg_onlyUseUtp, &QCheckBox::toggled, this, &NetworkPref::onlyUseUtpEnabled); +} + +NetworkPref::~NetworkPref() +{ +} + +void NetworkPref::loadSettings() +{ + kcfg_maxDownloadRate->setValue(Settings::maxDownloadRate()); + kcfg_maxUploadRate->setValue(Settings::maxUploadRate()); + kcfg_maxConnections->setValue(Settings::maxConnections()); + kcfg_maxTotalConnections->setValue(Settings::maxTotalConnections()); + + combo_networkInterface->clear(); + combo_networkInterface->addItem(QIcon::fromTheme(QStringLiteral("network-wired")), i18n("All interfaces")); + + kcfg_onlyUseUtp->setEnabled(Settings::utpEnabled()); + kcfg_primaryTransportProtocol->setEnabled(Settings::utpEnabled() && !Settings::onlyUseUtp()); + + // get all the network devices and add them to the combo box + const QList iface_list = QNetworkInterface::allInterfaces(); + + // FIXME KF5 const QList netlist = Solid::Device::listFromType(Solid::DeviceInterface::NetworkInterface); + + for (const QNetworkInterface &iface : iface_list) { + QIcon icon = QIcon::fromTheme(QStringLiteral("network-wired")); +#if 0 // FIXME KF5 + for (const Solid::Device& device : netlist) { + const Solid::NetworkInterface* netdev = device.as(); + if (netdev->ifaceName() == iface.name() && netdev->isWireless()) { + icon = QIcon::fromTheme(QStringLiteral("network-wireless")); + break; + } + + } +#endif + + combo_networkInterface->addItem(icon, iface.humanReadableName()); + } + const QString iface = Settings::networkInterface(); + int idx = (iface.isEmpty()) ? 0 /*all*/ : combo_networkInterface->findText(iface); + if (idx < 0) { + bool ok; + iface.toInt(&ok); + if (ok) { + idx = 0; + } else { + combo_networkInterface->addItem(iface); + idx = combo_networkInterface->findText(iface); + } + } + combo_networkInterface->setCurrentIndex(idx); +} + +void NetworkPref::updateSettings() +{ + QString iface = combo_networkInterface->currentText(); + Settings::setNetworkInterface(iface); +} + +void NetworkPref::loadDefaults() +{ +} + +void NetworkPref::utpEnabled(bool on) +{ + kcfg_onlyUseUtp->setEnabled(on); + kcfg_primaryTransportProtocol->setEnabled(on && !kcfg_onlyUseUtp->isChecked()); +} + +void NetworkPref::onlyUseUtpEnabled(bool on) +{ + kcfg_primaryTransportProtocol->setEnabled(!on && kcfg_utpEnabled->isChecked()); +} + +} diff --git a/ktorrent/pref/networkpref.h b/ktorrent/pref/networkpref.h new file mode 100644 index 0000000..54d30a7 --- /dev/null +++ b/ktorrent/pref/networkpref.h @@ -0,0 +1,38 @@ +/* + SPDX-FileCopyrightText: 2007 Joris Guisson + SPDX-FileCopyrightText: 2007 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KTNETWORKPREF_H +#define KTNETWORKPREF_H + +#include "ui_networkpref.h" +#include + +namespace kt +{ +/** + Preference page for network settings. +*/ +class NetworkPref : public PrefPageInterface, public Ui_NetworkPref +{ + Q_OBJECT +public: + NetworkPref(QWidget *parent); + ~NetworkPref() override; + + void loadSettings() override; + void loadDefaults() override; + void updateSettings() override; +Q_SIGNALS: + void calculateRecommendedSettings(); + +private Q_SLOTS: + void utpEnabled(bool on); + void onlyUseUtpEnabled(bool on); +}; + +} + +#endif diff --git a/ktorrent/pref/networkpref.ui b/ktorrent/pref/networkpref.ui new file mode 100644 index 0000000..38b25a3 --- /dev/null +++ b/ktorrent/pref/networkpref.ui @@ -0,0 +1,337 @@ + + + NetworkPref + + + + 0 + 0 + 501 + 495 + + + + + + + Ports && Limits + + + + + + Port: + + + + + + + <p>Port used for the bittorrent protocol.</p> +<p><span style=" font-weight:600;">Attention:</span> If you are behind a router, this port needs to be forwarded to accept incoming connections. The UPnP plugin can do this for you.</p> + + + 1 + + + 65535 + + + 3000 + + + + + + + UDP tracker port: + + + + + + + <p>Port used for the UDP tracker protocol.</p> +<p><span style=" font-weight:600;">Attention:</span> If you are behind a router, this port needs to be forwarded. The UPnP plugin can do this for you.</p> + + + 1 + + + 65536 + + + 4000 + + + + + + + Maximum connections per torrent: + + + + + + + The maximum number of connections allowed per torrent. + + + No limit + + + 99999 + + + + + + + Global connection limit: + + + + + + + The global connection limit for all torrents combined. + + + No limit + + + 99999 + + + + + + + Maximum download speed: + + + + + + + The maximum download speed in KiB/s. + + + No limit + + + KiB/s + + + 6500000 + + + + + + + Maximum upload speed: + + + + + + + The maximum upload speed in KiB/s + + + No limit + + + KiB/s + + + 6500000 + + + + + + + + + + Advanced + + + + + + + + DSCP value for IP packets: + + + + + + + This value will be filled in the DSCP field of all the IP packets sent for the bittorrent protocol. + + + 63 + + + + + + + Maximum number of connection setups: + + + + + + + <p>The maximum number of outgoing connections ktorrent will try to setup simultaneously.</p> +<p>If you are having trouble with ktorrent blocking other internet traffic, try setting this number a bit lower.</p> + + + 10 + + + 200 + + + 50 + + + + + + + Network interface: + + + + + + + + 0 + 0 + + + + <p>Which network interface to listen on for incoming connections.</p> +<p><span style="font-weight:600;">Note: </span>Requires a restart to take effect.</p> + + + + + + + + + The µTorrent transport protocol runs over UDP and is optimized to not interfere with other traffic, while still using up all unused bandwidth. + + + Use the µTorrent transport protocol (µTP) + + + + + + + Don't use TCP for bittorrent connections, only use the µTP protocol. This can lead to slower speeds, seeing that not all bittorrent clients support µTP. + + + Only use µTP + + + + + + + + + Primary transport protocol: + + + + + + + The primary transport protocol to use, when attempting to setup a connection with a peer, this protocol is chosen first. + + + + TCP + + + + + µTP + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Let KTorrent calculate the recommended settings based upon the bandwidth you have available. + + + Recommended Settings... + + + + + + + + + Qt::Vertical + + + + 493 + 41 + + + + + + + + + diff --git a/ktorrent/pref/prefdialog.cpp b/ktorrent/pref/prefdialog.cpp new file mode 100644 index 0000000..1eed94e --- /dev/null +++ b/ktorrent/pref/prefdialog.cpp @@ -0,0 +1,161 @@ +/* + SPDX-FileCopyrightText: 2007 Joris Guisson + SPDX-FileCopyrightText: 2007 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include +#include +#include + +#include "advancedpref.h" +#include "btpref.h" +#include "colorpref.h" +#include "core.h" +#include "generalpref.h" +#include "networkpref.h" +#include "prefdialog.h" +#include "proxypref.h" +#include "qmpref.h" +#include "recommendedsettingsdlg.h" +#include "settings.h" + +namespace kt +{ +PrefDialog::PrefDialog(QWidget *parent, Core *core) + : KConfigDialog(parent, QStringLiteral("settings"), Settings::self()) +{ + KConfigDialogManager::propertyMap()->insert(QStringLiteral("KUrlRequester"), QByteArrayLiteral("url")); + setFaceType(KPageDialog::List); + connect(this, &PrefDialog::settingsChanged, [core](const QString &) { + core->applySettings(); + }); + addPrefPage(new GeneralPref(this)); + net_pref = new NetworkPref(this); + addPrefPage(net_pref); + addPrefPage(new ProxyPref(this)); + addPrefPage(new BTPref(this)); + qm_pref = new QMPref(this); + addPrefPage(qm_pref); + addPrefPage(new ColorPref(this)); + addPrefPage(new AdvancedPref(this)); + + connect(net_pref, &NetworkPref::calculateRecommendedSettings, this, &PrefDialog::calculateRecommendedSettings); +} + +PrefDialog::~PrefDialog() +{ +} + +void PrefDialog::addPrefPage(PrefPageInterface *page) +{ + PrefPageScrollArea *area = new PrefPageScrollArea(page, this); + connect(area->page, &PrefPageInterface::updateButtons, this, &PrefDialog::updateButtons); + + KPageWidgetItem *p = addPage(area, page->config(), page->pageName(), page->pageIcon()); + area->page_widget_item = p; + pages.append(area); + if (!isHidden()) + page->loadSettings(); +} + +void PrefDialog::removePrefPage(PrefPageInterface *page) +{ + PrefPageScrollArea *found = nullptr; + for (PrefPageScrollArea *area : qAsConst(pages)) { + if (area->page == page) { + found = area; + break; + } + } + + if (found) { + found->takeWidget(); + pages.removeAll(found); + removePage(found->page_widget_item); + } +} + +void PrefDialog::updateWidgetsAndShow() +{ + updateWidgets(); + show(); +} + +void PrefDialog::updateWidgets() +{ + for (PrefPageScrollArea *area : qAsConst(pages)) + area->page->loadSettings(); +} + +void PrefDialog::updateWidgetsDefault() +{ + for (PrefPageScrollArea *area : qAsConst(pages)) + area->page->loadDefaults(); +} + +void PrefDialog::updateSettings() +{ + for (PrefPageScrollArea *area : qAsConst(pages)) + area->page->updateSettings(); +} + +void PrefDialog::calculateRecommendedSettings() +{ + RecommendedSettingsDlg dlg(this); + if (dlg.exec() == QDialog::Accepted) { + qm_pref->kcfg_maxSeeds->setValue(dlg.max_seeds); + qm_pref->kcfg_maxDownloads->setValue(dlg.max_downloads); + qm_pref->kcfg_numUploadSlots->setValue(dlg.max_slots); + net_pref->kcfg_maxDownloadRate->setValue(dlg.max_download_speed); + net_pref->kcfg_maxUploadRate->setValue(dlg.max_upload_speed); + net_pref->kcfg_maxConnections->setValue(dlg.max_conn_tor); + net_pref->kcfg_maxTotalConnections->setValue(dlg.max_conn_glob); + } +} + +void PrefDialog::loadState(KSharedConfigPtr cfg) +{ + KConfigGroup g = cfg->group("PrefDialog"); + QSize s = g.readEntry("size", sizeHint()); + resize(s); +} + +void PrefDialog::saveState(KSharedConfigPtr cfg) +{ + KConfigGroup g = cfg->group("PrefDialog"); + g.writeEntry("size", size()); +} + +bool PrefDialog::hasChanged() +{ + if (KConfigDialog::hasChanged()) + return true; + + for (PrefPageScrollArea *area : qAsConst(pages)) + if (area->page->customWidgetsChanged()) + return true; + + return false; +} + +/////////////////////////////////////// + +PrefPageScrollArea::PrefPageScrollArea(kt::PrefPageInterface *page, QWidget *parent) + : QScrollArea(parent) + , page(page) + , page_widget_item(nullptr) +{ + setWidget(page); + setWidgetResizable(true); + setFrameStyle(QFrame::NoFrame); + viewport()->setAutoFillBackground(false); +} + +PrefPageScrollArea::~PrefPageScrollArea() +{ +} + +} + +// kate: indent-mode cstyle; indent-width 4; replace-tabs on; mixed-indent off; diff --git a/ktorrent/pref/prefdialog.h b/ktorrent/pref/prefdialog.h new file mode 100644 index 0000000..b4f53c5 --- /dev/null +++ b/ktorrent/pref/prefdialog.h @@ -0,0 +1,89 @@ +/* + SPDX-FileCopyrightText: 2007 Joris Guisson + SPDX-FileCopyrightText: 2007 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KT_PREFDIALOG_HH +#define KT_PREFDIALOG_HH + +#include +#include + +#include + +namespace kt +{ +class Core; +class PrefPageInterface; +class NetworkPref; +class QMPref; +class PrefPageScrollArea; + +/** + * KTorrent's preferences dialog, this uses the new KConfigDialog class which should make our live much easier. + * In order for this to work properly the widgets have to be named kcfg_FOO where FOO is a somehting out of our settings class. + * + * The use of KConfigDialog should deprecate PrefPageInterface. + * */ +class PrefDialog : public KConfigDialog +{ + Q_OBJECT +public: + PrefDialog(QWidget *parent, Core *core); + ~PrefDialog() override; + + /** + * Add a pref page to the dialog. + * @param page The page + * */ + void addPrefPage(PrefPageInterface *page); + + /** + * Remove a pref page. + * @param page The page + * */ + void removePrefPage(PrefPageInterface *page); + + /** + * Update the widgets and show + */ + void updateWidgetsAndShow(); + + /** + * Load the state of the dialog + */ + void loadState(KSharedConfigPtr cfg); + + /** + * Save the state of the dialog + */ + void saveState(KSharedConfigPtr cfg); + +protected: + void updateWidgets() override; + void updateWidgetsDefault() override; + void updateSettings() override; + bool hasChanged() override; + +private Q_SLOTS: + void calculateRecommendedSettings(); + +private: + QList pages; + NetworkPref *net_pref; + QMPref *qm_pref; +}; + +class PrefPageScrollArea : public QScrollArea +{ +public: + PrefPageScrollArea(PrefPageInterface *page, QWidget *parent = nullptr); + ~PrefPageScrollArea() override; + + PrefPageInterface *page; + KPageWidgetItem *page_widget_item; +}; +} + +#endif diff --git a/ktorrent/pref/proxypref.cpp b/ktorrent/pref/proxypref.cpp new file mode 100644 index 0000000..203063f --- /dev/null +++ b/ktorrent/pref/proxypref.cpp @@ -0,0 +1,61 @@ +/* + SPDX-FileCopyrightText: 2008 Joris Guisson + SPDX-FileCopyrightText: 2008 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "proxypref.h" +#include "settings.h" + +namespace kt +{ +ProxyPref::ProxyPref(QWidget *parent) + : PrefPageInterface(Settings::self(), i18n("Proxy"), QStringLiteral("preferences-system-network-proxy"), parent) +{ + setupUi(this); + connect(kcfg_socksEnabled, &QCheckBox::toggled, this, &ProxyPref::socksEnabledToggled); + connect(kcfg_socksUsePassword, &QCheckBox::toggled, this, &ProxyPref::usernamePasswordToggled); +} + +ProxyPref::~ProxyPref() +{ +} + +void ProxyPref::loadDefaults() +{ + loadSettings(); +} + +void ProxyPref::loadSettings() +{ + kcfg_httpProxy->setEnabled(!Settings::useKDEProxySettings()); + kcfg_httpProxyPort->setEnabled(!Settings::useKDEProxySettings()); + kcfg_useProxyForWebSeeds->setEnabled(!Settings::useKDEProxySettings()); + kcfg_useProxyForTracker->setEnabled(!Settings::useKDEProxySettings()); + + kcfg_socksProxy->setEnabled(Settings::socksEnabled()); + kcfg_socksVersion->setEnabled(Settings::socksEnabled()); + kcfg_socksPort->setEnabled(Settings::socksEnabled()); + + kcfg_socksUsePassword->setEnabled(Settings::socksEnabled()); + kcfg_socksPassword->setEnabled(Settings::socksUsePassword() && Settings::socksEnabled()); + kcfg_socksUsername->setEnabled(Settings::socksUsePassword() && Settings::socksEnabled()); +} + +void ProxyPref::updateSettings() +{ +} + +void ProxyPref::socksEnabledToggled(bool on) +{ + kcfg_socksUsePassword->setEnabled(on); + kcfg_socksPassword->setEnabled(on && kcfg_socksUsePassword->isChecked()); + kcfg_socksUsername->setEnabled(on && kcfg_socksUsePassword->isChecked()); +} + +void ProxyPref::usernamePasswordToggled(bool on) +{ + kcfg_socksPassword->setEnabled(on && kcfg_socksEnabled->isChecked()); + kcfg_socksUsername->setEnabled(on && kcfg_socksEnabled->isChecked()); +} +} diff --git a/ktorrent/pref/proxypref.h b/ktorrent/pref/proxypref.h new file mode 100644 index 0000000..1086448 --- /dev/null +++ b/ktorrent/pref/proxypref.h @@ -0,0 +1,35 @@ +/* + SPDX-FileCopyrightText: 2008 Joris Guisson + SPDX-FileCopyrightText: 2008 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KTPROXYPREF_H +#define KTPROXYPREF_H + +#include "ui_proxypref.h" +#include + +namespace kt +{ +/** + @author +*/ +class ProxyPref : public PrefPageInterface, public Ui_ProxyPref +{ + Q_OBJECT +public: + ProxyPref(QWidget *parent); + ~ProxyPref() override; + + void loadDefaults() override; + void loadSettings() override; + void updateSettings() override; +private Q_SLOTS: + void socksEnabledToggled(bool on); + void usernamePasswordToggled(bool on); +}; + +} + +#endif diff --git a/ktorrent/pref/proxypref.ui b/ktorrent/pref/proxypref.ui new file mode 100644 index 0000000..5eb42ce --- /dev/null +++ b/ktorrent/pref/proxypref.ui @@ -0,0 +1,348 @@ + + + ProxyPref + + + + 0 + 0 + 417 + 387 + + + + + + + HTTP + + + + + + Use the HTTP proxy settings configured in the settings of KDE. + + + Use KDE proxy settings + + + + + + + + + Proxy: + + + + + + + IP address or hostname of the HTTP proxy to use. + + + + + + + Port: + + + + + + + Port of the HTTP proxy. + + + 1 + + + 65535 + + + 8080 + + + + + + + + + Whether or not to use the HTTP proxy for webseed connections. + + + Use proxy for webseed connections + + + + + + + Whether or not to use the HTTP proxy for tracker connections. + + + Use proxy for tracker connections + + + + + + + + + + SOCKS + + + + + + <p>Use a SOCKS proxy server for bittorrent connections.</p> +<p><span style=" font-weight:600;">Note: </span>This is not used for tracker connections</p> + + + Use a SOCKS proxy server: + + + + + + + + + Server: + + + + + + + IP address or hostname of the SOCKS server. + + + + + + + Port: + + + + + + + Port used by the SOCKS server. + + + + + + + Version: + + + + + + + + + + + + If the SOCKS server requires authentication via a username and a password, enable this box. + + + Username and password required + + + + + + + + + + + Username: + + + + + + + Username for the SOCKS proxy server. + + + + + + + Password: + + + + + + + Password for the SOCKS proxy server. + + + QLineEdit::Password + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + Qt::Vertical + + + + 409 + 21 + + + + + + + + + + kcfg_socksEnabled + toggled(bool) + kcfg_socksProxy + setEnabled(bool) + + + 67 + 216 + + + 179 + 249 + + + + + kcfg_socksEnabled + toggled(bool) + kcfg_socksPort + setEnabled(bool) + + + 230 + 216 + + + 290 + 248 + + + + + kcfg_socksEnabled + toggled(bool) + kcfg_socksVersion + setEnabled(bool) + + + 302 + 216 + + + 402 + 248 + + + + + kcfg_useKDEProxySettings + toggled(bool) + kcfg_httpProxy + setDisabled(bool) + + + 107 + 47 + + + 170 + 85 + + + + + kcfg_useKDEProxySettings + toggled(bool) + kcfg_httpProxyPort + setDisabled(bool) + + + 201 + 50 + + + 368 + 77 + + + + + kcfg_useKDEProxySettings + toggled(bool) + kcfg_useProxyForWebSeeds + setDisabled(bool) + + + 111 + 50 + + + 96 + 111 + + + + + kcfg_useKDEProxySettings + toggled(bool) + kcfg_useProxyForTracker + setDisabled(bool) + + + 69 + 48 + + + 67 + 146 + + + + + diff --git a/ktorrent/pref/qmpref.cpp b/ktorrent/pref/qmpref.cpp new file mode 100644 index 0000000..8aba4ff --- /dev/null +++ b/ktorrent/pref/qmpref.cpp @@ -0,0 +1,41 @@ +/* + SPDX-FileCopyrightText: 2009 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "qmpref.h" +#include "settings.h" + +namespace kt +{ +QMPref::QMPref(QWidget *parent) + : PrefPageInterface(Settings::self(), i18n("Queue Manager"), QStringLiteral("preferences-log"), parent) +{ + setupUi(this); + connect(kcfg_manuallyControlTorrents, &QCheckBox::toggled, this, &QMPref::onControlTorrentsManuallyToggled); + kcfg_stallTimer->setSuffix(i18n(" min")); +} + +QMPref::~QMPref() +{ +} + +void QMPref::loadSettings() +{ + kcfg_stallTimer->setEnabled(Settings::decreasePriorityOfStalledTorrents() && !Settings::manuallyControlTorrents()); + kcfg_maxDownloads->setDisabled(Settings::manuallyControlTorrents()); + kcfg_maxSeeds->setDisabled(Settings::manuallyControlTorrents()); + kcfg_decreasePriorityOfStalledTorrents->setDisabled(Settings::manuallyControlTorrents()); +} + +void QMPref::loadDefaults() +{ + loadSettings(); +} + +void QMPref::onControlTorrentsManuallyToggled(bool on) +{ + kcfg_stallTimer->setEnabled(kcfg_decreasePriorityOfStalledTorrents->isChecked() && !on); +} + +} diff --git a/ktorrent/pref/qmpref.h b/ktorrent/pref/qmpref.h new file mode 100644 index 0000000..2c773c2 --- /dev/null +++ b/ktorrent/pref/qmpref.h @@ -0,0 +1,32 @@ +/* + SPDX-FileCopyrightText: 2009 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KTQMPREF_H +#define KTQMPREF_H + +#include "ui_qmpref.h" +#include + +namespace kt +{ +/** + Preference page for the queue manager +*/ +class QMPref : public PrefPageInterface, public Ui_QMPref +{ + Q_OBJECT +public: + QMPref(QWidget *parent); + ~QMPref() override; + + void loadSettings() override; + void loadDefaults() override; +private Q_SLOTS: + void onControlTorrentsManuallyToggled(bool on); +}; + +} + +#endif diff --git a/ktorrent/pref/qmpref.ui b/ktorrent/pref/qmpref.ui new file mode 100644 index 0000000..d78ac9a --- /dev/null +++ b/ktorrent/pref/qmpref.ui @@ -0,0 +1,356 @@ + + + QMPref + + + + 0 + 0 + 635 + 483 + + + + + + + When enabled, the queue manager will be totally disabled, allowing you to fully control all torrents manually. + + + Control torrents manually + + + + + + + Queue Manager + + + + + + + + Maximum downloads: + + + + + + + The maximum number of simultaneous downloads the queue manager will run. + + + No limit + + + 99999 + + + + + + + Maximum seeds: + + + + + + + The maximum number of simultaneous seeds the queue manager will run. + + + No limit + + + 99999 + + + + + + + When diskspace is running low: + + + + + + + What to do when the diskspace is running low and the queue manager wants to start a torrent. + + + + Don't start torrents + + + + + Ask if torrents can be started + + + + + Start torrents + + + + + + + + Stop torrents when free disk space is lower than: + + + + + + + When the free diskspace drops below this value, stop all torrents downloading. + + + MiB + + + 1 + + + 999999999 + + + 100 + + + + + + + <p>With this enabled, the queue manager will decrease the priority of a torrent which has been stalled for too long. </p> +<p>This will allow the queue manager to run other torrents, when a torrent is doing nothing.</p> + + + Decrease priority of torrents which are stalled for more than: + + + + + + + <p>Time used for the stall timer. When a torrent is stalled longer than this, its priority will be decreased.</p> + + + 1 + + + 1000000 + + + + + + + + + + + + Seeding + + + + + + When a download is finished, continue seeding it. If this is disabled, the torrent will be stopped. + + + Keep seeding after download is finished + + + + + + + Number of upload slots: + + + + + + + The number of upload slots, this determines the number of peers you can upload to simultaneously for one torrent. + + + 2 + + + 100 + + + 2 + + + + + + + Default maximum share ratio: + + + + + + + <p>The maximum share ratio, if this value is reached seeding will be stopped. This is only applied to newly opened torrents, existing torrents will not be affected by changing this.</p> +<p><span style=" font-weight:600;">Attention: </span>This is not used when downloading, only when seeding.</p> + + + No limit + + + 0.010000000000000 + + + 1.500000000000000 + + + + + + + Default maximum seed time: + + + + + + + The maximum seed time in hours. Once you reach this time, the torrent will be stopped. This is only applied to newly opened torrents, existing torrents will not be affected by changing this. + + + No limit + + + 65000000.000000000000000 + + + 0.050000000000000 + + + 0.000000000000000 + + + + + + + Value at which the share ratio will be displayed with a green color. Lower ratios will be displayed in red. Colors can be changed in color settings. + + + Minimum good share ratio: + + + + + + + Value at which the share ratio will be displayed with a green color. Lower ratios will be displayed in red. Colors can be changed in color settings. + + + No limit + + + 0.010000000000000 + + + 1.500000000000000 + + + + + + + + + + Qt::Vertical + + + + 547 + 31 + + + + + + + + + + kcfg_decreasePriorityOfStalledTorrents + toggled(bool) + kcfg_stallTimer + setEnabled(bool) + + + 132 + 221 + + + 243 + 255 + + + + + kcfg_manuallyControlTorrents + toggled(bool) + kcfg_maxDownloads + setDisabled(bool) + + + 128 + 12 + + + 369 + 90 + + + + + kcfg_manuallyControlTorrents + toggled(bool) + kcfg_maxSeeds + setDisabled(bool) + + + 253 + 17 + + + 349 + 112 + + + + + kcfg_manuallyControlTorrents + toggled(bool) + kcfg_decreasePriorityOfStalledTorrents + setDisabled(bool) + + + 207 + 9 + + + 167 + 216 + + + + + diff --git a/ktorrent/pref/recommendedsettingsdlg.cpp b/ktorrent/pref/recommendedsettingsdlg.cpp new file mode 100644 index 0000000..587295a --- /dev/null +++ b/ktorrent/pref/recommendedsettingsdlg.cpp @@ -0,0 +1,213 @@ +/* + SPDX-FileCopyrightText: 2008 Joris Guisson + SPDX-FileCopyrightText: 2008 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include +#include + +#include +#include + +#include "recommendedsettingsdlg.h" +#include "settings.h" +#include +#include + +using namespace bt; + +namespace kt +{ +RecommendedSettingsDlg::RecommendedSettingsDlg(QWidget *parent) + : QDialog(parent) +{ + setWindowTitle(i18n("Calculate Recommended Settings")); + setupUi(this); + connect(m_buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); + connect(m_buttonBox->button(QDialogButtonBox::Apply), &QPushButton::clicked, this, &RecommendedSettingsDlg::apply); + connect(m_calculate, &QPushButton::clicked, this, &RecommendedSettingsDlg::calculate); + connect(m_chk_avg_speed_slot, &QCheckBox::toggled, this, &RecommendedSettingsDlg::avgSpeedSlotToggled); + connect(m_chk_sim_torrents, &QCheckBox::toggled, this, &RecommendedSettingsDlg::simTorrentsToggled); + connect(m_chk_slots, &QCheckBox::toggled, this, &RecommendedSettingsDlg::slotsToggled); + connect(m_upload_bw, qOverload(&QSpinBox::valueChanged), this, &RecommendedSettingsDlg::uploadBWChanged); + connect(m_download_bw, qOverload(&QSpinBox::valueChanged), this, &RecommendedSettingsDlg::downloadBWChanged); + + m_avg_speed_slot->setEnabled(false); + m_slots->setEnabled(false); + m_sim_torrents->setEnabled(false); + + loadState(KSharedConfig::openConfig()); + + calculate(); +} + +RecommendedSettingsDlg::~RecommendedSettingsDlg() +{ +} + +void RecommendedSettingsDlg::apply() +{ + saveState(KSharedConfig::openConfig()); + /* Settings::setMaxDownloadRate(max_download_speed); + Settings::setMaxUploadRate(max_upload_speed); + Settings::setMaxConnections(max_conn_tor); + Settings::setMaxTotalConnections(max_conn_glob); + Settings::setNumUploadSlots(max_slots); + Settings::setMaxDownloads(max_downloads); + Settings::setMaxSeeds(max_seeds);*/ + QDialog::accept(); +} + +void RecommendedSettingsDlg::saveState(KSharedConfigPtr cfg) +{ + KConfigGroup g = cfg->group("RecommendedSettingsDlg"); + g.writeEntry("upload_bw", m_upload_bw->value()); + g.writeEntry("download_bw", m_download_bw->value()); + g.writeEntry("avg_speed_slot_enabled", m_chk_avg_speed_slot->isChecked()); + g.writeEntry("avg_speed_slot", m_avg_speed_slot->value()); + g.writeEntry("slots_enabled", m_chk_slots->isChecked()); + g.writeEntry("slots", m_slots->value()); + g.writeEntry("sim_torrents_enabled", m_chk_sim_torrents->isChecked()); + g.writeEntry("sim_torrents", m_sim_torrents->value()); +} + +void RecommendedSettingsDlg::loadState(KSharedConfigPtr cfg) +{ + KConfigGroup g = cfg->group("RecommendedSettingsDlg"); + m_upload_bw->setValue(g.readEntry("upload_bw", 256)); + m_download_bw->setValue(g.readEntry("download_bw", 4000)); + m_chk_avg_speed_slot->setChecked(g.readEntry("avg_speed_slot_enabled", false)); + m_avg_speed_slot->setValue(g.readEntry("avg_speed_slot", 4)); + m_chk_slots->setChecked(g.readEntry("slots_enabled", false)); + m_slots->setValue(g.readEntry("slots", 3)); + m_chk_sim_torrents->setChecked(g.readEntry("sim_torrents_enabled", false)); + m_sim_torrents->setValue(g.readEntry("sim_torrents", 2)); + + uploadBWChanged(m_upload_bw->value()); + downloadBWChanged(m_download_bw->value()); +} + +void RecommendedSettingsDlg::calculate() +{ + Uint32 upload_rate = m_upload_bw->value() / 8; + Uint32 download_rate = m_download_bw->value() / 8; + + max_upload_speed = floor(upload_rate * 0.8); + max_download_speed = floor(download_rate * 0.8); + + qreal avg_slot_up = ceil(std::max(pow(upload_rate / 3.5, 0.55), 4.0)); + + Uint32 max_torrents = std::round(pow(upload_rate * 0.25, 0.3)); + max_downloads = ceil((float)(max_torrents * 2 / 3)); + max_seeds = std::max(max_torrents - max_downloads, (bt::Uint32)1); + + if (m_chk_avg_speed_slot->isChecked()) + avg_slot_up = (qreal)m_avg_speed_slot->value(); + + if (m_chk_sim_torrents->isChecked()) { + max_downloads = m_sim_torrents->value(); + max_torrents = floor(max_downloads * 1.33); + max_seeds = std::max(max_torrents - max_downloads, (bt::Uint32)1); + } + + max_slots = floor(upload_rate / (max_torrents * avg_slot_up)); + + if (m_chk_slots->isChecked()) + max_slots = m_slots->value(); + + if (m_chk_avg_speed_slot->isChecked() && m_chk_sim_torrents->isChecked()) { + max_slots = floor(max_upload_speed / (max_torrents * avg_slot_up)); + } else if (m_chk_avg_speed_slot->isChecked() && m_chk_slots->isChecked()) { + max_torrents = std::round(max_upload_speed / (avg_slot_up * max_slots)); + max_downloads = ceil((float)(max_torrents * 2 / 3)); + max_seeds = std::max(max_torrents - max_downloads, (bt::Uint32)1); + } else if (m_chk_sim_torrents->isChecked() && m_chk_slots->isChecked()) { + avg_slot_up = ceil((float)(max_upload_speed / (max_slots * max_torrents))); + } else if (m_chk_slots->isChecked()) { + avg_slot_up = ceil(std::max(pow(max_upload_speed / 3.5, 0.55), 4.0)); // basis to calculate the number of torrents + max_torrents = std::round(max_upload_speed / (avg_slot_up * max_slots)); + max_downloads = ceil((float)(max_torrents * 2 / 3)); + max_seeds = std::max(max_torrents - max_downloads, (bt::Uint32)1); + avg_slot_up = ceil((float)(max_upload_speed / (max_slots * max_torrents))); // real number after the slots have been multiplied with the torrents + } + + if (max_downloads == 0) + max_downloads = 1; + + max_conn_glob = std::round(std::min((double)pow((int)(upload_rate * 8), 0.8) + 50, 900.0)); + max_conn_tor = std::round(std::min((qreal)(max_conn_glob * 1.2 / max_torrents), (qreal)max_conn_glob)); + + m_max_upload->setText(QStringLiteral("%1").arg(BytesPerSecToString(max_upload_speed * 1024))); + m_max_download->setText(QStringLiteral("%1").arg(BytesPerSecToString(max_download_speed * 1024))); + m_max_conn_per_torrent->setText(QStringLiteral("%1").arg(max_conn_tor)); + m_max_conn_global->setText(QStringLiteral("%1").arg(max_conn_glob)); + m_max_downloads->setText(QStringLiteral("%1").arg(max_downloads)); + m_max_seeds->setText(QStringLiteral("%1").arg(max_seeds)); + m_upload_slots->setText(QStringLiteral("%1").arg(max_slots)); +} + +void RecommendedSettingsDlg::avgSpeedSlotToggled(bool on) +{ + m_avg_speed_slot->setEnabled(on); + if (on && m_chk_slots->isChecked()) { + m_chk_sim_torrents->setEnabled(false); + m_sim_torrents->setEnabled(false); + } else if (on && m_chk_sim_torrents->isChecked()) { + m_chk_slots->setEnabled(false); + m_slots->setEnabled(false); + } else { + m_chk_slots->setEnabled(true); + m_slots->setEnabled(m_chk_slots->isChecked()); + m_chk_sim_torrents->setEnabled(true); + m_sim_torrents->setEnabled(m_chk_sim_torrents->isChecked()); + m_chk_avg_speed_slot->setEnabled(true); + } +} + +void RecommendedSettingsDlg::simTorrentsToggled(bool on) +{ + m_sim_torrents->setEnabled(on); + if (on && m_chk_slots->isChecked()) { + m_chk_avg_speed_slot->setEnabled(false); + m_avg_speed_slot->setEnabled(false); + } else if (on && m_chk_avg_speed_slot->isChecked()) { + m_chk_slots->setEnabled(false); + m_slots->setEnabled(false); + } else { + m_chk_slots->setEnabled(true); + m_slots->setEnabled(m_chk_slots->isChecked()); + m_chk_sim_torrents->setEnabled(true); + m_chk_avg_speed_slot->setEnabled(true); + m_avg_speed_slot->setEnabled(m_chk_avg_speed_slot->isChecked()); + } +} + +void RecommendedSettingsDlg::slotsToggled(bool on) +{ + m_slots->setEnabled(on); + if (on && m_chk_avg_speed_slot->isChecked()) { + m_chk_sim_torrents->setEnabled(false); + m_sim_torrents->setEnabled(false); + } else if (on && m_chk_sim_torrents->isChecked()) { + m_chk_avg_speed_slot->setEnabled(false); + m_avg_speed_slot->setEnabled(false); + } else { + m_chk_slots->setEnabled(true); + m_chk_sim_torrents->setEnabled(true); + m_sim_torrents->setEnabled(m_chk_sim_torrents->isChecked()); + m_chk_avg_speed_slot->setEnabled(true); + m_avg_speed_slot->setEnabled(m_chk_avg_speed_slot->isChecked()); + } +} + +void RecommendedSettingsDlg::uploadBWChanged(int val) +{ + m_upload_bw_display->setText(i18n("(= %1/s)", KFormat().formatByteSize(val * 128))); +} + +void RecommendedSettingsDlg::downloadBWChanged(int val) +{ + m_download_bw_display->setText(i18n("(= %1/s)", KFormat().formatByteSize(val * 128))); +} +} diff --git a/ktorrent/pref/recommendedsettingsdlg.h b/ktorrent/pref/recommendedsettingsdlg.h new file mode 100644 index 0000000..84f176a --- /dev/null +++ b/ktorrent/pref/recommendedsettingsdlg.h @@ -0,0 +1,51 @@ +/* + SPDX-FileCopyrightText: 2008 Joris Guisson + SPDX-FileCopyrightText: 2008 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KTRECOMMENDEDSETTINGSDLG_H +#define KTRECOMMENDEDSETTINGSDLG_H + +#include "ui_recommendedsettingsdlg.h" +#include +#include + +namespace kt +{ +/** + Dialog to compute the best settings +*/ +class RecommendedSettingsDlg : public QDialog, public Ui_RecommendedSettingsDlg +{ + Q_OBJECT +public: + RecommendedSettingsDlg(QWidget *parent); + ~RecommendedSettingsDlg() override; + +private Q_SLOTS: + void calculate(); + void apply(); + void avgSpeedSlotToggled(bool on); + void simTorrentsToggled(bool on); + void slotsToggled(bool on); + void uploadBWChanged(int val); + void downloadBWChanged(int val); + +private: + void saveState(KSharedConfigPtr cfg); + void loadState(KSharedConfigPtr cfg); + +public: + bt::Uint32 max_upload_speed; + bt::Uint32 max_download_speed; + bt::Uint32 max_conn_tor; + bt::Uint32 max_conn_glob; + bt::Uint32 max_downloads; + bt::Uint32 max_seeds; + bt::Uint32 max_slots; +}; + +} + +#endif diff --git a/ktorrent/pref/recommendedsettingsdlg.ui b/ktorrent/pref/recommendedsettingsdlg.ui new file mode 100644 index 0000000..fee56aa --- /dev/null +++ b/ktorrent/pref/recommendedsettingsdlg.ui @@ -0,0 +1,349 @@ + + + RecommendedSettingsDlg + + + + 0 + 0 + 825 + 492 + + + + Recommended Settings + + + + + + + + + + Available upload bandwidth: + + + + + + + Kbps + + + kb/s + + + 1 + + + 100000000 + + + 256 + + + + + + + Available download bandwidth: + + + + + + + Kbps + + + kb/s + + + 1 + + + 100000000 + + + 4000 + + + + + + + (= 32 KiB/s) + + + + + + + (= 500 KiB/s) + + + + + + + + + + + Calculate + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + Preferences + + + + + + Average speed per slot: + + + + + + + KiB/s + + + 1 + + + 100000000 + + + 4 + + + + + + + Number of torrents you usually download simultaneously: + + + + + + + 1 + + + 1000000 + + + 2 + + + + + + + Slots per torrent: + + + + + + + 1 + + + 10000 + + + 3 + + + + + + + + + + Recommended settings: + + + + + + + + + Network + + + + + + Maximum upload speed: + + + + + + + TextLabel + + + + + + + Maximum download speed: + + + + + + + TextLabel + + + + + + + Maximum connections per torrent: + + + + + + + TextLabel + + + + + + + Global connection limit: + + + + + + + TextLabel + + + + + + + + + + Queue Manager + + + + + + + + Number of upload slots: + + + + + + + TextLabel + + + + + + + Maximum downloads: + + + + + + + TextLabel + + + + + + + Maximum seeds: + + + + + + + TextLabel + + + + + + + + + Qt::Vertical + + + + 20 + 12 + + + + + + + + + + + + + Qt::Horizontal + + + + + + + QDialogButtonBox::Apply|QDialogButtonBox::Cancel + + + + + + + + diff --git a/ktorrent/statusbar.cpp b/ktorrent/statusbar.cpp new file mode 100644 index 0000000..0a8e398 --- /dev/null +++ b/ktorrent/statusbar.cpp @@ -0,0 +1,113 @@ +/* + SPDX-FileCopyrightText: 2007 Joris Guisson + SPDX-FileCopyrightText: 2007 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "statusbar.h" + +#include +#include +#include + +#include +#include + +#include "statusbarofflineindicator.h" +using namespace bt; + +namespace kt +{ +StatusBar::StatusBar(QWidget *parent) + : QStatusBar(parent) + , speed(nullptr) + , up_speed(0) + , down_speed(0) + , transfer(nullptr) + , up_transfer(0) + , down_transfer(0) + , dht_status(nullptr) + , dht_peers(0) + , dht_tasks(0) + , dht_on(false) +{ + QString s = i18n("Speed down: %1 / up: %2", BytesPerSecToString((double)down_speed), BytesPerSecToString((double)up_speed)); + QString t = i18n("Transferred down: %1 / up: %2", BytesToString(down_transfer), BytesToString(up_transfer)); + + dht_status = new QLabel(i18n("DHT: off"), this); + dht_status->setFrameShape(QFrame::Box); + dht_status->setFrameShadow(QFrame::Sunken); + addPermanentWidget(dht_status); + + speed = new QLabel(s, this); + speed->setFrameShape(QFrame::Box); + speed->setFrameShadow(QFrame::Sunken); + addPermanentWidget(speed); + + transfer = new QLabel(t, this); + transfer->setFrameShape(QFrame::Box); + transfer->setFrameShadow(QFrame::Sunken); + addPermanentWidget(transfer); + + addPermanentWidget(new StatusBarOfflineIndicator(this)); +} + +StatusBar::~StatusBar() +{ +} + +void StatusBar::updateSpeed(bt::Uint32 up, bt::Uint32 down) +{ + if (up == up_speed && down == down_speed) + return; + + up_speed = up; + down_speed = down; + QString s = i18n("Speed down: %1 / up: %2", BytesPerSecToString((double)down_speed), BytesPerSecToString((double)up_speed)); + speed->setText(s); +} + +void StatusBar::updateTransfer(bt::Uint64 up, bt::Uint64 down) +{ + if (up == up_transfer && down == down_transfer) + return; + + up_transfer = up; + down_transfer = down; + QString t = i18n("Transferred down: %1 / up: %2", BytesToString(down_transfer), BytesToString(up_transfer)); + transfer->setText(t); +} + +void StatusBar::updateDHTStatus(bool on, const dht::Stats &s) +{ + if (on == dht_on && dht_peers == s.num_peers && dht_tasks == s.num_tasks) + return; + + dht_on = on; + dht_peers = s.num_peers; + dht_tasks = s.num_tasks; + if (on) + dht_status->setText(i18n("DHT: %1, %2", i18np("%1 node", "%1 nodes", s.num_peers), i18np("%1 task", "%1 tasks", s.num_tasks))); + else + dht_status->setText(i18n("DHT: off")); +} + +QProgressBar *StatusBar::createProgressBar() +{ + QProgressBar *pb = new QProgressBar(this); + pb->setMaximumHeight(height()); + addPermanentWidget(pb); + return pb; +} + +void StatusBar::removeProgressBar(QProgressBar *pb) +{ + removeWidget(pb); + pb->deleteLater(); +} + +void StatusBar::message(const QString &msg) +{ + showMessage(msg, 30 * 1000); +} +} diff --git a/ktorrent/statusbar.h b/ktorrent/statusbar.h new file mode 100644 index 0000000..ec949a4 --- /dev/null +++ b/ktorrent/statusbar.h @@ -0,0 +1,67 @@ +/* + SPDX-FileCopyrightText: 2007 Joris Guisson + SPDX-FileCopyrightText: 2007 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KT_STATUSBAR_HH +#define KT_STATUSBAR_HH + +#include +#include +#include + +class QLabel; + +namespace dht +{ +struct Stats; +} + +namespace kt +{ +/** + * Class which handles the statusbar + * */ +class StatusBar : public QStatusBar, public StatusBarInterface +{ + Q_OBJECT +public: + StatusBar(QWidget *parent); + ~StatusBar() override; + + /// Update the speed info of the status bar (speeds are in bytes per sec) + void updateSpeed(bt::Uint32 up, bt::Uint32 down); + + /// Update the number of bytes transferred + void updateTransfer(bt::Uint64 up, bt::Uint64 down); + + /// Update the DHT stats + void updateDHTStatus(bool on, const dht::Stats &s); + + /// Create a progress bar and put it on the right side of the statusbar + QProgressBar *createProgressBar() override; + + /// Remove a progress bar created with createProgressBar + void removeProgressBar(QProgressBar *pb) override; +public Q_SLOTS: + /// Show an information message + void message(const QString &msg) override; + +private: + QLabel *speed; + bt::Uint32 up_speed; + bt::Uint32 down_speed; + + QLabel *transfer; + bt::Uint64 up_transfer; + bt::Uint64 down_transfer; + + QLabel *dht_status; + bt::Uint32 dht_peers; + bt::Uint32 dht_tasks; + bool dht_on; +}; +} + +#endif diff --git a/ktorrent/statusbarofflineindicator.cpp b/ktorrent/statusbarofflineindicator.cpp new file mode 100644 index 0000000..1b6effd --- /dev/null +++ b/ktorrent/statusbarofflineindicator.cpp @@ -0,0 +1,65 @@ +/* This file is part of the KDE project + SPDX-FileCopyrightText: 2007 Will Stephenson + SPDX-License-Identifier: LGPL-2.0-only WITH Qt-Commercial-exception-1.0 +*/ + +#include "statusbarofflineindicator.h" + +#include +#include +#include +#include + +#include +#include + +class StatusBarOfflineIndicatorPrivate : public QObject +{ +public: + explicit StatusBarOfflineIndicatorPrivate(StatusBarOfflineIndicator *parent) + : q(parent) + , networkConfiguration(new QNetworkConfigurationManager(parent)) + { + } + + void initialize(); + void _k_networkStatusChanged(bool isOnline); + + StatusBarOfflineIndicator *const q; + QNetworkConfigurationManager *networkConfiguration; +}; + +StatusBarOfflineIndicator::StatusBarOfflineIndicator(QWidget *parent) + : QWidget(parent) + , d(new StatusBarOfflineIndicatorPrivate(this)) +{ + QVBoxLayout *layout = new QVBoxLayout(this); + layout->setContentsMargins(2, 2, 2, 2); + QLabel *label = new QLabel(this); + label->setPixmap(QIcon::fromTheme(QStringLiteral("network-disconnect")).pixmap(KIconLoader::SizeSmall)); + label->setToolTip(i18n("The desktop is offline")); + layout->addWidget(label); + d->initialize(); + connect(d->networkConfiguration, &QNetworkConfigurationManager::onlineStateChanged, d, &StatusBarOfflineIndicatorPrivate::_k_networkStatusChanged); +} + +StatusBarOfflineIndicator::~StatusBarOfflineIndicator() +{ + delete d; +} + +void StatusBarOfflineIndicatorPrivate::initialize() +{ + _k_networkStatusChanged(networkConfiguration->isOnline()); +} + +void StatusBarOfflineIndicatorPrivate::_k_networkStatusChanged(bool isOnline) +{ + if (isOnline) { + q->hide(); + } else { + q->show(); + } +} + +#include "moc_statusbarofflineindicator.cpp" diff --git a/ktorrent/statusbarofflineindicator.h b/ktorrent/statusbarofflineindicator.h new file mode 100644 index 0000000..b79150d --- /dev/null +++ b/ktorrent/statusbarofflineindicator.h @@ -0,0 +1,28 @@ +/* This file is part of the KDE project + SPDX-FileCopyrightText: 2007 Will Stephenson + SPDX-License-Identifier: LGPL-2.0-only WITH Qt-Commercial-exception-1.0 +*/ + +#ifndef STATUSBAROFFLINEINDICATOR_H +#define STATUSBAROFFLINEINDICATOR_H + +#include + +class StatusBarOfflineIndicatorPrivate; + +class StatusBarOfflineIndicator : public QWidget +{ + Q_OBJECT +public: + /** + * Default constructor. + * @param parent the widget's parent + */ + explicit StatusBarOfflineIndicator(QWidget *parent); + ~StatusBarOfflineIndicator(); + +private: + StatusBarOfflineIndicatorPrivate *const d; +}; + +#endif diff --git a/ktorrent/tools/magnetmodel.cpp b/ktorrent/tools/magnetmodel.cpp new file mode 100644 index 0000000..804aec5 --- /dev/null +++ b/ktorrent/tools/magnetmodel.cpp @@ -0,0 +1,169 @@ +/* + SPDX-FileCopyrightText: 2009 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "magnetmodel.h" +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace kt +{ +MagnetModel::MagnetModel(MagnetManager *magnetManager, QObject *parent) + : QAbstractTableModel(parent) + , currentRows(0) + , mman(magnetManager) +{ + connect(mman.data(), &MagnetManager::updateQueue, this, &MagnetModel::onUpdateQueue); +} + +MagnetModel::~MagnetModel() +{ +} + +void MagnetModel::removeMagnets(int row, int count) +{ + mman->removeMagnets(row, count); +} + +void MagnetModel::start(int row, int count) +{ + mman->start(row, count); +} + +void MagnetModel::stop(int row, int count) +{ + mman->stop(row, count); +} + +bool MagnetModel::isStopped(int row) const +{ + return mman->isStopped(row); +} + +void MagnetModel::onUpdateQueue(bt::Uint32 idx, bt::Uint32 count) +{ + int rows = mman->count(); + if (currentRows < rows) // add new rows + insertRows(idx, rows - currentRows, QModelIndex()); + else if (currentRows > rows) // delete rows + removeRows(idx, currentRows - rows, QModelIndex()); + + currentRows = rows; + Q_EMIT dataChanged(index(idx, 0), index(count, columnCount(QModelIndex()))); +} + +QVariant MagnetModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid() || index.row() < 0 || index.row() >= mman->count()) + return QVariant(); + + const MagnetDownloader *md = mman->getMagnetDownloader(index.row()); + if (role == Qt::DisplayRole) { + switch (index.column()) { + case 0: + return displayName(md); + case 1: + return status(index.row()); + case 2: + return md->numPeers(); + default: + return QVariant(); + } + } else if (role == Qt::DecorationRole) { + if (index.column() == 0) + return QIcon::fromTheme(QStringLiteral("kt-magnet")); + } else if (role == Qt::ToolTipRole) { + if (index.column() == 0) + return md->magnetLink().toString(); + } + + return QVariant(); +} + +QVariant MagnetModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if (orientation == Qt::Vertical) + return QVariant(); + + if (role == Qt::DisplayRole) { + switch (section) { + case 0: + return i18n("Magnet Link"); + case 1: + return i18n("Status"); + case 2: + return i18n("Peers"); + default: + return QVariant(); + } + } + + return QVariant(); +} + +int MagnetModel::columnCount(const QModelIndex &parent) const +{ + if (parent.isValid()) + return 0; + else + return 3; +} + +int MagnetModel::rowCount(const QModelIndex &parent) const +{ + if (parent.isValid() || !mman) + return 0; + else + return mman->count(); +} + +bool MagnetModel::insertRows(int row, int count, const QModelIndex &parent) +{ + Q_UNUSED(parent); + beginInsertRows(QModelIndex(), row, row + count - 1); + endInsertRows(); + return true; +} + +bool MagnetModel::removeRows(int row, int count, const QModelIndex &parent) +{ + Q_UNUSED(parent); + beginRemoveRows(QModelIndex(), row, row + count - 1); + endRemoveRows(); + return true; +} + +QString MagnetModel::displayName(const bt::MagnetDownloader *md) const +{ + if (md->magnetLink().displayName().isEmpty()) + return md->magnetLink().toString(); + else + return md->magnetLink().displayName(); +} + +QString MagnetModel::status(int row) const +{ + switch (mman->status(row)) { + case MagnetManager::DOWNLOADING: + return i18n("Downloading"); + + case MagnetManager::QUEUED: + return i18n("Queued"); + + case MagnetManager::STOPPED: + default: + return i18n("Stopped"); + } +} +} diff --git a/ktorrent/tools/magnetmodel.h b/ktorrent/tools/magnetmodel.h new file mode 100644 index 0000000..ce8b5e0 --- /dev/null +++ b/ktorrent/tools/magnetmodel.h @@ -0,0 +1,62 @@ +/* + SPDX-FileCopyrightText: 2009 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KT_MAGNETMODEL_H +#define KT_MAGNETMODEL_H + +#include +#include +#include + +namespace bt +{ +class MagnetDownloader; +} + +namespace kt +{ +class MagnetManager; + +class MagnetModel : public QAbstractTableModel +{ + Q_OBJECT +public: + MagnetModel(MagnetManager *magnetManager, QObject *parent = nullptr); + ~MagnetModel() override; + + /// Remove a magnet downloader + void removeMagnets(int row, int count); + + /// Start a magnet downloader + void start(int row, int count); + + /// Stop a magnet downloader + void stop(int row, int count); + + /// Check if the magnet downloader that correspond to row is stopped + bool isStopped(int row) const; + + QVariant headerData(int section, Qt::Orientation orientation, int role) const override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + int columnCount(const QModelIndex &parent = QModelIndex()) const override; + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + bool removeRows(int row, int count, const QModelIndex &parent) override; + bool insertRows(int row, int count, const QModelIndex &parent) override; + +public Q_SLOTS: + void onUpdateQueue(bt::Uint32 idx, bt::Uint32 count); + +private: + QString displayName(const bt::MagnetDownloader *md) const; + QString status(int row) const; + +private: + int currentRows; + QPointer mman; +}; + +} + +#endif // KT_MAGNETMODEL_H diff --git a/ktorrent/tools/magnetview.cpp b/ktorrent/tools/magnetview.cpp new file mode 100644 index 0000000..424943c --- /dev/null +++ b/ktorrent/tools/magnetview.cpp @@ -0,0 +1,142 @@ +/* + SPDX-FileCopyrightText: 2009 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "magnetview.h" +#include "magnetmodel.h" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace kt +{ +MagnetView::MagnetView(MagnetManager *magnetManager, QWidget *parent) + : QWidget(parent) + , mman(magnetManager) +{ + model = new MagnetModel(magnetManager, this); + + QHBoxLayout *layout = new QHBoxLayout(this); + layout->setMargin(0); + layout->setSpacing(0); + + // magnets view + view = new QTreeView(this); + view->setModel(model); + view->setUniformRowHeights(true); + view->setRootIsDecorated(false); + view->setAlternatingRowColors(true); + view->setSelectionBehavior(QAbstractItemView::SelectRows); + view->setSortingEnabled(false); + view->setAllColumnsShowFocus(true); + view->setSelectionMode(QAbstractItemView::ContiguousSelection); + view->setContextMenuPolicy(Qt::CustomContextMenu); + connect(view, &QTreeView::customContextMenuRequested, this, &MagnetView::showContextMenu); + layout->addWidget(view); + + // context menu + menu = new QMenu(this); + start = menu->addAction(QIcon::fromTheme(QStringLiteral("kt-start")), i18n("Start Magnet"), this, &MagnetView::startMagnetDownload); + stop = menu->addAction(QIcon::fromTheme(QStringLiteral("kt-stop")), i18n("Stop Magnet"), this, &MagnetView::stopMagnetDownload); + copy_url = menu->addAction(QIcon::fromTheme(QStringLiteral("edit-copy")), i18n("Copy Magnet URL"), this, &MagnetView::copyMagnetUrl); + menu->addSeparator(); + remove = menu->addAction(QIcon::fromTheme(QStringLiteral("kt-remove")), i18n("Remove Magnet"), this, &MagnetView::removeMagnetDownload); +} + +MagnetView::~MagnetView() +{ +} + +void MagnetView::loadState(KSharedConfigPtr cfg) +{ + KConfigGroup g = cfg->group("MagnetView"); + QByteArray s = QByteArray::fromBase64(g.readEntry("state", QByteArray())); + if (!s.isEmpty()) { + QHeaderView *v = view->header(); + v->restoreState(s); + } +} + +void MagnetView::saveState(KSharedConfigPtr cfg) +{ + KConfigGroup g = cfg->group("MagnetView"); + g.writeEntry("state", view->header()->saveState().toBase64()); +} + +void MagnetView::showContextMenu(QPoint p) +{ + const QModelIndexList idx_list = view->selectionModel()->selectedRows(); + + start->setEnabled(false); + stop->setEnabled(false); + remove->setEnabled(idx_list.count() > 0); + + for (const QModelIndex &idx : idx_list) { + if (!mman->isStopped(idx.row())) + stop->setEnabled(true); + else + start->setEnabled(true); + } + menu->popup(view->viewport()->mapToGlobal(p)); +} + +void MagnetView::removeMagnetDownload() +{ + QModelIndexList idx_list = view->selectionModel()->selectedRows(); + if (!idx_list.isEmpty()) + mman->removeMagnets(idx_list.front().row(), idx_list.size()); +} + +void MagnetView::startMagnetDownload() +{ + QModelIndexList idx_list = view->selectionModel()->selectedRows(); + if (!idx_list.isEmpty()) { + mman->start(idx_list.front().row(), idx_list.size()); + view->clearSelection(); + } +} + +void MagnetView::stopMagnetDownload() +{ + QModelIndexList idx_list = view->selectionModel()->selectedRows(); + if (!idx_list.isEmpty()) { + mman->stop(idx_list.front().row(), idx_list.size()); + view->clearSelection(); + } +} + +void MagnetView::copyMagnetUrl() +{ + QStringList sl; + const QModelIndexList idx_list = view->selectionModel()->selectedRows(); + for (const QModelIndex &idx : idx_list) { + if (const MagnetDownloader *md = mman->getMagnetDownloader(idx.row())) { + sl.append(md->magnetLink().toString()); + } + } + if (QClipboard *clipboard = QGuiApplication::clipboard()) { + clipboard->setText(sl.join(QStringLiteral("\n"))); + } +} + +void MagnetView::keyPressEvent(QKeyEvent *event) +{ + if (event->key() == Qt::Key_Delete) { + removeMagnetDownload(); + event->accept(); + } else + QWidget::keyPressEvent(event); +} +} diff --git a/ktorrent/tools/magnetview.h b/ktorrent/tools/magnetview.h new file mode 100644 index 0000000..38c3a17 --- /dev/null +++ b/ktorrent/tools/magnetview.h @@ -0,0 +1,56 @@ +/* + SPDX-FileCopyrightText: 2009 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KT_MAGNETVIEW_H +#define KT_MAGNETVIEW_H + +#include +#include + +class QTreeView; +class QMenu; + +namespace kt +{ +class MagnetManager; +class MagnetModel; + +/** + View which displays a list of magnet links being downloaded +*/ +class MagnetView : public QWidget +{ + Q_OBJECT +public: + MagnetView(MagnetManager *magnetManager, QWidget *parent = nullptr); + ~MagnetView() override; + + void saveState(KSharedConfigPtr cfg); + void loadState(KSharedConfigPtr cfg); + + void keyPressEvent(QKeyEvent *event) override; + +private Q_SLOTS: + void showContextMenu(QPoint p); + void removeMagnetDownload(); + void startMagnetDownload(); + void stopMagnetDownload(); + void copyMagnetUrl(); + +private: + MagnetManager *mman; + MagnetModel *model; + QTreeView *view; + QMenu *menu; + + QAction *start; + QAction *stop; + QAction *copy_url; + QAction *remove; +}; + +} + +#endif // KT_MAGNETVIEW_H diff --git a/ktorrent/tools/queuemanagermodel.cpp b/ktorrent/tools/queuemanagermodel.cpp new file mode 100644 index 0000000..1f11d8f --- /dev/null +++ b/ktorrent/tools/queuemanagermodel.cpp @@ -0,0 +1,507 @@ +/* + SPDX-FileCopyrightText: 2007 Joris Guisson + SPDX-FileCopyrightText: 2007 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "queuemanagermodel.h" + +#include +#include +#include +#include +#include + +#include + +#include "settings.h" +#include +#include +#include +#include + +using namespace bt; + +namespace kt +{ +QueueManagerModel::QueueManagerModel(QueueManager *qman, QObject *parent) + : QAbstractTableModel(parent) + , qman(qman) + , show_uploads(true) + , show_downloads(true) + , show_not_queud(true) +{ + connect(qman, &QueueManager::queueOrdered, this, &QueueManagerModel::onQueueOrdered); + for (bt::TorrentInterface *tc : qAsConst(*qman)) { + connect(tc, &bt::TorrentInterface::statusChanged, this, &QueueManagerModel::onTorrentStatusChanged); + + if (visible(tc)) { + Item item = {tc, 0}; + queue.append(item); + } + } + + // dumpQueue(); +} + +QueueManagerModel::~QueueManagerModel() +{ +} + +void QueueManagerModel::onQueueOrdered() +{ + updateQueue(); +} + +void QueueManagerModel::softReset() +{ + Q_EMIT dataChanged(index(0, 0), index(queue.count() - 1, columnCount(QModelIndex()) - 1)); +} + +void QueueManagerModel::updateQueue() +{ + int count = queue.count(); + queue.clear(); + + for (bt::TorrentInterface *tc : qAsConst(*qman)) { + if (visible(tc)) { + Item item = {tc, 0}; + queue.append(item); + } + } + + if (count == queue.count()) { + softReset(); + } else if (queue.count() > count) { + insertRows(0, queue.count() - count, QModelIndex()); + softReset(); + } else { // queue.count() < count) + removeRows(0, count - queue.count(), QModelIndex()); + softReset(); + } +} + +void QueueManagerModel::setShowDownloads(bool on) +{ + show_downloads = on; + updateQueue(); +} + +void QueueManagerModel::setShowUploads(bool on) +{ + show_uploads = on; + updateQueue(); +} + +void QueueManagerModel::setShowNotQueued(bool on) +{ + show_not_queud = on; + updateQueue(); +} + +void QueueManagerModel::onTorrentAdded(bt::TorrentInterface *tc) +{ + connect(tc, &bt::TorrentInterface::statusChanged, this, &QueueManagerModel::onTorrentStatusChanged); +} + +void QueueManagerModel::onTorrentRemoved(bt::TorrentInterface *tc) +{ + disconnect(tc, &bt::TorrentInterface::statusChanged, this, &QueueManagerModel::onTorrentStatusChanged); + int r = 0; + bool found = false; + + for (const auto &i : qAsConst(queue)) { + if (tc == i.tc) { + found = true; + break; + } + r++; + } + + if (found) { + queue.removeAt(r); + removeRow(r); + } +} + +void QueueManagerModel::onTorrentStatusChanged(bt::TorrentInterface *tc) +{ + int r = 0; + bool found = false; + for (const Item &i : qAsConst(queue)) { + if (tc == i.tc) { + found = true; + break; + } + + r++; + } + + if (found) { + if (!visible(tc)) { + queue.removeAt(r); + removeRow(r); + } else { + QModelIndex idx = index(r, 2); + Q_EMIT dataChanged(idx, idx); + } + return; + } + + if (visible(tc)) { + updateQueue(); + } +} + +int QueueManagerModel::rowCount(const QModelIndex &parent) const +{ + if (parent.isValid()) + return 0; + else + return queue.count(); +} + +int QueueManagerModel::columnCount(const QModelIndex &parent) const +{ + if (parent.isValid()) + return 0; + else + return 4; +} + +QVariant QueueManagerModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if (role != Qt::DisplayRole || orientation != Qt::Horizontal) + return QVariant(); + + switch (section) { + case 0: + return i18n("Order"); + case 1: + return i18n("Name"); + case 2: + return i18n("Status"); + case 3: + return i18n("Time Stalled"); + case 4: + return i18n("Priority"); + default: + return QVariant(); + } +} + +QVariant QueueManagerModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid() || index.row() >= queue.count() || index.row() < 0) + return QVariant(); + + const bt::TorrentInterface *tc = queue.at(index.row()).tc; + if (role == Qt::ForegroundRole) { + if (index.column() == 2) { + if (tc->getStats().running) + return QColor(40, 205, 40); // green + else if (tc->getStats().status == bt::QUEUED) + return QColor(255, 174, 0); // yellow + else + return QVariant(); + } + return QVariant(); + } else if (role == Qt::DisplayRole) { + switch (index.column()) { + case 0: + return index.row() + 1; + case 1: + return tc->getDisplayName(); + case 2: + if (tc->getStats().running) + return i18n("Running"); + else if (tc->getStats().status == bt::QUEUED) + return i18n("Queued"); + else + return i18n("Not queued"); + break; + case 3: { + if (!tc->getStats().running) + return QVariant(); + + Int64 stalled_time = queue.at(index.row()).stalled_time; + if (stalled_time >= 1) + return i18n("%1", DurationToString(stalled_time)); + else + return QVariant(); + } break; + case 4: + return tc->getPriority(); + default: + return QVariant(); + } + } else if (role == Qt::ToolTipRole && index.column() == 0) { + return i18n("Order of a torrent in the queue.\nUse drag and drop or the move up and down buttons on the right to change the order."); + } else if (role == Qt::DecorationRole && index.column() == 1) { + if (!tc->getStats().completed) + return QIcon::fromTheme(QStringLiteral("arrow-down")); + else + return QIcon::fromTheme(QStringLiteral("arrow-up")); + } else if (role == Qt::FontRole && !search_text.isEmpty()) { + QFont f = QApplication::font(); + if (tc->getDisplayName().contains(search_text, Qt::CaseInsensitive)) + f.setBold(true); + + return f; + } + + return QVariant(); +} + +Qt::ItemFlags QueueManagerModel::flags(const QModelIndex &index) const +{ + Qt::ItemFlags defaultFlags = QAbstractTableModel::flags(index); + + if (index.isValid()) + return Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled | defaultFlags; + else + return Qt::ItemIsDropEnabled | defaultFlags; +} + +Qt::DropActions QueueManagerModel::supportedDropActions() const +{ + return Qt::CopyAction | Qt::MoveAction; +} + +QStringList QueueManagerModel::mimeTypes() const +{ + QStringList types; + types << QStringLiteral("application/vnd.text.list"); + return types; +} + +QMimeData *QueueManagerModel::mimeData(const QModelIndexList &indexes) const +{ + QMimeData *mimeData = new QMimeData(); + + dragged_items.clear(); + + for (const QModelIndex &index : indexes) { + if (index.isValid() && !dragged_items.contains(index.row())) + dragged_items.append(index.row()); + } + + mimeData->setData(QStringLiteral("application/vnd.text.list"), QByteArrayLiteral("stuff")); + return mimeData; +} + +bool QueueManagerModel::dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) +{ + Q_UNUSED(column); + if (action == Qt::IgnoreAction) + return true; + + if (!data->hasFormat(QStringLiteral("application/vnd.text.list"))) + return false; + + int begin_row = row; + if (row != -1) { + begin_row = row; + } else if (parent.isValid()) { + begin_row = parent.row(); + } else { + moveBottom(dragged_items.front(), dragged_items.count()); + return true; + } + + int from = dragged_items.front(); + int count = dragged_items.count(); + if (from < begin_row) { + while (from < begin_row) { + for (int i = count - 1; i >= 0; i--) + swapItems(from + i, from + i + 1); + from++; + } + } else { + while (from > begin_row) { + for (int i = 0; i < count; i++) + swapItems(from + i, from + i - 1); + from--; + } + } + + updatePriorities(); + // reorder the queue + qman->orderQueue(); + endResetModel(); + return true; +} + +bool QueueManagerModel::removeRows(int row, int count, const QModelIndex &parent) +{ + Q_UNUSED(parent); + beginInsertRows(QModelIndex(), row, row + count - 1); + endInsertRows(); + return true; +} + +bool QueueManagerModel::insertRows(int row, int count, const QModelIndex &parent) +{ + Q_UNUSED(parent); + beginInsertRows(QModelIndex(), row, row + count - 1); + endInsertRows(); + return true; +} + +void QueueManagerModel::moveUp(int row, int count) +{ + if (row <= 0 || row > qman->count()) + return; + + for (int i = 0; i < count; i++) { + swapItems(row + i, row + i - 1); + } + + updatePriorities(); + // dumpQueue(); + // reorder the queue + qman->orderQueue(); + endResetModel(); +} + +void QueueManagerModel::moveDown(int row, int count) +{ + if (row < 0 || row >= qman->count() - 1) + return; + + for (int i = count - 1; i >= 0; i--) { + swapItems(row + i, row + i + 1); + } + + updatePriorities(); + // dumpQueue(); + // reorder the queue + qman->orderQueue(); + endResetModel(); +} + +void QueueManagerModel::moveTop(int row, int count) +{ + if (row < 0 || row >= qman->count()) + return; + + while (row > 0) { + for (int i = 0; i < count; i++) { + swapItems(row + i, row + i - 1); + } + row--; + } + + updatePriorities(); + // dumpQueue(); + // reorder the queue + qman->orderQueue(); + endResetModel(); +} + +void QueueManagerModel::moveBottom(int row, int count) +{ + if (row < 0 || row >= qman->count()) + return; + + while (row + count < queue.count()) { + for (int i = count - 1; i >= 0; i--) { + swapItems(row + i, row + i + 1); + } + row++; + } + + updatePriorities(); + // dumpQueue(); + // reorder the queue + qman->orderQueue(); + endResetModel(); +} + +void QueueManagerModel::dumpQueue() +{ + int idx = 0; + for (const Item &item : qAsConst(queue)) { + Out(SYS_GEN | LOG_DEBUG) << "Item " << idx << ": " << item.tc->getDisplayName() << " " << item.tc->getPriority() << endl; + idx++; + } +} + +void QueueManagerModel::updatePriorities() +{ + int idx = queue.size(); + for (const Item &i : qAsConst(queue)) + i.tc->setPriority(idx--); +} + +void QueueManagerModel::update() +{ + TimeStamp now = bt::CurrentTime(); + int r = 0; + for (Item &i : queue) { + bt::TorrentInterface *tc = i.tc; + if (!tc->getStats().running) { + if (i.stalled_time != -1) { + i.stalled_time = -1; + Q_EMIT dataChanged(createIndex(r, 3), createIndex(r, 3)); + } + } else { + Int64 stalled_time = 0; + if (tc->getStats().completed) + stalled_time = (now - tc->getStats().last_upload_activity_time) / 1000; + else + stalled_time = (now - tc->getStats().last_download_activity_time) / 1000; + + if (i.stalled_time != stalled_time) { + i.stalled_time = stalled_time; + Q_EMIT dataChanged(createIndex(r, 3), createIndex(r, 3)); + } + } + r++; + } +} + +QModelIndex QueueManagerModel::find(const QString &text) +{ + search_text = text; + if (text.isEmpty()) { + endResetModel(); + return QModelIndex(); + } + + int idx = 0; + for (const Item &i : qAsConst(queue)) { + bt::TorrentInterface *tc = i.tc; + if (tc->getDisplayName().contains(text, Qt::CaseInsensitive)) { + endResetModel(); + return index(idx, 0); + } + idx++; + } + + endResetModel(); + return QModelIndex(); +} + +bool QueueManagerModel::visible(const bt::TorrentInterface *tc) +{ + if (!show_uploads && tc->getStats().completed) + return false; + + if (!show_downloads && !tc->getStats().completed) + return false; + + if (!show_not_queud && !tc->isAllowedToStart()) + return false; + + return true; +} + +void QueueManagerModel::swapItems(int a, int b) +{ + if (a < 0 || a >= queue.count() || b < 0 || b >= queue.count()) + return; + + queue.swapItemsAt(a, b); +} + +} diff --git a/ktorrent/tools/queuemanagermodel.h b/ktorrent/tools/queuemanagermodel.h new file mode 100644 index 0000000..863d8b8 --- /dev/null +++ b/ktorrent/tools/queuemanagermodel.h @@ -0,0 +1,129 @@ +/* + SPDX-FileCopyrightText: 2007 Joris Guisson + SPDX-FileCopyrightText: 2007 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KTQUEUEMANAGERMODEL_H +#define KTQUEUEMANAGERMODEL_H + +#include +#include + +#include + +class QMimeData; + +namespace bt +{ +class TorrentInterface; +} + +namespace kt +{ +class QueueManager; + +/** + * @author Joris Guisson + * + * Model for the QM + */ +class QueueManagerModel : public QAbstractTableModel +{ + Q_OBJECT +public: + QueueManagerModel(QueueManager *qman, QObject *parent); + ~QueueManagerModel() override; + + void setShowUploads(bool on); + void setShowDownloads(bool on); + void setShowNotQueued(bool on); + + int rowCount(const QModelIndex &parent) const override; + int columnCount(const QModelIndex &parent) const override; + QVariant headerData(int section, Qt::Orientation orientation, int role) const override; + QVariant data(const QModelIndex &index, int role) const override; + bool removeRows(int row, int count, const QModelIndex &parent) override; + bool insertRows(int row, int count, const QModelIndex &parent) override; + Qt::ItemFlags flags(const QModelIndex &index) const override; + Qt::DropActions supportedDropActions() const override; + QStringList mimeTypes() const override; + QMimeData *mimeData(const QModelIndexList &indexes) const override; + bool dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) override; + + /** + * Move items one row up + * @param row The row of the item + * @param count The number of items to move + */ + void moveUp(int row, int count); + + /** + * Move items one row down + * @param row The row of the item + * @param count The number of items to move + */ + void moveDown(int row, int count); + + /** + * Move items to the top + * @param row The row of the item + * @param count The number of items to move + */ + void moveTop(int row, int count); + + /** + * Move items to the bottom + * @param row The row of the item + * @param count The number of items to move + */ + void moveBottom(int row, int count); + + /** + * Update the model + */ + void update(); + + /** + Given a search text find a matching torrent + */ + QModelIndex find(const QString &text); + +public Q_SLOTS: + void onTorrentAdded(bt::TorrentInterface *tc); + void onTorrentRemoved(bt::TorrentInterface *tc); + void onQueueOrdered(); + void onTorrentStatusChanged(bt::TorrentInterface *tc); + +private: + struct Item { + bt::TorrentInterface *tc; + bt::Int64 stalled_time; + + bool operator<(const Item &item) const + { + return tc->getPriority() < item.tc->getPriority(); + } + }; + + bool visible(const bt::TorrentInterface *tc); + void updateQueue(); + void swapItems(int a, int b); + void dumpQueue(); + void updatePriorities(); + void softReset(); + +private: + QueueManager *qman; + QList queue; + mutable QList dragged_items; + QString search_text; + + bool show_uploads; + bool show_downloads; + bool show_not_queud; +}; + +} + +#endif diff --git a/ktorrent/tools/queuemanagerwidget.cpp b/ktorrent/tools/queuemanagerwidget.cpp new file mode 100644 index 0000000..b36add8 --- /dev/null +++ b/ktorrent/tools/queuemanagerwidget.cpp @@ -0,0 +1,304 @@ +/* + SPDX-FileCopyrightText: 2007 Joris Guisson + SPDX-FileCopyrightText: 2007 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "queuemanagerwidget.h" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "queuemanagermodel.h" +#include +#include + +using namespace bt; + +namespace kt +{ +QueueManagerWidget::QueueManagerWidget(QueueManager *qman, QWidget *parent) + : QWidget(parent) + , qman(qman) +{ + QHBoxLayout *layout = new QHBoxLayout(this); + layout->setMargin(0); + layout->setSpacing(0); + QVBoxLayout *vbox = new QVBoxLayout(); + vbox->setMargin(0); + vbox->setSpacing(0); + view = new QTreeView(this); + view->setUniformRowHeights(true); + toolbar = new QToolBar(this); + toolbar->setOrientation(Qt::Vertical); + toolbar->setToolButtonStyle(Qt::ToolButtonIconOnly); + layout->addWidget(toolbar); + + search = new QLineEdit(this); + search->setPlaceholderText(i18n("Search")); + search->setClearButtonEnabled(true); + search->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); + connect(search, &QLineEdit::textChanged, this, &QueueManagerWidget::searchTextChanged); + search->hide(); + vbox->addWidget(search); + vbox->addWidget(view); + layout->addLayout(vbox); + + show_search = toolbar->addAction(QIcon::fromTheme(QStringLiteral("edit-find")), i18n("Show Search")); + show_search->setToolTip(i18n("Show or hide the search bar")); + show_search->setCheckable(true); + connect(show_search, &QAction::toggled, this, &QueueManagerWidget::showSearch); + + move_top = toolbar->addAction(QIcon::fromTheme(QStringLiteral("go-top")), i18n("Move Top"), this, &QueueManagerWidget::moveTopClicked); + move_top->setToolTip(i18n("Move a torrent to the top of the queue")); + + move_up = toolbar->addAction(QIcon::fromTheme(QStringLiteral("go-up")), i18n("Move Up"), this, &QueueManagerWidget::moveUpClicked); + move_up->setToolTip(i18n("Move a torrent up in the queue")); + + move_down = toolbar->addAction(QIcon::fromTheme(QStringLiteral("go-down")), i18n("Move Down"), this, &QueueManagerWidget::moveDownClicked); + move_down->setToolTip(i18n("Move a torrent down in the queue")); + + move_bottom = toolbar->addAction(QIcon::fromTheme(QStringLiteral("go-bottom")), i18n("Move Bottom"), this, &QueueManagerWidget::moveBottomClicked); + move_bottom->setToolTip(i18n("Move a torrent to the bottom of the queue")); + + show_downloads = toolbar->addAction(QIcon::fromTheme(QStringLiteral("arrow-down")), i18n("Show Downloads")); + show_downloads->setToolTip(i18n("Show all downloads")); + show_downloads->setCheckable(true); + connect(show_downloads, &QAction::toggled, this, &QueueManagerWidget::showDownloads); + + show_uploads = toolbar->addAction(QIcon::fromTheme(QStringLiteral("arrow-up")), i18n("Show Uploads")); + show_uploads->setToolTip(i18n("Show all uploads")); + show_uploads->setCheckable(true); + connect(show_uploads, &QAction::toggled, this, &QueueManagerWidget::showUploads); + + show_not_queued = toolbar->addAction(QIcon::fromTheme(QStringLiteral("kt-queue-manager")), i18n("Show Not Queued")); + show_not_queued->setToolTip(i18n("Show all not queued torrents")); + show_not_queued->setCheckable(true); + connect(show_not_queued, &QAction::toggled, this, &QueueManagerWidget::showNotQueued); + + model = new QueueManagerModel(qman, this); + view->setModel(model); + view->setRootIsDecorated(false); + view->setAlternatingRowColors(true); + view->setSelectionBehavior(QAbstractItemView::SelectRows); + view->setSortingEnabled(false); + view->setDragDropMode(QAbstractItemView::InternalMove); + view->setDragEnabled(true); + view->setAcceptDrops(true); + view->setDropIndicatorShown(true); + view->setAutoScroll(true); + view->setSelectionMode(QAbstractItemView::ContiguousSelection); + + connect(view->selectionModel(), &QItemSelectionModel::selectionChanged, this, &QueueManagerWidget::selectionChanged); + + updateButtons(); +} + +QueueManagerWidget::~QueueManagerWidget() +{ +} + +void QueueManagerWidget::onTorrentAdded(bt::TorrentInterface *tc) +{ + model->onTorrentAdded(tc); +} + +void QueueManagerWidget::onTorrentRemoved(bt::TorrentInterface *tc) +{ + model->onTorrentRemoved(tc); +} + +void QueueManagerWidget::moveUpClicked() +{ + const QModelIndexList sel = view->selectionModel()->selectedRows(); + QList rows; + for (const QModelIndex &idx : sel) + rows.append(idx.row()); + + if (rows.isEmpty() || rows.front() == 0) + return; + + model->moveUp(rows.front(), rows.count()); + + QItemSelection nsel; + int cols = model->columnCount(QModelIndex()); + QModelIndex top_left = model->index(rows.front() - 1, 0); + QModelIndex bottom_right = model->index(rows.back() - 1, cols - 1); + nsel.select(top_left, bottom_right); + view->selectionModel()->select(nsel, QItemSelectionModel::Select); + if (!indexVisible(top_left)) + view->scrollTo(top_left, QAbstractItemView::PositionAtCenter); + + updateButtons(); +} + +void QueueManagerWidget::moveDownClicked() +{ + const QModelIndexList sel = view->selectionModel()->selectedRows(); + QList rows; + for (const QModelIndex &idx : sel) + rows.append(idx.row()); + + int rowcount = model->rowCount(QModelIndex()); + if (rows.isEmpty() || rows.back() == rowcount - 1) + return; + + model->moveDown(rows.front(), rows.count()); + + QItemSelection nsel; + int cols = model->columnCount(QModelIndex()); + QModelIndex top_left = model->index(rows.front() + 1, 0); + QModelIndex bottom_right = model->index(rows.back() + 1, cols - 1); + nsel.select(top_left, bottom_right); + view->selectionModel()->select(nsel, QItemSelectionModel::Select); + if (!indexVisible(top_left)) + view->scrollTo(top_left, QAbstractItemView::PositionAtCenter); + + updateButtons(); +} + +void QueueManagerWidget::moveTopClicked() +{ + const QModelIndexList sel = view->selectionModel()->selectedRows(); + QList rows; + for (const QModelIndex &idx : sel) + rows.append(idx.row()); + + if (rows.isEmpty() || rows.front() == 0) + return; + + model->moveTop(rows.front(), rows.count()); + + QItemSelection nsel; + int cols = model->columnCount(QModelIndex()); + nsel.select(model->index(0, 0), model->index(rows.count() - 1, cols - 1)); + view->selectionModel()->select(nsel, QItemSelectionModel::Select); + view->scrollToTop(); + + updateButtons(); +} + +void QueueManagerWidget::moveBottomClicked() +{ + const QModelIndexList sel = view->selectionModel()->selectedRows(); + QList rows; + for (const QModelIndex &idx : sel) + rows.append(idx.row()); + + int rowcount = model->rowCount(QModelIndex()); + if (rows.isEmpty() || rows.back() == rowcount - 1) + return; + + model->moveBottom(rows.front(), rows.count()); + + QItemSelection nsel; + int cols = model->columnCount(QModelIndex()); + nsel.select(model->index(rowcount - rows.count(), 0), model->index(rowcount - 1, cols - 1)); + view->selectionModel()->select(nsel, QItemSelectionModel::Select); + view->scrollToBottom(); + + updateButtons(); +} + +void QueueManagerWidget::saveState(KSharedConfigPtr cfg) +{ + KConfigGroup g = cfg->group("QueueManagerWidget"); + QByteArray s = view->header()->saveState(); + g.writeEntry("view_state", s.toBase64()); + g.writeEntry("search_text", search->text()); + g.writeEntry("search_bar_visible", show_search->isChecked()); + g.writeEntry("show_uploads", show_uploads->isChecked()); + g.writeEntry("show_downloads", show_downloads->isChecked()); + g.writeEntry("show_not_queued", show_not_queued->isChecked()); +} + +void QueueManagerWidget::loadState(KSharedConfigPtr cfg) +{ + KConfigGroup g = cfg->group("QueueManagerWidget"); + QByteArray s = QByteArray::fromBase64(g.readEntry("view_state", QByteArray())); + if (!s.isEmpty()) + view->header()->restoreState(s); + + QString st = g.readEntry("search_text", QString()); + if (!st.isEmpty()) + search->setText(st); + + show_search->setChecked(g.readEntry("search_bar_visible", false)); + show_downloads->setChecked(g.readEntry("show_downloads", true)); + show_uploads->setChecked(g.readEntry("show_uploads", true)); + show_not_queued->setChecked(g.readEntry("show_not_queued", true)); +} + +void QueueManagerWidget::update() +{ + model->update(); +} + +void QueueManagerWidget::searchTextChanged(const QString &t) +{ + QModelIndex idx = model->find(t); + if (idx.isValid()) { + view->scrollTo(idx, QAbstractItemView::PositionAtCenter); + } +} + +void QueueManagerWidget::showSearch(bool on) +{ + search->setVisible(on); +} + +void QueueManagerWidget::showDownloads(bool on) +{ + model->setShowDownloads(on); +} + +void QueueManagerWidget::showUploads(bool on) +{ + model->setShowUploads(on); +} + +void QueueManagerWidget::showNotQueued(bool on) +{ + model->setShowNotQueued(on); +} + +bool QueueManagerWidget::indexVisible(const QModelIndex &idx) +{ + QRect r = view->visualRect(idx); + return view->viewport()->rect().contains(r); +} + +void QueueManagerWidget::selectionChanged(const QItemSelection &selected, const QItemSelection &deselected) +{ + Q_UNUSED(selected); + Q_UNUSED(deselected); + updateButtons(); +} + +void QueueManagerWidget::updateButtons() +{ + QModelIndexList idx = view->selectionModel()->selectedRows(); + if (idx.count() == 0) { + move_top->setEnabled(false); + move_up->setEnabled(false); + move_down->setEnabled(false); + move_bottom->setEnabled(false); + } else { + move_top->setEnabled(idx.front().row() != 0); + move_up->setEnabled(idx.front().row() != 0); + + int rows = model->rowCount(QModelIndex()); + move_down->setEnabled(idx.back().row() != rows - 1); + move_bottom->setEnabled(idx.back().row() != rows - 1); + } +} + +} diff --git a/ktorrent/tools/queuemanagerwidget.h b/ktorrent/tools/queuemanagerwidget.h new file mode 100644 index 0000000..aaa9811 --- /dev/null +++ b/ktorrent/tools/queuemanagerwidget.h @@ -0,0 +1,87 @@ +/* + SPDX-FileCopyrightText: 2007 Joris Guisson + SPDX-FileCopyrightText: 2007 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KTQUEUEMANAGERWIDGET_H +#define KTQUEUEMANAGERWIDGET_H + +#include +#include + +class QItemSelection; +class QModelIndex; +class QToolBar; +class QTreeView; +class QLineEdit; + +namespace bt +{ +class TorrentInterface; +} + +namespace kt +{ +class QueueManager; +class QueueManagerModel; + +/** + * @author Joris Guisson + * + * Widget for the QueueManager + */ +class QueueManagerWidget : public QWidget +{ + Q_OBJECT +public: + QueueManagerWidget(QueueManager *qman, QWidget *parent); + ~QueueManagerWidget() override; + + /// Save the widget state + void saveState(KSharedConfigPtr cfg); + /// Load the widget state + void loadState(KSharedConfigPtr cfg); + /// Update the widget + void update(); + +public Q_SLOTS: + void onTorrentAdded(bt::TorrentInterface *tc); + void onTorrentRemoved(bt::TorrentInterface *tc); + +private Q_SLOTS: + void moveUpClicked(); + void moveDownClicked(); + void moveTopClicked(); + void moveBottomClicked(); + void searchTextChanged(const QString &t); + void showSearch(bool on); + void showDownloads(bool on); + void showUploads(bool on); + void showNotQueued(bool on); + void selectionChanged(const QItemSelection &selected, const QItemSelection &deselected); + +private: + bool indexVisible(const QModelIndex &idx); + void updateButtons(); + +private: + QueueManagerModel *model; + QueueManager *qman; + QTreeView *view; + QToolBar *toolbar; + QLineEdit *search; + + QAction *show_search; + QAction *move_top; + QAction *move_up; + QAction *move_down; + QAction *move_bottom; + + QAction *show_uploads; + QAction *show_downloads; + QAction *show_not_queued; +}; +} + +#endif diff --git a/ktorrent/torrentactivity.cpp b/ktorrent/torrentactivity.cpp new file mode 100644 index 0000000..decf66c --- /dev/null +++ b/ktorrent/torrentactivity.cpp @@ -0,0 +1,260 @@ +/* + SPDX-FileCopyrightText: 2009 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "torrentactivity.h" + +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "core.h" +#include "groups/groupswitcher.h" +#include "groups/groupview.h" +#include "gui.h" +#include "tools/magnetview.h" +#include "tools/queuemanagerwidget.h" +#include "torrent/queuemanager.h" +#include "view/torrentsearchbar.h" +#include "view/view.h" +#include +#include +#include + +using namespace bt; + +namespace kt +{ +TorrentActivity::TorrentActivity(Core *core, GUI *gui, QWidget *parent) + : TorrentActivityInterface(i18n("Torrents"), QStringLiteral("torrents"), parent) + , core(core) + , gui(gui) +{ + setXMLGUIFile(QStringLiteral("kttorrentactivityui.rc")); + QWidget *view_part = new QWidget(this); + view = new View(core, gui, view_part); + connect(view, &View::currentTorrentChanged, this, &TorrentActivity::currentTorrentChanged); + search_bar = new TorrentSearchBar(view, view_part); + search_bar->setHidden(true); + + QVBoxLayout *layout = new QVBoxLayout(this); + layout->setSpacing(0); + layout->setMargin(0); + vsplit = new QSplitter(Qt::Vertical, this); + layout->addWidget(vsplit); + hsplit = new QSplitter(Qt::Horizontal, vsplit); + + group_switcher = new GroupSwitcher(view, core->getGroupManager(), this); + connect(core->getQueueManager(), &QueueManager::queueOrdered, this, &TorrentActivity::queueOrdered); + + QVBoxLayout *vlayout = new QVBoxLayout(view_part); + vlayout->setSpacing(0); + vlayout->setMargin(0); + vlayout->addWidget(group_switcher); + vlayout->addWidget(search_bar); + vlayout->addWidget(view); + + group_view = new GroupView(core->getGroupManager(), view, core, gui, hsplit); + group_view->setupActions(part()->actionCollection()); + connect(group_view, &GroupView::currentGroupChanged, group_switcher, &GroupSwitcher::currentGroupChanged); + connect(group_view, &GroupView::openTab, group_switcher, &GroupSwitcher::addTab); + + setupActions(); + + hsplit->addWidget(group_view); + hsplit->addWidget(view_part); + hsplit->setStretchFactor(0, 1); + hsplit->setStretchFactor(1, 3); + vsplit->addWidget(hsplit); + tool_views = new TabBarWidget(vsplit, this); + vsplit->setStretchFactor(0, 3); + vsplit->setStretchFactor(1, 1); + layout->addWidget(tool_views); + + qm = new QueueManagerWidget(core->getQueueManager(), this); + connect(core, &Core::torrentAdded, qm, &QueueManagerWidget::onTorrentAdded); + connect(core, &Core::torrentRemoved, qm, &QueueManagerWidget::onTorrentRemoved); + tool_views->addTab(qm, i18n("Queue Manager"), QStringLiteral("kt-queue-manager"), i18n("Widget to manage the torrent queue")); + + magnet_view = new MagnetView(core->getMagnetManager(), this); + tool_views->addTab(magnet_view, i18n("Magnet Downloader"), QStringLiteral("kt-magnet"), i18n("Displays the currently downloading magnet links")); + + QueueManager *qman = core->getQueueManager(); + connect(qman, &QueueManager::suspendStateChanged, this, &TorrentActivity::onSuspendedStateChanged); + + queue_suspend_action->setChecked(core->getSuspendedState()); +} + +TorrentActivity::~TorrentActivity() +{ +} + +void TorrentActivity::setupActions() +{ + KActionCollection *ac = part()->actionCollection(); + start_all_action = new QAction(QIcon::fromTheme(QStringLiteral("kt-start-all")), i18n("Start All"), this); + start_all_action->setToolTip(i18n("Start all torrents")); + connect(start_all_action, &QAction::triggered, this, &TorrentActivity::startAllTorrents); + ac->addAction(QStringLiteral("start_all"), start_all_action); + + stop_all_action = new QAction(QIcon::fromTheme(QStringLiteral("kt-stop-all")), i18n("Stop All"), this); + stop_all_action->setToolTip(i18n("Stop all torrents")); + connect(stop_all_action, &QAction::triggered, this, &TorrentActivity::stopAllTorrents); + ac->addAction(QStringLiteral("stop_all"), stop_all_action); + + queue_suspend_action = new KToggleAction(QIcon::fromTheme(QStringLiteral("kt-pause")), i18n("Suspend Torrents"), this); + ac->addAction(QStringLiteral("queue_suspend"), queue_suspend_action); + ac->setDefaultShortcut(queue_suspend_action, QKeySequence(Qt::SHIFT + Qt::Key_P)); + queue_suspend_action->setToolTip(i18n("Suspend all running torrents")); + // KF5 queue_suspend_action->setGlobalShortcut(QKeySequence(Qt::ALT + Qt::SHIFT + Qt::Key_P)); + connect(queue_suspend_action, &KToggleAction::toggled, this, &TorrentActivity::suspendQueue); + + show_group_view_action = new KToggleAction(QIcon::fromTheme(QStringLiteral("view-list-tree")), i18n("Group View"), this); + show_group_view_action->setToolTip(i18n("Show or hide the group view")); + connect(show_group_view_action, &QAction::toggled, this, &TorrentActivity::setGroupViewVisible); + ac->addAction(QStringLiteral("show_group_view"), show_group_view_action); + + filter_torrent_action = new QAction(i18n("Filter Torrents"), this); + filter_torrent_action->setToolTip(i18n("Filter torrents based on filter string")); + connect(filter_torrent_action, &QAction::triggered, search_bar, &TorrentSearchBar::showBar); + ac->addAction(QStringLiteral("filter_torrent"), filter_torrent_action); + ac->setDefaultShortcut(filter_torrent_action, QKeySequence(Qt::CTRL + Qt::Key_F)); + + view->setupActions(ac); +} + +void TorrentActivity::addToolWidget(QWidget *widget, const QString &text, const QString &icon, const QString &tooltip) +{ + tool_views->addTab(widget, text, icon, tooltip); +} + +void TorrentActivity::removeToolWidget(QWidget *widget) +{ + tool_views->removeTab(widget); +} + +void TorrentActivity::loadState(KSharedConfigPtr cfg) +{ + KConfigGroup g = cfg->group("TorrentActivitySplitters"); + if (vsplit) { + QByteArray data; + data = QByteArray::fromBase64(g.readEntry("vsplit", data)); + vsplit->restoreState(data); + } + + if (hsplit) { + QByteArray data; + data = QByteArray::fromBase64(g.readEntry("hsplit", data)); + hsplit->restoreState(data); + } + + search_bar->loadState(cfg); + group_view->loadState(cfg); + qm->loadState(cfg); + tool_views->loadState(cfg, QStringLiteral("TorrentActivityBottomTabBar")); + notifyViewListeners(view->getCurrentTorrent()); + magnet_view->loadState(cfg); + group_switcher->loadState(cfg); + + show_group_view_action->setChecked(!group_view->isHidden()); +} + +void TorrentActivity::saveState(KSharedConfigPtr cfg) +{ + search_bar->saveState(cfg); + group_view->saveState(cfg); + group_switcher->saveState(cfg); + qm->saveState(cfg); + tool_views->saveState(cfg, QStringLiteral("TorrentActivityBottomTabBar")); + magnet_view->saveState(cfg); + + KConfigGroup g = cfg->group("TorrentActivitySplitters"); + if (vsplit) { + QByteArray data = vsplit->saveState(); + g.writeEntry("vsplit", data.toBase64()); + } + + if (hsplit) { + QByteArray data = hsplit->saveState(); + g.writeEntry("hsplit", data.toBase64()); + } +} + +const TorrentInterface *TorrentActivity::getCurrentTorrent() const +{ + return view->getCurrentTorrent(); +} + +bt::TorrentInterface *TorrentActivity::getCurrentTorrent() +{ + return view->getCurrentTorrent(); +} + +void TorrentActivity::currentTorrentChanged(bt::TorrentInterface *tc) +{ + notifyViewListeners(tc); +} + +void TorrentActivity::updateActions() +{ + view->updateActions(); + bt::Uint32 nr = core->getNumTorrentsRunning(); + queue_suspend_action->setEnabled(core->getSuspendedState() || nr > 0); + start_all_action->setEnabled(core->getNumTorrentsNotRunning() > 0); + stop_all_action->setEnabled(nr > 0); +} + +void TorrentActivity::update() +{ + view->update(); + if (qm->isVisible()) + qm->update(); + group_switcher->update(); +} + +void TorrentActivity::setGroupViewVisible(bool visible) +{ + group_view->setVisible(visible); +} + +void TorrentActivity::startAllTorrents() +{ + core->startAll(); +} + +void TorrentActivity::stopAllTorrents() +{ + core->stopAll(); +} + +void TorrentActivity::suspendQueue(bool suspend) +{ + Out(SYS_GEN | LOG_NOTICE) << "Setting suspended state to " << suspend << endl; + core->setSuspendedState(suspend); + updateActions(); +} + +void TorrentActivity::onSuspendedStateChanged(bool suspended) +{ + queue_suspend_action->setChecked(suspended); +} + +Group *TorrentActivity::addNewGroup() +{ + return group_view->addNewGroup(); +} + +void TorrentActivity::queueOrdered() +{ + group_view->updateGroupCount(); + group_switcher->updateGroupCount(); +} + +} diff --git a/ktorrent/torrentactivity.h b/ktorrent/torrentactivity.h new file mode 100644 index 0000000..f180b71 --- /dev/null +++ b/ktorrent/torrentactivity.h @@ -0,0 +1,104 @@ +/* + SPDX-FileCopyrightText: 2009 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef TORRENTACTIVITY_H +#define TORRENTACTIVITY_H + +#include +#include + +class QAction; +class KToggleAction; + +namespace kt +{ +class MagnetView; +class GUI; +class Core; +class View; +class GroupView; +class QueueManagerWidget; +class TabBarWidget; +class Group; +class TorrentSearchBar; +class GroupSwitcher; + +/** + * Activity which manages torrents. + */ +class TorrentActivity : public TorrentActivityInterface +{ + Q_OBJECT +public: + TorrentActivity(Core *core, GUI *gui, QWidget *parent); + ~TorrentActivity() override; + + /// Get the group view + GroupView *getGroupView() + { + return group_view; + } + + void loadState(KSharedConfigPtr cfg); + void saveState(KSharedConfigPtr cfg); + const bt::TorrentInterface *getCurrentTorrent() const override; + bt::TorrentInterface *getCurrentTorrent() override; + void updateActions() override; + void addToolWidget(QWidget *widget, const QString &text, const QString &icon, const QString &tooltip) override; + void removeToolWidget(QWidget *widget) override; + Group *addNewGroup() override; + + /// Update the activity + void update(); + + /// Setup all actions + void setupActions(); + +public Q_SLOTS: + /** + * Called by the ViewManager when the current torrent has changed + * @param tc The torrent + * */ + void currentTorrentChanged(bt::TorrentInterface *tc); + + /** + Hide or show the group view + */ + void setGroupViewVisible(bool visible); + + /** + * The suspended state has changed + * @param suspended + */ + void onSuspendedStateChanged(bool suspended); + +private Q_SLOTS: + void startAllTorrents(); + void stopAllTorrents(); + void suspendQueue(bool suspend); + void queueOrdered(); + +private: + Core *core; + GUI *gui; + View *view; + GroupView *group_view; + GroupSwitcher *group_switcher; + QueueManagerWidget *qm; + QSplitter *hsplit; + QSplitter *vsplit; + TabBarWidget *tool_views; + MagnetView *magnet_view; + TorrentSearchBar *search_bar; + + QAction *start_all_action; + QAction *stop_all_action; + KToggleAction *queue_suspend_action; + QAction *show_group_view_action; + QAction *filter_torrent_action; +}; +} + +#endif // TORRENTACTIVITY_H diff --git a/ktorrent/trayicon.cpp b/ktorrent/trayicon.cpp new file mode 100644 index 0000000..0c53cff --- /dev/null +++ b/ktorrent/trayicon.cpp @@ -0,0 +1,423 @@ +/* + SPDX-FileCopyrightText: 2005-2007 Joris Guisson + SPDX-FileCopyrightText: 2005-2007 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "trayicon.h" + +#include + +#include +#include +#include +#include + +#include +#include +#include + +#include "core.h" +#include "gui.h" +#include +#include +#include +#include +#include +#include + +using namespace bt; + +namespace kt +{ +TrayIcon::TrayIcon(Core *core, GUI *parent) + : QObject(parent) + , core(core) + , mwnd(parent) + , previousDownloadHeight(0) + , previousUploadHeight(0) + , max_upload_rate(nullptr) + , max_download_rate(nullptr) + , status_notifier_item(nullptr) + , queue_suspended(false) + , menu(nullptr) +{ + connect(core, &Core::openedSilently, this, &TrayIcon::torrentSilentlyOpened); + connect(core, &Core::finished, this, &TrayIcon::finished); + connect(core, &Core::torrentStoppedByError, this, &TrayIcon::torrentStoppedByError); + connect(core, &Core::maxShareRatioReached, this, &TrayIcon::maxShareRatioReached); + connect(core, &Core::maxSeedTimeReached, this, &TrayIcon::maxSeedTimeReached); + connect(core, &Core::corruptedData, this, &TrayIcon::corruptedData); + connect(core, &Core::queuingNotPossible, this, &TrayIcon::queuingNotPossible); + connect(core, &Core::canNotStart, this, &TrayIcon::canNotStart); + connect(core, &Core::lowDiskSpace, this, &TrayIcon::lowDiskSpace); + connect(core, &Core::canNotLoadSilently, this, &TrayIcon::cannotLoadTorrentSilently); + connect(core, &Core::dhtNotEnabled, this, &TrayIcon::dhtNotEnabled); + connect(core->getQueueManager(), &QueueManager::suspendStateChanged, this, &TrayIcon::suspendStateChanged); + + suspendStateChanged(core->getQueueManager()->getSuspendedState()); +} + +TrayIcon::~TrayIcon() +{ +} + +void TrayIcon::hide() +{ + if (!status_notifier_item) + return; + + delete status_notifier_item; + status_notifier_item = nullptr; + menu = nullptr; + max_download_rate = max_upload_rate = nullptr; +} + +void TrayIcon::show() +{ + if (status_notifier_item) { + suspendStateChanged(core->getQueueManager()->getSuspendedState()); + return; + } + + status_notifier_item = new KStatusNotifierItem(mwnd); + connect(status_notifier_item, &KStatusNotifierItem::secondaryActivateRequested, this, &TrayIcon::secondaryActivate); + + menu = status_notifier_item->contextMenu(); + + max_upload_rate = new SetMaxRate(core, SetMaxRate::UPLOAD, menu); + max_upload_rate->setTitle(i18n("Set max upload speed")); + max_download_rate = new SetMaxRate(core, SetMaxRate::DOWNLOAD, menu); + max_download_rate->setTitle(i18n("Set max download speed")); + menu->addMenu(max_download_rate); + menu->addMenu(max_upload_rate); + menu->addSeparator(); + + KActionCollection *ac = mwnd->getTorrentActivity()->part()->actionCollection(); + menu->addAction(ac->action(QStringLiteral("start_all"))); + menu->addAction(ac->action(QStringLiteral("stop_all"))); + menu->addAction(ac->action(QStringLiteral("queue_suspend"))); + menu->addSeparator(); + + ac = mwnd->actionCollection(); + menu->addAction(ac->action(QStringLiteral("paste_url"))); + menu->addAction(ac->action(QString::fromUtf8(KStandardAction::name(KStandardAction::Open)))); + menu->addSeparator(); + menu->addAction(ac->action(QString::fromUtf8(KStandardAction::name(KStandardAction::Preferences)))); + menu->addSeparator(); + + status_notifier_item->setIconByName(QStringLiteral("ktorrent")); + status_notifier_item->setCategory(KStatusNotifierItem::ApplicationStatus); + status_notifier_item->setStatus(KStatusNotifierItem::Passive); + status_notifier_item->setStandardActionsEnabled(true); + status_notifier_item->setContextMenu(menu); + + queue_suspended = core->getQueueManager()->getSuspendedState(); + if (queue_suspended) + status_notifier_item->setOverlayIconByName(QStringLiteral("kt-pause")); +} + +void TrayIcon::updateStats(const CurrentStats &stats) +{ + if (!status_notifier_item) + return; + + status_notifier_item->setStatus(core->getQueueManager()->getNumRunning(QueueManager::DOWNLOADS) > 0 ? KStatusNotifierItem::Active + : KStatusNotifierItem::Passive); + QString tip = i18n( + "Download speed: %1
" + "Upload speed: %2
" + "Received: %3
" + "Transmitted: %4", + BytesPerSecToString((double)stats.download_speed), + BytesPerSecToString((double)stats.upload_speed), + BytesToString(stats.bytes_downloaded), + BytesToString(stats.bytes_uploaded)); + + status_notifier_item->setToolTip(QStringLiteral("ktorrent"), i18n("Status"), tip); +} + +void TrayIcon::showPassivePopup(const QString &msg, const QString &title) +{ + if (status_notifier_item) + status_notifier_item->showMessage(title, msg, QStringLiteral("ktorrent")); +} + +void TrayIcon::cannotLoadTorrentSilently(const QString &msg) +{ + if (!Settings::showPopups()) + return; + + KNotification::event(QStringLiteral("CannotLoadSilently"), msg, QPixmap(), mwnd); +} + +void TrayIcon::dhtNotEnabled(const QString &msg) +{ + if (!Settings::showPopups()) + return; + + KNotification::event(QStringLiteral("DHTNotEnabled"), msg, QPixmap(), mwnd); +} + +void TrayIcon::torrentSilentlyOpened(bt::TorrentInterface *tc) +{ + if (!Settings::showPopups()) + return; + + QString msg = i18n("%1 was silently opened.", tc->getDisplayName()); + KNotification::event(QStringLiteral("TorrentSilentlyOpened"), msg, QPixmap(), mwnd); +} + +void TrayIcon::finished(bt::TorrentInterface *tc) +{ + if (!Settings::showPopups()) + return; + + const TorrentStats &s = tc->getStats(); + double speed_up = (double)s.bytes_uploaded; + double speed_down = (double)(s.bytes_downloaded - s.imported_bytes); + + QString msg = i18n( + "%1 has completed downloading." + "
Average speed: %2 DL / %3 UL.", + tc->getDisplayName(), + BytesPerSecToString(speed_down / tc->getRunningTimeDL()), + BytesPerSecToString(speed_up / tc->getRunningTimeUL())); + + KNotification::event(QStringLiteral("TorrentFinished"), msg, QPixmap(), mwnd); +} + +void TrayIcon::maxShareRatioReached(bt::TorrentInterface *tc) +{ + if (!Settings::showPopups()) + return; + + const TorrentStats &s = tc->getStats(); + double speed_up = (double)s.bytes_uploaded; + + QString msg = i18n( + "%1 has reached its maximum share ratio of %2 and has been stopped." + "
Uploaded %3 at an average speed of %4.", + tc->getDisplayName(), + QLocale().toString(s.max_share_ratio, 'f', 2), + BytesToString(s.bytes_uploaded), + BytesPerSecToString(speed_up / tc->getRunningTimeUL())); + + KNotification::event(QStringLiteral("MaxShareRatioReached"), msg, QPixmap(), mwnd); +} + +void TrayIcon::maxSeedTimeReached(bt::TorrentInterface *tc) +{ + if (!Settings::showPopups()) + return; + + const TorrentStats &s = tc->getStats(); + double speed_up = (double)s.bytes_uploaded; + + QString msg = i18n( + "%1 has reached its maximum seed time of %2 hours and has been stopped." + "
Uploaded %3 at an average speed of %4.", + tc->getDisplayName(), + QLocale().toString(s.max_seed_time, 'f', 2), + BytesToString(s.bytes_uploaded), + BytesPerSecToString(speed_up / tc->getRunningTimeUL())); + + KNotification::event(QStringLiteral("MaxSeedTimeReached"), msg, QPixmap(), mwnd); +} + +void TrayIcon::torrentStoppedByError(bt::TorrentInterface *tc, QString msg) +{ + if (!Settings::showPopups()) + return; + + QString err_msg = i18n("%1 has been stopped with the following error:
%2", tc->getDisplayName(), msg); + + KNotification::event(QStringLiteral("TorrentStoppedByError"), err_msg, QPixmap(), mwnd); +} + +void TrayIcon::corruptedData(bt::TorrentInterface *tc) +{ + if (!Settings::showPopups()) + return; + + QString err_msg = i18n( + "Corrupted data has been found in the torrent %1" + "
It would be a good idea to do a data integrity check on the torrent.", + tc->getDisplayName()); + + KNotification::event(QStringLiteral("CorruptedData"), err_msg, QPixmap(), mwnd); +} + +void TrayIcon::queuingNotPossible(bt::TorrentInterface *tc) +{ + if (!Settings::showPopups()) + return; + + const TorrentStats &s = tc->getStats(); + + QString msg; + + if (tc->overMaxRatio()) + msg = i18n( + "%1 has reached its maximum share ratio of %2 and cannot be enqueued. " + "
Remove the limit manually if you want to continue seeding.", + tc->getDisplayName(), + QLocale().toString(s.max_share_ratio, 'f', 2)); + else + msg = i18n( + "%1 has reached its maximum seed time of %2 hours and cannot be enqueued. " + "
Remove the limit manually if you want to continue seeding.", + tc->getDisplayName(), + QLocale().toString(s.max_seed_time, 'f', 2)); + + KNotification::event(QStringLiteral("QueueNotPossible"), msg, QPixmap(), mwnd); +} + +void TrayIcon::canNotStart(bt::TorrentInterface *tc, bt::TorrentStartResponse reason) +{ + if (!Settings::showPopups()) + return; + + QString msg = i18n("Cannot start %1:
", tc->getDisplayName()); + switch (reason) { + case bt::QM_LIMITS_REACHED: + if (tc->getStats().bytes_left_to_download == 0) { + // is a seeder + msg += i18np("Cannot seed more than 1 torrent.
", "Cannot seed more than %1 torrents.
", Settings::maxSeeds()); + } else { + msg += i18np("Cannot download more than 1 torrent.
", "Cannot download more than %1 torrents.
", Settings::maxDownloads()); + } + msg += i18n("Go to Settings -> Configure KTorrent, if you want to change the limits."); + KNotification::event(QStringLiteral("CannotStart"), msg, QPixmap(), mwnd); + break; + case bt::NOT_ENOUGH_DISKSPACE: + msg += i18n("There is not enough diskspace available."); + KNotification::event(QStringLiteral("CannotStart"), msg, QPixmap(), mwnd); + break; + default: + break; + } +} + +void TrayIcon::lowDiskSpace(bt::TorrentInterface *tc, bool stopped) +{ + if (!Settings::showPopups()) + return; + + QString msg = i18n("Your disk is running out of space.
%1 is being downloaded to '%2'.", tc->getDisplayName(), tc->getDataDir()); + + if (stopped) + msg.prepend(i18n("Torrent has been stopped.
")); + + KNotification::event(QStringLiteral("LowDiskSpace"), msg); +} + +void TrayIcon::updateMaxRateMenus() +{ + if (max_download_rate && max_upload_rate) { + max_upload_rate->update(); + max_download_rate->update(); + } +} + +SetMaxRate::SetMaxRate(Core *core, Type t, QWidget *parent) + : QMenu(parent) +{ + setIcon(t == UPLOAD ? QIcon::fromTheme(QStringLiteral("kt-set-max-upload-speed")) : QIcon::fromTheme(QStringLiteral("kt-set-max-download-speed"))); + m_core = core; + type = t; + makeMenu(); + connect(this, &SetMaxRate::triggered, this, &SetMaxRate::onTriggered); + connect(this, &SetMaxRate::aboutToShow, this, &SetMaxRate::update); +} + +SetMaxRate::~SetMaxRate() +{ +} + +void SetMaxRate::makeMenu() +{ + int rate = (type == UPLOAD) ? net::SocketMonitor::getUploadCap() / 1024 : net::SocketMonitor::getDownloadCap() / 1024; + int maxBandwidth = (rate > 0) ? rate : (type == UPLOAD) ? 0 : 20; + int delta = 0; + int maxBandwidthRounded; + + if (type == UPLOAD) + setTitle(i18n("Upload speed limit in KiB/s")); + else + setTitle(i18n("Download speed limit in KiB/s")); + + unlimited = addAction(i18n("Unlimited")); + unlimited->setCheckable(true); + unlimited->setChecked(rate == 0); + + if ((maxBandwidth % 5) >= 3) + maxBandwidthRounded = maxBandwidth + 5 - (maxBandwidth % 5); + else + maxBandwidthRounded = maxBandwidth - (maxBandwidth % 5); + + QList values; + for (int i = 0; i < 15; i++) { + if (delta == 0) + values.append(maxBandwidth); + else { + if ((maxBandwidth % 5) != 0) { + values.append(maxBandwidthRounded - delta); + values.append(maxBandwidthRounded + delta); + } else { + values.append(maxBandwidth - delta); + values.append(maxBandwidth + delta); + } + } + + delta += (delta >= 50) ? 50 : (delta >= 20) ? 10 : 5; + } + + std::sort(values.begin(), values.end()); + for (int v : qAsConst(values)) { + if (v >= 1) { + QAction *act = addAction(QString::number(v)); + act->setCheckable(true); + act->setChecked(rate == v); + } + } +} + +void SetMaxRate::update() +{ + clear(); + makeMenu(); +} + +void SetMaxRate::onTriggered(QAction *act) +{ + int rate; + if (act == unlimited) + rate = 0; + else + rate = act->text().remove(QLatin1Char('&')).toInt(); // remove ampersands + + if (type == UPLOAD) { + Settings::setMaxUploadRate(rate); + net::SocketMonitor::setUploadCap(Settings::maxUploadRate() * 1024); + } else { + Settings::setMaxDownloadRate(rate); + net::SocketMonitor::setDownloadCap(Settings::maxDownloadRate() * 1024); + } + Settings::self()->save(); +} + +void TrayIcon::suspendStateChanged(bool suspended) +{ + queue_suspended = suspended; + if (status_notifier_item) + status_notifier_item->setOverlayIconByName(suspended ? QStringLiteral("kt-pause") : QString()); +} + +void TrayIcon::secondaryActivate(const QPoint &pos) +{ + Q_UNUSED(pos); + core->setSuspendedState(!core->getSuspendedState()); +} + +} diff --git a/ktorrent/trayicon.h b/ktorrent/trayicon.h new file mode 100644 index 0000000..43776b3 --- /dev/null +++ b/ktorrent/trayicon.h @@ -0,0 +1,176 @@ +/* + SPDX-FileCopyrightText: 2005-2007 Joris Guisson + SPDX-FileCopyrightText: 2005-2007 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef TRAYICON_H +#define TRAYICON_H + +#include +#include + +#include +#include + +using namespace bt; + +class QString; + +namespace kt +{ +struct CurrentStats; +class Core; +class SetMaxRate; +class GUI; + +struct TrayStats { + bt::Uint32 download_speed; + bt::Uint32 upload_speed; + bt::Uint64 bytes_downloaded; + bt::Uint64 bytes_uploaded; +}; + +/** + * @author Joris Guisson + * @author Ivan Vasic + */ + +class TrayIcon : public QObject +{ + Q_OBJECT +public: + TrayIcon(Core *tc, GUI *parent); + ~TrayIcon() override; + + /// Update stats for system tray icon + void updateStats(const CurrentStats &stats); + + /// Update the max rate menus + void updateMaxRateMenus(); + + /// Show the icon + void show(); + + /// Hide the icon + void hide(); + + /// Get the co + QMenu *contextMenu(); + +private: + void showPassivePopup(const QString &msg, const QString &titile); + +private Q_SLOTS: + /** + * Show a passive popup, that a torrent has been silently added. + * @param tc The torrent + */ + void torrentSilentlyOpened(bt::TorrentInterface *tc); + + /** + * Show a passive popup, that the torrent has stopped downloading. + * @param tc The torrent + */ + void finished(bt::TorrentInterface *tc); + + /** + * Show a passive popup that a torrent has reached it's max share ratio. + * @param tc The torrent + */ + void maxShareRatioReached(bt::TorrentInterface *tc); + + /** + * Show a passive popup that a torrent has reached it's max seed time + * @param tc The torrent + */ + void maxSeedTimeReached(bt::TorrentInterface *tc); + + /** + * Show a passive popup when a torrent has been stopped by an error. + * @param tc The torrent + * @param msg Error message + */ + void torrentStoppedByError(bt::TorrentInterface *tc, QString msg); + + /** + * Corrupted data has been found. + * @param tc The torrent + */ + void corruptedData(bt::TorrentInterface *tc); + + /** + * User tried to enqueue a torrent that has reached max share ratio or max seed time + * Show passive popup message. + */ + void queuingNotPossible(bt::TorrentInterface *tc); + + /** + * We failed to start a torrent + * @param tc The torrent + * @param reason The reason it failed + */ + void canNotStart(bt::TorrentInterface *tc, bt::TorrentStartResponse reason); + + /// Shows passive popup message + void lowDiskSpace(bt::TorrentInterface *tc, bool stopped); + + /** + * A torrent could not be loaded silently. + * @param msg Message to show + */ + void cannotLoadTorrentSilently(const QString &msg); + + /** + The QM changes suspended state. + */ + void suspendStateChanged(bool suspended); + + /** + Show a warning message + */ + void dhtNotEnabled(const QString &msg); + +private Q_SLOTS: + void secondaryActivate(const QPoint &pos); + +private: + Core *core; + GUI *mwnd; + int previousDownloadHeight; + int previousUploadHeight; + SetMaxRate *max_upload_rate; + SetMaxRate *max_download_rate; + KStatusNotifierItem *status_notifier_item; + bool queue_suspended; + QMenu *menu; +}; + +class SetMaxRate : public QMenu +{ + Q_OBJECT +public: + enum Type { + UPLOAD, + DOWNLOAD, + }; + SetMaxRate(Core *tc, Type t, QWidget *parent); + ~SetMaxRate() override; + +public Q_SLOTS: + void update(); + +private: + void makeMenu(); + +private Q_SLOTS: + void onTriggered(QAction *act); + +private: + Core *m_core; + Type type; + QAction *unlimited; +}; +} + +#endif diff --git a/ktorrent/view/propertiesdlg.cpp b/ktorrent/view/propertiesdlg.cpp new file mode 100644 index 0000000..b1d8695 --- /dev/null +++ b/ktorrent/view/propertiesdlg.cpp @@ -0,0 +1,67 @@ +/* + SPDX-FileCopyrightText: 2010 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "propertiesdlg.h" +#include +#include +#include + +namespace kt +{ +PropertiesDlg::PropertiesDlg(bt::TorrentInterface *tc, QWidget *parent) + : QDialog(parent) + , tc(tc) +{ + setupUi(this); + setWindowTitle(i18n("Torrent Settings")); + + connect(m_buttonBox, &QDialogButtonBox::accepted, this, &PropertiesDlg::accept); + connect(m_buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); + + QString folder = tc->getMoveWhenCompletedDir(); + if (QFile::exists(folder)) { + move_on_completion_enabled->setChecked(true); + move_on_completion_url->setUrl(QUrl::fromLocalFile(folder)); + move_on_completion_url->setEnabled(true); + } else { + move_on_completion_enabled->setChecked(false); + move_on_completion_url->setEnabled(false); + } + + // disable DHT and PEX if they are globally disabled + const bt::TorrentStats &s = tc->getStats(); + dht->setEnabled(!s.priv_torrent); + pex->setEnabled(!s.priv_torrent); + dht->setChecked(!s.priv_torrent && tc->isFeatureEnabled(bt::DHT_FEATURE)); + pex->setChecked(!s.priv_torrent && tc->isFeatureEnabled(bt::UT_PEX_FEATURE)); + + superseeding->setChecked(s.superseeding); + connect(move_on_completion_enabled, &QCheckBox::toggled, this, &PropertiesDlg::moveOnCompletionEnabled); +} + +PropertiesDlg::~PropertiesDlg() +{ +} + +void PropertiesDlg::moveOnCompletionEnabled(bool on) +{ + move_on_completion_url->setEnabled(on); +} + +void PropertiesDlg::accept() +{ + if (move_on_completion_enabled->isChecked()) { + tc->setMoveWhenCompletedDir(move_on_completion_url->url().toLocalFile()); + } else { + tc->setMoveWhenCompletedDir(QString()); + } + + tc->setFeatureEnabled(bt::DHT_FEATURE, dht->isChecked()); + tc->setFeatureEnabled(bt::UT_PEX_FEATURE, pex->isChecked()); + tc->setSuperSeeding(superseeding->isChecked()); + QDialog::accept(); +} + +} diff --git a/ktorrent/view/propertiesdlg.h b/ktorrent/view/propertiesdlg.h new file mode 100644 index 0000000..4f9167a --- /dev/null +++ b/ktorrent/view/propertiesdlg.h @@ -0,0 +1,41 @@ +/* + SPDX-FileCopyrightText: 2010 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KT_PROPERTIESDLG_H +#define KT_PROPERTIESDLG_H + +#include "ui_propertiesdlg.h" +#include + +namespace bt +{ +class TorrentInterface; +} + +namespace kt +{ +/** + Extender which shows properties about a torrent. +*/ +class PropertiesDlg : public QDialog, public Ui_PropertiesDlg +{ + Q_OBJECT +public: + PropertiesDlg(bt::TorrentInterface *tc, QWidget *parent); + ~PropertiesDlg() override; + +public Q_SLOTS: + void moveOnCompletionEnabled(bool on); + +private: + void accept() override; + +private: + bt::TorrentInterface *tc; +}; + +} + +#endif // KT_PROPERTIESEXTENDER_H diff --git a/ktorrent/view/propertiesdlg.ui b/ktorrent/view/propertiesdlg.ui new file mode 100644 index 0000000..d2e96c7 --- /dev/null +++ b/ktorrent/view/propertiesdlg.ui @@ -0,0 +1,94 @@ + + + PropertiesDlg + + + + 0 + 0 + 406 + 247 + + + + Properties + + + + + + DHT is a distributed database which can be used to find more peers for a torrent. + + + Use DHT to find more peers + + + + + + + Peer exchange results in more peers being found, by exchanging information about peers with other peers. + + + Use peer exchange to find more peers + + + + + + + <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Superseeding mode, is a special seeding mode which allows you to achieve much higher seeding efficiencies. This results in less bandwidth used to spread a torrent.</p> +<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"></p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-weight:600;">Note: </span>This should only be used if the swarm has not fully been established. Once there are multiple active seeders, this mode becomes useless.</p> + + + Use superseeding mode for seeding + + + + + + + Move the files of the torrent to a different directory when it has finished downloading. + + + Move when completed to: + + + + + + + Location to move the files to. + + + KFile::Directory|KFile::ExistingOnly|KFile::LocalOnly + + + + + + + Qt::Horizontal + + + + + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + KUrlRequester + QWidget +
kurlrequester.h
+
+
+ + +
diff --git a/ktorrent/view/scanextender.cpp b/ktorrent/view/scanextender.cpp new file mode 100644 index 0000000..d316d2e --- /dev/null +++ b/ktorrent/view/scanextender.cpp @@ -0,0 +1,132 @@ +/* + SPDX-FileCopyrightText: 2009 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "scanextender.h" + +#include +#include +#include + +#include +#include +#include + +namespace kt +{ +ScanExtender::ScanExtender(bt::Job *job, QWidget *parent) + : JobProgressWidget(job, parent) +{ + setupUi(this); + + bt::DataCheckerJob *dcj = (bt::DataCheckerJob *)job; + setAutomaticRemove(dcj->isAutoImport()); + connect(job, &bt::Job::result, this, &ScanExtender::finished); + + KGuiItem::assign(cancel_button, KStandardGuiItem::cancel()); + KGuiItem::assign(close_button, KStandardGuiItem::close()); + close_button->setEnabled(false); + connect(close_button, &QPushButton::clicked, this, &ScanExtender::closeRequested); + connect(cancel_button, &QPushButton::clicked, this, &ScanExtender::cancelPressed); + + progress_bar->setFormat(i18n("Checked %v of %m chunks")); + progress_bar->setValue(0); + progress_bar->setMaximum(tc->getStats().total_chunks); + + error_msg->clear(); + error_msg->hide(); + + QFont font = chunks_failed->font(); + font.setBold(true); + chunks_failed->setFont(font); + chunks_found->setFont(font); + chunks_downloaded->setFont(font); + chunks_not_downloaded->setFont(font); +} + +ScanExtender::~ScanExtender() +{ +} + +void ScanExtender::description(const QString &title, const QPair &field1, const QPair &field2) +{ + Q_UNUSED(title); + chunks_failed->setText(field1.first); + chunks_found->setText(field1.second); + chunks_downloaded->setText(field2.first); + chunks_not_downloaded->setText(field2.second); + + if (error_msg->isVisible()) { + error_msg->hide(); + Q_EMIT resized(this); + } +} + +void ScanExtender::processedAmount(KJob::Unit unit, qulonglong amount) +{ + Q_UNUSED(unit); + progress_bar->setValue(amount); +} + +void ScanExtender::totalAmount(KJob::Unit unit, qulonglong amount) +{ + Q_UNUSED(unit); + progress_bar->setMaximum(amount); +} + +void ScanExtender::infoMessage(const QString &plain, const QString &rich) +{ + Q_UNUSED(rich); + error_msg->setText(plain); + error_msg->show(); + Q_EMIT resized(this); +} + +void ScanExtender::warning(const QString &plain, const QString &rich) +{ + Q_UNUSED(rich); + Q_UNUSED(plain); +} + +void ScanExtender::speed(long unsigned int value) +{ + Q_UNUSED(value); +} + +void ScanExtender::percent(long unsigned int percent) +{ + Q_UNUSED(percent); +} + +void ScanExtender::finished(KJob *j) +{ + progress_bar->setValue(progress_bar->maximum()); + progress_bar->setEnabled(false); + cancel_button->setDisabled(true); + close_button->setEnabled(true); + + if (j->error() && !j->errorText().isEmpty()) { + error_msg->show(); + error_msg->setText(i18n("%1", j->errorText())); + Q_EMIT resized(this); + } +} + +void ScanExtender::cancelPressed() +{ + if (job) + job->kill(false); +} + +void ScanExtender::closeRequested() +{ + closeRequest(this); +} + +bool ScanExtender::similar(Extender *ext) const +{ + return qobject_cast(ext) != nullptr; +} + +} diff --git a/ktorrent/view/scanextender.h b/ktorrent/view/scanextender.h new file mode 100644 index 0000000..1e4b49c --- /dev/null +++ b/ktorrent/view/scanextender.h @@ -0,0 +1,43 @@ +/* + SPDX-FileCopyrightText: 2009 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KT_SCANEXTENDER_H +#define KT_SCANEXTENDER_H + +#include +#include + +#include "ui_scanextender.h" +#include + +namespace kt +{ +/** + Extender widget which displays the results of a data scan +*/ +class ScanExtender : public JobProgressWidget, public Ui_ScanExtender +{ + Q_OBJECT +public: + ScanExtender(bt::Job *job, QWidget *parent); + ~ScanExtender() override; + + void description(const QString &title, const QPair &field1, const QPair &field2) override; + void infoMessage(const QString &plain, const QString &rich) override; + void warning(const QString &plain, const QString &rich) override; + void percent(long unsigned int percent) override; + void speed(long unsigned int value) override; + void processedAmount(KJob::Unit unit, qulonglong amount) override; + void totalAmount(KJob::Unit unit, qulonglong amount) override; + bool similar(Extender *ext) const override; + +private Q_SLOTS: + void cancelPressed(); + void finished(KJob *j); + void closeRequested(); +}; +} + +#endif // KT_SCANEXTENDER_H diff --git a/ktorrent/view/scanextender.ui b/ktorrent/view/scanextender.ui new file mode 100644 index 0000000..bc37e55 --- /dev/null +++ b/ktorrent/view/scanextender.ui @@ -0,0 +1,238 @@ + + + ScanExtender + + + + 0 + 0 + 596 + 88 + + + + + + + + + + + Found: + + + false + + + + + + + + 0 + 0 + + + + + 50 + 0 + + + + The number of chunks which were not downloaded but have been found anyway. + + + QFrame::NoFrame + + + 0 + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + false + + + + + + + + + + + Failed: + + + false + + + + + + + + 50 + 0 + + + + The number of chunks which have been downloaded, and which are not OK. + + + QFrame::NoFrame + + + QFrame::Plain + + + 0 + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + false + + + + + + + + + + + Not downloaded: + + + + + + + + 0 + 0 + + + + + 50 + 0 + + + + The number of chunks which have not been downloaded. + + + QFrame::NoFrame + + + QFrame::Plain + + + 0 + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + false + + + + + + + + + + + Downloaded: + + + + + + + + 50 + 0 + + + + The number of chunks which have been downloaded, and which are OK. + + + QFrame::NoFrame + + + QFrame::Plain + + + 0 + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + false + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + 24 + + + + + + + Cancel + + + + + + + Close + + + + + + + + + Errors: + + + + + + + + diff --git a/ktorrent/view/torrentsearchbar.cpp b/ktorrent/view/torrentsearchbar.cpp new file mode 100644 index 0000000..6c587be --- /dev/null +++ b/ktorrent/view/torrentsearchbar.cpp @@ -0,0 +1,88 @@ +/* + SPDX-FileCopyrightText: 2011 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "torrentsearchbar.h" + +#include +#include +#include + +#include +#include + +#include "view.h" + +namespace kt +{ +TorrentSearchBar::TorrentSearchBar(View *view, QWidget *parent) + : QWidget(parent) +{ + QHBoxLayout *layout = new QHBoxLayout(this); + layout->setSpacing(0); + layout->setMargin(0); + + hide_search_bar = new QToolButton(this); + hide_search_bar->setIcon(QIcon::fromTheme(QStringLiteral("window-close"))); + hide_search_bar->setAutoRaise(true); + connect(hide_search_bar, &QToolButton::clicked, this, &TorrentSearchBar::hideBar); + connect(this, &TorrentSearchBar::filterBarHidden, view, &View::setFilterString); + + search_bar = new QLineEdit(this); + search_bar->setClearButtonEnabled(true); + search_bar->setPlaceholderText(i18n("Filter...")); + connect(search_bar, &QLineEdit::textChanged, view, &View::setFilterString); + connect(this, &TorrentSearchBar::filterBarShown, view, &View::setFilterString); + + layout->addWidget(hide_search_bar); + layout->addWidget(search_bar); + + search_bar->installEventFilter(this); +} + +TorrentSearchBar::~TorrentSearchBar() +{ +} + +void TorrentSearchBar::showBar() +{ + show(); + search_bar->setFocus(); + Q_EMIT filterBarShown(search_bar->text()); +} + +void TorrentSearchBar::hideBar() +{ + hide(); + Q_EMIT filterBarHidden(QString()); +} + +bool TorrentSearchBar::eventFilter(QObject *obj, QEvent *ev) +{ + if (ev->type() == QEvent::KeyPress && ((QKeyEvent *)ev)->key() == Qt::Key_Escape) + hideBar(); + + return QObject::eventFilter(obj, ev); +} + +void TorrentSearchBar::loadState(KSharedConfigPtr cfg) +{ + KConfigGroup g = cfg->group("TorrentSearchBar"); + search_bar->setText(g.readEntry("text", QString())); + + if (g.readEntry("hidden", true)) { + setHidden(true); + Q_EMIT filterBarHidden(QString()); + } else + setHidden(false); +} + +void TorrentSearchBar::saveState(KSharedConfigPtr cfg) +{ + KConfigGroup g = cfg->group("TorrentSearchBar"); + g.writeEntry("hidden", isHidden()); + g.writeEntry("text", search_bar->text()); +} + +} diff --git a/ktorrent/view/torrentsearchbar.h b/ktorrent/view/torrentsearchbar.h new file mode 100644 index 0000000..8b6d16c --- /dev/null +++ b/ktorrent/view/torrentsearchbar.h @@ -0,0 +1,49 @@ +/* + SPDX-FileCopyrightText: 2011 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KT_TORRENTSEARCHBAR_H +#define KT_TORRENTSEARCHBAR_H + +#include +#include +#include + +#include + +namespace kt +{ +class View; + +/** + * Search bar widget for torrents. + */ +class TorrentSearchBar : public QWidget +{ + Q_OBJECT +public: + TorrentSearchBar(View *view, QWidget *parent); + ~TorrentSearchBar() override; + + void loadState(KSharedConfigPtr cfg); + void saveState(KSharedConfigPtr cfg); + +public Q_SLOTS: + void showBar(); + void hideBar(); + +Q_SIGNALS: + void filterBarHidden(QString str); + void filterBarShown(QString str); + +protected: + bool eventFilter(QObject *obj, QEvent *ev) override; + +private: + QToolButton *hide_search_bar; + QLineEdit *search_bar; +}; +} + +#endif // KT_TORRENTSEARCHBAR_H diff --git a/ktorrent/view/view.cpp b/ktorrent/view/view.cpp new file mode 100644 index 0000000..72b875e --- /dev/null +++ b/ktorrent/view/view.cpp @@ -0,0 +1,868 @@ +/* + SPDX-FileCopyrightText: 2007 Joris Guisson + SPDX-FileCopyrightText: 2007 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "view.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "core.h" +#include "dialogs/addpeersdlg.h" +#include "dialogs/speedlimitsdlg.h" +#include "gui.h" +#include "interfaces/torrentactivityinterface.h" +#include "propertiesdlg.h" +#include "settings.h" +#include "viewdelegate.h" +#include "viewjobtracker.h" +#include "viewmodel.h" +#include "viewselectionmodel.h" +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace bt; + +namespace kt +{ +View::View(Core *core, GUI *gui, QWidget *parent) + : QTreeView(parent) + , core(core) + , gui(gui) + , group(core->getGroupManager()->allGroup()) + , num_torrents(0) + , num_running(0) + , model(nullptr) +{ + new ViewJobTracker(this); + + model = new ViewModel(core, this); + selection_model = new ViewSelectionModel(model, this); + + setContextMenuPolicy(Qt::CustomContextMenu); + setRootIsDecorated(false); + setSortingEnabled(true); + setAlternatingRowColors(true); + setDragEnabled(true); + setSelectionMode(QAbstractItemView::ExtendedSelection); + setSelectionBehavior(QAbstractItemView::SelectRows); + setAcceptDrops(true); + setDragDropMode(DragDrop); + setUniformRowHeights(true); + + connect(this, &View::customContextMenuRequested, this, &View::showMenu); + + header()->setContextMenuPolicy(Qt::CustomContextMenu); + connect(header(), &QHeaderView::customContextMenuRequested, this, &View::showHeaderMenu); + header_menu = new QMenu(this); + header_menu->addSection(i18n("Columns")); + + for (int i = 0; i < model->columnCount(QModelIndex()); i++) { + QString col = model->headerData(i, Qt::Horizontal, Qt::DisplayRole).toString(); + QAction *act = new QAction(col, header_menu); + header_menu->addAction(act); + act->setCheckable(true); + act->setChecked(true); + column_idx_map[act] = i; + column_action_list.append(act); + } + + connect(header_menu, &QMenu::triggered, this, &View::onHeaderMenuItemTriggered); + + setModel(model); + setSelectionModel(selection_model); + connect(selectionModel(), &QItemSelectionModel::currentChanged, this, &View::onCurrentItemChanged); + connect(selectionModel(), &QItemSelectionModel::selectionChanged, this, &View::onSelectionChanged); + connect(model, &ViewModel::sorted, selection_model, &ViewSelectionModel::sorted); + connect(this, &View::doubleClicked, this, &View::onDoubleClicked); + + delegate = new ViewDelegate(core, model, this); + setItemDelegate(delegate); + + gui->setCaption(group->groupName()); + + default_state = header()->saveState(); + + connect(core->getGroupManager(), &GroupManager::groupAdded, this, &View::onGroupAdded); + connect(core->getGroupManager(), &GroupManager::groupRemoved, this, &View::onGroupRemoved); + connect(core->getGroupManager(), &GroupManager::groupRenamed, this, &View::onGroupRenamed); +} + +View::~View() +{ + delegate->contractAll(); +} + +void View::setupActions(KActionCollection *ac) +{ + KStandardAction::selectAll(this, &View::selectAll, ac); + + start_torrent = new QAction(QIcon::fromTheme(QStringLiteral("kt-start")), i18nc("@action Start all selected torrents in the current tab", "Start"), this); + start_torrent->setToolTip(i18n("Start all selected torrents in the current tab")); + connect(start_torrent, &QAction::triggered, this, &View::startTorrents); + ac->addAction(QStringLiteral("start"), start_torrent); + ac->setDefaultShortcut(start_torrent, QKeySequence(Qt::CTRL + Qt::Key_S)); + + force_start_torrent = + new QAction(QIcon::fromTheme(QStringLiteral("kt-start")), i18nc("@action Force start all selected torrents in the current tab", "Force Start"), this); + force_start_torrent->setToolTip(i18n("Force start all selected torrents in the current tab")); + connect(force_start_torrent, &QAction::triggered, this, &View::forceStartTorrents); + ac->addAction(QStringLiteral("force_start"), force_start_torrent); + + stop_torrent = new QAction(QIcon::fromTheme(QStringLiteral("kt-stop")), i18nc("@action Stop all selected torrents in the current tab", "Stop"), this); + stop_torrent->setToolTip(i18n("Stop all selected torrents in the current tab")); + connect(stop_torrent, &QAction::triggered, this, &View::stopTorrents); + ac->addAction(QStringLiteral("stop"), stop_torrent); + ac->setDefaultShortcut(stop_torrent, QKeySequence(Qt::CTRL + Qt::Key_H)); + + pause_torrent = new QAction(QIcon::fromTheme(QStringLiteral("media-playback-pause")), // + i18nc("@action Pause all selected torrents in the current tab", "Pause"), + this); + pause_torrent->setToolTip(i18n("Pause all selected torrents in the current tab")); + connect(pause_torrent, &QAction::triggered, this, &View::pauseTorrents); + ac->addAction(QStringLiteral("pause"), pause_torrent); + + remove_torrent = new QAction(QIcon::fromTheme(QStringLiteral("kt-remove")), // + i18nc("@action Remove all selected torrents in the current tab", "Remove"), + this); + remove_torrent->setToolTip(i18n("Remove all selected torrents in the current tab")); + connect(remove_torrent, &QAction::triggered, this, &View::removeTorrents); + ac->addAction(QStringLiteral("remove"), remove_torrent); + ac->setDefaultShortcut(remove_torrent, QKeySequence(Qt::SHIFT + Qt::Key_Delete)); + + start_all = new QAction(QIcon::fromTheme(QStringLiteral("kt-start-all")), i18nc("@action Start all torrents in the current tab", "Start All"), this); + start_all->setToolTip(i18n("Start all torrents in the current tab")); + connect(start_all, &QAction::triggered, this, &View::startAllTorrents); + ac->addAction(QStringLiteral("start_all"), start_all); + ac->setDefaultShortcut(start_all, QKeySequence(Qt::SHIFT + Qt::Key_S)); + + stop_all = new QAction(QIcon::fromTheme(QStringLiteral("kt-stop-all")), i18nc("@action Stop all torrents in the current tab", "Stop All"), this); + stop_all->setToolTip(i18n("Stop all torrents in the current tab")); + connect(stop_all, &QAction::triggered, this, &View::stopAllTorrents); + ac->addAction(QStringLiteral("stop_all"), stop_all); + ac->setDefaultShortcut(stop_all, QKeySequence(Qt::SHIFT + Qt::Key_H)); + + remove_torrent_and_data = new QAction(QIcon::fromTheme(QStringLiteral("kt-remove")), i18n("Remove Torrent and Data"), this); + connect(remove_torrent_and_data, &QAction::triggered, this, &View::removeTorrentsAndData); + ac->addAction(QStringLiteral("view_remove_torrent_and_data"), remove_torrent_and_data); + ac->setDefaultShortcut(remove_torrent_and_data, QKeySequence(Qt::CTRL + Qt::Key_Delete)); + + rename_torrent = new QAction(QIcon::fromTheme(QStringLiteral("edit-rename")), i18n("Rename Torrent"), this); + connect(rename_torrent, &QAction::triggered, this, &View::renameTorrent); + ac->addAction(QStringLiteral("view_rename_torrent"), rename_torrent); + + add_peers = new QAction(QIcon::fromTheme(QStringLiteral("list-add")), i18n("Add Peers"), this); + connect(add_peers, &QAction::triggered, this, &View::addPeers); + ac->addAction(QStringLiteral("view_add_peers"), add_peers); + + manual_announce = new QAction(i18n("Manual Announce"), this); + connect(manual_announce, &QAction::triggered, this, &View::manualAnnounce); + ac->addAction(QStringLiteral("view_announce"), manual_announce); + ac->setDefaultShortcut(manual_announce, QKeySequence(Qt::SHIFT + Qt::Key_A)); + + do_scrape = new QAction(i18n("Scrape"), this); + connect(do_scrape, &QAction::triggered, this, &View::scrape); + ac->addAction(QStringLiteral("view_scrape"), do_scrape); + + preview = new QAction(QIcon::fromTheme(QStringLiteral("document-open")), i18n("Preview"), this); + connect(preview, &QAction::triggered, this, &View::previewTorrents); + ac->addAction(QStringLiteral("view_preview"), preview); + + data_dir = new QAction(QIcon::fromTheme(QStringLiteral("folder-open")), i18n("Data Directory"), this); + connect(data_dir, &QAction::triggered, this, &View::openDataDir); + ac->addAction(QStringLiteral("view_open_data_dir"), data_dir); + + tor_dir = new QAction(QIcon::fromTheme(QStringLiteral("folder-open")), i18n("Temporary Directory"), this); + connect(tor_dir, &QAction::triggered, this, &View::openTorDir); + ac->addAction(QStringLiteral("view_open_tmp_dir"), tor_dir); + + move_data = new QAction(i18n("Move Data"), this); + connect(move_data, &QAction::triggered, this, &View::moveData); + ac->addAction(QStringLiteral("view_move_data"), move_data); + + torrent_properties = new QAction(i18n("Settings"), this); + connect(torrent_properties, &QAction::triggered, this, &View::showProperties); + ac->addAction(QStringLiteral("view_torrent_properties"), torrent_properties); + + remove_from_group = new QAction(i18n("Remove from Group"), this); + connect(remove_from_group, &QAction::triggered, this, &View::removeFromGroup); + ac->addAction(QStringLiteral("view_remove_from_group"), remove_from_group); + + add_to_new_group = new QAction(QIcon::fromTheme(QStringLiteral("document-new")), i18n("New Group"), this); + connect(add_to_new_group, &QAction::triggered, this, &View::addToNewGroup); + ac->addAction(QStringLiteral("view_add_to_new_group"), add_to_new_group); + + check_data = new QAction(QIcon::fromTheme(QStringLiteral("kt-check-data")), i18n("Check Data"), this); + check_data->setToolTip(i18n("Check all the data of a torrent")); + connect(check_data, &QAction::triggered, this, &View::checkData); + ac->addAction(QStringLiteral("check_data"), check_data); + ac->setDefaultShortcut(check_data, QKeySequence(Qt::SHIFT + Qt::Key_C)); + + const GroupManager *gman = core->getGroupManager(); + for (GroupManager::CItr i = gman->begin(); i != gman->end(); i++) { + if (!i->second->isStandardGroup()) { + QAction *act = new QAction(QIcon::fromTheme(QStringLiteral("application-x-bittorrent")), i->first, this); + connect(act, &QAction::triggered, this, &View::addToGroupItemTriggered); + group_actions.insert(i->second, act); + } + } + + open_dir_menu = new QAction(QIcon::fromTheme(QStringLiteral("folder-open")), i18n("Open Directory"), this); + ac->addAction(QStringLiteral("OpenDirMenu"), open_dir_menu); + + groups_menu = new QAction(QIcon::fromTheme(QStringLiteral("application-x-bittorrent")), i18n("Add to Group"), this); + ac->addAction(QStringLiteral("GroupsSubMenu"), groups_menu); + + copy_url = new QAction(QIcon::fromTheme(QStringLiteral("edit-copy")), i18n("Copy Torrent URL"), this); + connect(copy_url, &QAction::triggered, this, &View::copyTorrentURL); + ac->addAction(QStringLiteral("view_copy_url"), copy_url); + + export_torrent = new QAction(QIcon::fromTheme(QStringLiteral("document-export")), i18n("Export Torrent"), this); + connect(export_torrent, &QAction::triggered, this, &View::exportTorrent); + ac->addAction(QStringLiteral("view_export_torrent"), export_torrent); + + speed_limits = new QAction(QIcon::fromTheme(QStringLiteral("kt-speed-limits")), i18n("Speed Limits"), this); + speed_limits->setToolTip(i18n("Set the speed limits of individual torrents")); + connect(speed_limits, &QAction::triggered, this, &View::speedLimits); + ac->addAction(QStringLiteral("speed_limits"), speed_limits); + ac->setDefaultShortcut(speed_limits, QKeySequence(Qt::CTRL + Qt::Key_L)); +} + +struct StartAndStopAllVisitor { + QAction *start_all; + QAction *stop_all; + + StartAndStopAllVisitor(QAction *start_all, QAction *stop_all) + : start_all(start_all) + , stop_all(stop_all) + { + } + + bool operator()(bt::TorrentInterface *tc) + { + if (tc->getJobQueue()->runningJobs()) + return true; + + const TorrentStats &s = tc->getStats(); + if (s.running || (tc->isAllowedToStart() && !tc->overMaxRatio() && !tc->overMaxSeedTime())) + stop_all->setEnabled(true); + else + start_all->setEnabled(true); + + return !stop_all->isEnabled() || !start_all->isEnabled(); + } +}; + +void View::updateActions() +{ + QList sel; + getSelection(sel); + + bool qm_enabled = !Settings::manuallyControlTorrents(); + bool en_start = false; + bool en_stop = false; + bool en_remove = false; + bool en_prev = false; + bool en_announce = false; + bool en_add_peer = false; + bool en_pause = false; + + for (bt::TorrentInterface *tc : qAsConst(sel)) { + const TorrentStats &s = tc->getStats(); + + if (tc->readyForPreview() && !s.multi_file_torrent) + en_prev = true; + + if (tc->getJobQueue()->runningJobs()) + continue; + + en_remove = true; + if (!s.running) { + if (qm_enabled) { + // Queued torrents can be stopped, and not started + if (s.queued) + en_stop = true; + else + en_start = true; + } else { + en_start = true; + } + } else { + en_stop = true; + if (tc->announceAllowed()) + en_announce = true; + + if (!s.paused) + en_pause = true; + else + en_start = true; + } + + if (!s.priv_torrent) { + en_add_peer = true; + } + } + + en_add_peer = en_add_peer && en_stop; + + start_torrent->setEnabled(en_start); + force_start_torrent->setEnabled(en_start); + stop_torrent->setEnabled(en_stop); + remove_torrent->setEnabled(en_remove); + remove_torrent_and_data->setEnabled(en_remove); + pause_torrent->setEnabled(en_pause); + preview->setEnabled(en_prev); + add_peers->setEnabled(en_add_peer); + manual_announce->setEnabled(en_announce); + do_scrape->setEnabled(sel.count() > 0); + move_data->setEnabled(sel.count() > 0); + + remove_from_group->setEnabled(group && !group->isStandardGroup()); + groups_menu->setEnabled(group_actions.count() > 0); + check_data->setEnabled(sel.count() > 0); + + rename_torrent->setEnabled(sel.count() == 1); + data_dir->setEnabled(sel.count() == 1); + tor_dir->setEnabled(sel.count() == 1); + open_dir_menu->setEnabled(sel.count() == 1); + add_to_new_group->setEnabled(sel.count() > 0); + copy_url->setEnabled(sel.count() == 1 && sel.front()->loadUrl().isValid()); + export_torrent->setEnabled(sel.count() == 1); + + if (qm_enabled) { + start_all->setEnabled(false); + stop_all->setEnabled(false); + StartAndStopAllVisitor v(start_all, stop_all); + model->visit(v); + } else { + start_all->setEnabled(numRunningTorrents() < numTorrents()); + stop_all->setEnabled(numRunningTorrents() > 0); + } +} + +void View::setGroup(Group *g) +{ + group = g; + model->setGroup(group); + update(); + selectionModel()->clear(); + gui->setCaption(g->groupName()); +} + +void View::update() +{ + if (!uniformRowHeights() && !delegate->hasExtenders()) + setUniformRowHeights(true); + + if (!model->update(delegate)) { + // model wasn't resorted, so update individual items + const QModelIndexList &to_update = model->updateList(); + for (const QModelIndex &idx : to_update) + QAbstractItemView::update(idx); + } +} + +void View::startTorrents() +{ + QList sel; + getSelection(sel); + if (sel.count() > 0) + core->start(sel); +} + +void View::forceStartTorrents() +{ + QList sel; + getSelection(sel); + if (sel.count() == 0) + return; + + QueueManager *qm = core->getQueueManager(); + if (qm->enabled()) { + // Give everybody in the selection a high priority + int prio = qm->count(); + int idx = 0; + for (bt::TorrentInterface *tc : qAsConst(sel)) + tc->setPriority(prio + sel.count() - idx++); + + core->start(sel); + } else + core->start(sel); +} + +void View::stopTorrents() +{ + QList sel; + getSelection(sel); + if (sel.count() > 0) + core->stop(sel); +} + +void View::pauseTorrents() +{ + QList sel; + getSelection(sel); + if (sel.count() > 0) + core->pause(sel); +} + +void View::removeTorrents() +{ + QList sel; + getSelection(sel); + for (bt::TorrentInterface *tc : qAsConst(sel)) { + if (tc && !tc->getJobQueue()->runningJobs()) { + const TorrentStats &s = tc->getStats(); + bool data_to = false; + if (!s.completed) { + QString msg = i18n( + "The torrent %1 has not finished downloading, " + "do you want to delete the incomplete data, too?", + tc->getDisplayName()); + int ret = KMessageBox::questionYesNoCancel(this, + msg, + i18n("Remove Download"), + KGuiItem(i18n("Delete Data")), + KGuiItem(i18n("Keep Data")), + KStandardGuiItem::cancel()); + if (ret == KMessageBox::Cancel) + return; + else if (ret == KMessageBox::Yes) + data_to = true; + } + core->remove(tc, data_to); + } + } +} + +void View::removeTorrentsAndData() +{ + QList sel; + getSelection(sel); + if (sel.count() == 0) + return; + + QStringList names; + for (bt::TorrentInterface *tc : qAsConst(sel)) { + names.append(tc->getDisplayName()); + } + + QString msg = i18n("You will lose all the downloaded data of the following torrents. Are you sure you want to do this?"); + if (KMessageBox::warningYesNoList(this, msg, names, i18n("Remove Torrent"), KStandardGuiItem::remove(), KStandardGuiItem::cancel()) == KMessageBox::Yes) { + core->remove(sel, true); + } +} + +void View::startAllTorrents() +{ + QList all; + model->allTorrents(all); + core->start(all); +} + +void View::stopAllTorrents() +{ + QList all; + model->allTorrents(all); + core->stop(all); +} + +void View::addPeers() +{ + QList sel; + getSelection(sel); + if (sel.count() > 0) { + AddPeersDlg dlg(sel[0], this); + dlg.exec(); + } +} + +void View::manualAnnounce() +{ + QList sel; + getSelection(sel); + for (bt::TorrentInterface *tc : qAsConst(sel)) { + if (tc->getStats().running) + tc->updateTracker(); + } +} + +void View::scrape() +{ + QList sel; + getSelection(sel); + for (bt::TorrentInterface *tc : qAsConst(sel)) { + tc->scrapeTracker(); + } +} + +void View::previewTorrents() +{ + QList sel; + getSelection(sel); + for (bt::TorrentInterface *tc : qAsConst(sel)) { + if (tc->readyForPreview() && !tc->getStats().multi_file_torrent) { + new KRun(QUrl::fromLocalFile(tc->getStats().output_path), nullptr, true); + } + } +} + +void View::openDataDir() +{ + QList sel; + getSelection(sel); + for (bt::TorrentInterface *tc : qAsConst(sel)) { + if (tc->getStats().multi_file_torrent) + new KRun(QUrl::fromLocalFile(tc->getStats().output_path), nullptr, true); + else + new KRun(QUrl::fromLocalFile(tc->getDataDir()), nullptr, true); + } +} + +void View::openTorDir() +{ + QList sel; + getSelection(sel); + for (bt::TorrentInterface *tc : qAsConst(sel)) { + new KRun(QUrl::fromLocalFile(tc->getTorDir()), nullptr, true); + } +} + +void View::moveData() +{ + QList sel; + getSelection(sel); + if (sel.count() == 0) + return; + + QString recentDirClass; + QString dir = + QFileDialog::getExistingDirectory(this, + i18n("Select a directory to move the data to."), + KFileWidget::getStartUrl(QUrl(QStringLiteral("kfiledialog:///saveTorrentData")), recentDirClass).toLocalFile()); + + if (dir.isEmpty()) + return; + + if (!recentDirClass.isEmpty()) + KRecentDirs::add(recentDirClass, dir); + + for (bt::TorrentInterface *tc : qAsConst(sel)) { + if (core->checkMissingFiles(tc)) + tc->changeOutputDir(dir, bt::TorrentInterface::MOVE_FILES); + } +} + +void View::showProperties() +{ + QList sel; + getSelection(sel); + if (sel.count() == 0) + return; + + PropertiesDlg dlg(sel.front(), this); + dlg.exec(); +} + +void View::removeFromGroup() +{ + if (!group || group->isStandardGroup()) + return; + + QList sel; + getSelection(sel); + for (bt::TorrentInterface *tc : qAsConst(sel)) + group->removeTorrent(tc); + core->getGroupManager()->saveGroups(); + update(); + num_running++; // set these wrong so that the caption is updated on the next update + num_torrents++; +} + +void View::renameTorrent() +{ + QModelIndexList indices = selectionModel()->selectedRows(); + if (indices.count() == 0) + return; + + QModelIndex idx = indices.front(); + QTreeView::edit(model->index(idx.row(), 0)); + editingItem(true); +} + +void View::checkData() +{ + QList sel; + getSelection(sel); + for (bt::TorrentInterface *tc : qAsConst(sel)) { + if (tc->getStats().status != bt::ALLOCATING_DISKSPACE) + core->doDataCheck(tc); + } + + core->startUpdateTimer(); // make sure update timer of core is running +} + +void View::showMenu(const QPoint &pos) +{ + QMenu *view_menu = gui->getTorrentActivity()->part()->menu(QStringLiteral("ViewMenu")); + if (!view_menu) + return; + + gui->getTorrentActivity()->part()->plugActionList(QStringLiteral("view_groups_list"), group_actions.values()); + gui->getTorrentActivity()->part()->unplugActionList(QStringLiteral("view_columns_list")); + gui->getTorrentActivity()->part()->plugActionList(QStringLiteral("view_columns_list"), column_action_list); + view_menu->popup(viewport()->mapToGlobal(pos)); +} + +void View::showHeaderMenu(const QPoint &pos) +{ + header_menu->popup(header()->mapToGlobal(pos)); +} + +void View::getSelection(QList &sel) +{ + QModelIndexList indices = selectionModel()->selectedRows(); + model->torrentsFromIndexList(indices, sel); +} + +void View::restoreState(const QByteArray &state) +{ + if (!state.isEmpty()) { + QHeaderView *v = header(); + v->restoreState(state); + sortByColumn(v->sortIndicatorSection(), v->sortIndicatorOrder()); + model->sort(v->sortIndicatorSection(), v->sortIndicatorOrder()); + } + + QMap::iterator i = column_idx_map.begin(); + while (i != column_idx_map.end()) { + QAction *act = i.key(); + bool hidden = header()->isSectionHidden(i.value()); + act->setChecked(!hidden); + if (!hidden && header()->sectionSize(i.value()) == 0) + header()->resizeSection(i.value(), 20); + i++; + } +} + +bt::TorrentInterface *View::getCurrentTorrent() +{ + return model->torrentFromIndex(selectionModel()->currentIndex()); +} + +void View::onCurrentItemChanged(const QModelIndex ¤t, const QModelIndex & /*previous*/) +{ + // Out(SYS_GEN|LOG_DEBUG) << "onCurrentItemChanged " << current.row() << endl; + bt::TorrentInterface *tc = model->torrentFromIndex(current); + currentTorrentChanged(tc); +} + +void View::onHeaderMenuItemTriggered(QAction *act) +{ + int idx = column_idx_map[act]; + if (act->isChecked()) + header()->showSection(idx); + else + header()->hideSection(idx); +} + +void View::onSelectionChanged(const QItemSelection & /*selected*/, const QItemSelection & /*deselected*/) +{ + updateActions(); + torrentSelectionChanged(); +} + +void View::closeEditor(QWidget *editor, QAbstractItemDelegate::EndEditHint hint) +{ + QTreeView::closeEditor(editor, hint); + editingItem(false); + setFocus(); +} + +bool View::edit(const QModelIndex &index, EditTrigger trigger, QEvent *event) +{ + bool ret = QTreeView::edit(index, trigger, event); + if (ret) + editingItem(true); + + return ret; +} + +void View::onDoubleClicked(const QModelIndex &index) +{ + if (index.column() == 0) // double clicking on column 0 will change the name of a torrent + return; + + bt::TorrentInterface *tc = model->torrentFromIndex(index); + if (tc) { + if (tc->getStats().multi_file_torrent) + new KRun(QUrl::fromLocalFile(tc->getStats().output_path), nullptr, true); + else + new KRun(QUrl::fromLocalFile(tc->getDataDir()), nullptr, true); + } +} + +void View::extend(TorrentInterface *tc, kt::Extender *widget, bool close_similar) +{ + setUniformRowHeights(false); + delegate->extend(tc, widget, close_similar); + if (group && !group->isMember(tc)) + delegate->hideExtender(tc); +} + +void View::onCurrentGroupChanged(Group *g) +{ + setGroup(g); +} + +void View::onGroupAdded(Group *g) +{ + gui->getTorrentActivity()->part()->unplugActionList(QStringLiteral("view_groups_list")); + QAction *act = new QAction(QIcon::fromTheme(QStringLiteral("application-x-bittorrent")), g->groupName(), this); + connect(act, &QAction::triggered, this, &View::addToGroupItemTriggered); + group_actions.insert(g, act); + gui->getTorrentActivity()->part()->plugActionList(QStringLiteral("view_groups_list"), group_actions.values()); +} + +void View::onGroupRemoved(Group *g) +{ + if (group == g) + setGroup(core->getGroupManager()->allGroup()); + + gui->getTorrentActivity()->part()->unplugActionList(QStringLiteral("view_groups_list")); + QMap::iterator i = group_actions.find(g); + if (i != group_actions.end()) { + delete i.value(); + group_actions.erase(i); + } + + gui->getTorrentActivity()->part()->plugActionList(QStringLiteral("view_groups_list"), group_actions.values()); +} + +void View::onGroupRenamed(Group *g) +{ + QMap::iterator j = group_actions.find(g); + if (j != group_actions.end()) + j.value()->setText(g->groupName()); +} + +void View::addToGroupItemTriggered() +{ + QAction *s = (QAction *)sender(); + Group *g = nullptr; + QMap::iterator j = group_actions.begin(); + while (j != group_actions.end() && !g) { + if (j.value() == s) + g = j.key(); + j++; + } + + if (!g) + return; + + QList sel; + getSelection(sel); + for (bt::TorrentInterface *tc : qAsConst(sel)) { + g->addTorrent(tc, false); + } + core->getGroupManager()->saveGroups(); +} + +void View::addToNewGroup() +{ + Group *g = gui->getTorrentActivity()->addNewGroup(); + if (g) { + QList sel; + getSelection(sel); + for (bt::TorrentInterface *tc : qAsConst(sel)) { + g->addTorrent(tc, false); + } + core->getGroupManager()->saveGroups(); + } +} + +void View::copyTorrentURL() +{ + QList sel; + getSelection(sel); + if (sel.count() == 0) + return; + + bt::TorrentInterface *tc = sel.front(); + if (!tc->loadUrl().isValid()) + return; + + QClipboard *cb = QApplication::clipboard(); + cb->setText(tc->loadUrl().toDisplayString()); +} + +void View::speedLimits() +{ + QList sel; + getSelection(sel); + SpeedLimitsDlg dlg(sel.count() > 0 ? sel.front() : nullptr, core, gui->getMainWindow()); + dlg.exec(); +} + +void View::exportTorrent() +{ + QList sel; + getSelection(sel); + if (sel.count() == 1) { + bt::TorrentInterface *tc = sel.front(); + + QString recentDirClass; + QString fn = QFileDialog::getSaveFileName(gui, + QString(), + KFileWidget::getStartUrl(QUrl(QLatin1String("kfiledialog:///exportTorrent")), recentDirClass).toLocalFile(), + kt::TorrentFileFilter(false)); + + if (fn.isEmpty()) + return; + + if (!recentDirClass.isEmpty()) + KRecentDirs::add(recentDirClass, QFileInfo(fn).absolutePath()); + + KIO::file_copy(QUrl::fromLocalFile(tc->getTorDir() + QLatin1String("torrent")), QUrl::fromLocalFile(fn), -1, KIO::Overwrite); + } +} + +void View::setFilterString(const QString &filter) +{ + model->setFilterString(filter); + model->update(delegate); +} + +void View::keyPressEvent(QKeyEvent *event) +{ + if (event->key() == Qt::Key_Delete) { + removeTorrents(); + event->accept(); + } else + QTreeView::keyPressEvent(event); +} + +} diff --git a/ktorrent/view/view.h b/ktorrent/view/view.h new file mode 100644 index 0000000..9e5ca31 --- /dev/null +++ b/ktorrent/view/view.h @@ -0,0 +1,225 @@ +/* + SPDX-FileCopyrightText: 2007 Joris Guisson + SPDX-FileCopyrightText: 2007 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KTVIEW_HH +#define KTVIEW_HH + +#include +#include + +#include + +#include +#include + +class QMenu; +class QAction; +class KActionCollection; + +namespace bt +{ +class TorrentInterface; +} + +namespace kt +{ +class GUI; +class Extender; +class Core; +class ViewModel; +class ViewSelectionModel; +class ViewDelegate; +class Group; + +class View : public QTreeView +{ + Q_OBJECT +public: + View(Core *core, GUI *gui, QWidget *parent); + ~View() override; + + /// Setup the actions of the view manager + void setupActions(KActionCollection *ac); + + /// Update all actions + void updateActions(); + + /** + * Get the view model + * @return The ViewModel of this View + */ + ViewModel *viewModel() + { + return model; + } + + /** + * Set the group to show in this view + * @param g The Group + * */ + void setGroup(Group *g); + + /// Get the current group + Group *getCurrentGroup() const + { + return group; + } + + /** + * Put the current selection in a list. + * @param sel The list to put it in + */ + void getSelection(QList &sel); + + /// Get the current group + const Group *getGroup() const + { + return group; + } + + /// Restore the view state + void restoreState(const QByteArray &state); + + /// Get the current torrent + bt::TorrentInterface *getCurrentTorrent(); + + /// Get the number of torrents + bt::Uint32 numTorrents() const + { + return num_torrents; + }; + + /// Get the number of running torrents + bt::Uint32 numRunningTorrents() const + { + return num_running; + } + + void closeEditor(QWidget *editor, QAbstractItemDelegate::EndEditHint hint) override; + bool edit(const QModelIndex &index, EditTrigger trigger, QEvent *event) override; + + /// Get the ViewDelegate + ViewDelegate *viewDelegate() + { + return delegate; + } + + /// Extend a widget + void extend(bt::TorrentInterface *tc, Extender *widget, bool close_similar); + + /// Get the default state + const QByteArray &defaultState() const + { + return default_state; + } + + void keyPressEvent(QKeyEvent *event) override; + +public Q_SLOTS: + /// Set the filter string + void setFilterString(const QString &filter); + + /// Update all items in the view + void update(); + + void startTorrents(); + void forceStartTorrents(); + void stopTorrents(); + void pauseTorrents(); + void removeTorrents(); + void removeTorrentsAndData(); + void startAllTorrents(); + void stopAllTorrents(); + void checkData(); + void addPeers(); + void manualAnnounce(); + void previewTorrents(); + void openDataDir(); + void openTorDir(); + void removeFromGroup(); + void scrape(); + void moveData(); + void showProperties(); + void renameTorrent(); + void showMenu(const QPoint &pos); + void showHeaderMenu(const QPoint &pos); + void onHeaderMenuItemTriggered(QAction *act); + void onCurrentItemChanged(const QModelIndex ¤t, const QModelIndex &previous); + void onSelectionChanged(const QItemSelection &selected, const QItemSelection &deselected); + void onDoubleClicked(const QModelIndex &index); + void onCurrentGroupChanged(kt::Group *g); + void onGroupRenamed(Group *g); + void onGroupRemoved(Group *g); + void onGroupAdded(Group *g); + + /// An item in the groups menu was triggered + void addToGroupItemTriggered(); + + /// Copy the torrent URL to the clipboard + void copyTorrentURL(); + + /// Show the speed limits dialog + void speedLimits(); + + /// Export a torrent + void exportTorrent(); + + /// Add a new group and add the current selection to it + void addToNewGroup(); + +Q_SIGNALS: + void currentTorrentChanged(bt::TorrentInterface *tc); + void torrentSelectionChanged(); + void editingItem(bool on); + +private: + Core *core; + GUI *gui; + Group *group; + QMenu *header_menu; + QMap column_idx_map; + QList column_action_list; + bt::Uint32 num_torrents; + bt::Uint32 num_running; + ViewModel *model; + ViewSelectionModel *selection_model; + ViewDelegate *delegate; + QMap data_scan_extenders; + QByteArray default_state; + + // actions for the view menu + QAction *start_torrent; + QAction *force_start_torrent; + QAction *start_all; + QAction *stop_torrent; + QAction *stop_all; + QAction *pause_torrent; + QAction *unpause_torrent; + QAction *remove_torrent; + QAction *remove_torrent_and_data; + QAction *add_peers; + QAction *manual_announce; + QAction *do_scrape; + QAction *preview; + QAction *data_dir; + QAction *tor_dir; + QAction *move_data; + QAction *torrent_properties; + QAction *rename_torrent; + QAction *remove_from_group; + QMap group_actions; + QAction *add_to_new_group; + QAction *check_data; + QAction *open_dir_menu; + QAction *groups_menu; + QAction *copy_url; + QAction *export_torrent; + QList configure_columns_list; + QAction *speed_limits; +}; +} + +#endif diff --git a/ktorrent/view/viewdelegate.cpp b/ktorrent/view/viewdelegate.cpp new file mode 100644 index 0000000..b984470 --- /dev/null +++ b/ktorrent/view/viewdelegate.cpp @@ -0,0 +1,342 @@ +/* + SPDX-FileCopyrightText: 2009 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "viewdelegate.h" +#include "core.h" +#include "view.h" +#include "viewmodel.h" + +#include + +#include +#include +#include +#include + +#include + +namespace kt +{ +////////////////////////// + +ExtenderBox::ExtenderBox(QWidget *widget) + : QWidget(widget) +{ + layout = new QVBoxLayout(this); +} + +ExtenderBox::~ExtenderBox() +{ + clear(); +} + +void ExtenderBox::add(Extender *ext) +{ + layout->addWidget(ext); + extenders.append(ext); +} + +void ExtenderBox::remove(Extender *ext) +{ + layout->removeWidget(ext); + extenders.removeAll(ext); + ext->hide(); + ext->deleteLater(); +} + +void ExtenderBox::removeSimilar(Extender *ext) +{ + for (QList::iterator i = extenders.begin(); i != extenders.end();) { + if (ext->similar(*i)) { + (*i)->hide(); + (*i)->deleteLater(); + i = extenders.erase(i); + } else + i++; + } +} + +void ExtenderBox::clear() +{ + for (Extender *ext : qAsConst(extenders)) { + ext->hide(); + ext->deleteLater(); + } + + extenders.clear(); +} + +////////////////////////// + +ViewDelegate::ViewDelegate(Core *core, ViewModel *model, View *parent) + : QStyledItemDelegate(parent) + , model(model) +{ + connect(core, &Core::torrentRemoved, this, &ViewDelegate::torrentRemoved); +} + +ViewDelegate::~ViewDelegate() +{ + contractAll(); +} + +void ViewDelegate::extend(bt::TorrentInterface *tc, kt::Extender *widget, bool close_similar) +{ + ExtenderBox *ext = nullptr; + ExtItr itr = extenders.find(tc); + if (itr == extenders.end()) { + QAbstractItemView *aiv = qobject_cast(parent()); + ext = new ExtenderBox(aiv->viewport()); + extenders.insert(tc, ext); + } else { + ext = itr.value(); + } + + if (close_similar) + ext->removeSimilar(widget); + + ext->add(widget); + widget->setParent(ext); + widget->show(); + + scheduleUpdateViewLayout(); + connect(widget, &Extender::closeRequest, this, &ViewDelegate::closeRequested); + connect(widget, &Extender::resized, this, &ViewDelegate::resized); +} + +void ViewDelegate::closeExtenders(bt::TorrentInterface *tc) +{ + ExtItr itr = extenders.find(tc); + if (itr != extenders.end()) { + ExtenderBox *ext = itr.value(); + ext->clear(); + ext->hide(); + ext->deleteLater(); + extenders.erase(itr); + } + + scheduleUpdateViewLayout(); +} + +void ViewDelegate::closeExtender(bt::TorrentInterface *tc, Extender *ext) +{ + ExtItr itr = extenders.find(tc); + if (itr != extenders.end()) { + ExtenderBox *box = itr.value(); + box->remove(ext); + if (box->count() == 0) { + box->hide(); + box->deleteLater(); + extenders.erase(itr); + } + } + + scheduleUpdateViewLayout(); +} + +void ViewDelegate::closeRequested(Extender *ext) +{ + closeExtender(ext->torrent(), ext); +} + +void ViewDelegate::torrentRemoved(bt::TorrentInterface *tc) +{ + closeExtenders(tc); +} + +void ViewDelegate::resized(Extender *ext) +{ + Q_UNUSED(ext); + scheduleUpdateViewLayout(); +} + +QSize ViewDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const +{ + QSize ret; + + if (!extenders.isEmpty()) + ret = maybeExtendedSize(option, index); + else + ret = QStyledItemDelegate::sizeHint(option, index); + + return ret; +} + +QSize ViewDelegate::maybeExtendedSize(const QStyleOptionViewItem &option, const QModelIndex &index) const +{ + bt::TorrentInterface *tc = model->torrentFromIndex(index); + QSize size(QStyledItemDelegate::sizeHint(option, index)); + if (!tc) + return size; + + ExtCItr itr = extenders.find(tc); + const QWidget *ext = itr == extenders.end() ? nullptr : itr.value(); + if (!ext) + return size; + + // add extender height to maximum height of any column in our row + int item_height = size.height(); + int row = index.row(); + int this_column = index.column(); + + // this is quite slow, but Qt is smart about when to call sizeHint(). + for (int column = 0; model->columnCount() < column; column++) { + if (column == this_column) + continue; + + QModelIndex neighborIndex(index.sibling(row, column)); + if (neighborIndex.isValid()) + item_height = std::max(item_height, QStyledItemDelegate::sizeHint(option, neighborIndex).height()); + } + + // we only want to reserve vertical space, the horizontal extender layout is our private business. + size.rheight() = item_height + ext->sizeHint().height(); + return size; +} + +void ViewDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const +{ + QStyleOptionViewItem indicatorOption(option); + initStyleOption(&indicatorOption, index); + if (index.column() == 0) + indicatorOption.viewItemPosition = QStyleOptionViewItem::Beginning; + else if (index.column() == index.model()->columnCount() - 1) + indicatorOption.viewItemPosition = QStyleOptionViewItem::End; + else + indicatorOption.viewItemPosition = QStyleOptionViewItem::Middle; + + QStyleOptionViewItem itemOption(option); + initStyleOption(&itemOption, index); + if (index.column() == 0) + itemOption.viewItemPosition = QStyleOptionViewItem::Beginning; + else if (index.column() == index.model()->columnCount() - 1) + itemOption.viewItemPosition = QStyleOptionViewItem::End; + else + itemOption.viewItemPosition = QStyleOptionViewItem::Middle; + + bt::TorrentInterface *tc = model->torrentFromIndex(index); + if (!tc || !extenders.contains(tc)) { + normalPaint(painter, itemOption, index); + return; + } + + QWidget *extender = extenders[tc]; + int extenderHeight = extender->sizeHint().height(); + + // an extender is present - make two rectangles: one to paint the original item, one for the extender + QStyleOptionViewItem extOption(option); + initStyleOption(&extOption, index); + extOption.rect = extenderRect(extender, option, index); + extender->setGeometry(extOption.rect); + // if we show it before, it will briefly flash in the wrong location. + // the downside is, of course, that an api user effectively can't hide it. + extender->show(); + + indicatorOption.rect.setHeight(option.rect.height() - extenderHeight); + itemOption.rect.setHeight(option.rect.height() - extenderHeight); + // tricky:make sure that the modified options' rect really has the + // same height as the unchanged option.rect if no extender is present + //(seems to work OK) + + normalPaint(painter, itemOption, index); +} + +void ViewDelegate::updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex &index) const +{ + bt::TorrentInterface *tc = model->torrentFromIndex(index); + if (!tc || !extenders.contains(tc)) { + QStyledItemDelegate::updateEditorGeometry(editor, option, index); + } else { + QWidget *extender = extenders[tc]; + int extenderHeight = extender->sizeHint().height(); + + QStyleOptionViewItem itemOption(option); + initStyleOption(&itemOption, index); + itemOption.rect.setHeight(option.rect.height() - extenderHeight); + editor->setGeometry(itemOption.rect); + } +} + +QRect ViewDelegate::extenderRect(QWidget *extender, const QStyleOptionViewItem &option, const QModelIndex &index) const +{ + QRect rect(option.rect); + rect.setTop(rect.bottom() + 1 - extender->sizeHint().height()); + + rect.setLeft(0); + if (QTreeView *tv = qobject_cast(parent())) { + int steps = 0; + for (QModelIndex idx(index.parent()); idx.isValid(); idx = idx.parent()) + steps++; + + if (tv->rootIsDecorated()) + steps++; + + rect.setLeft(steps * tv->indentation()); + } + + QAbstractScrollArea *container = qobject_cast(parent()); + rect.setRight(container->viewport()->width() - 1); + return rect; +} + +void ViewDelegate::scheduleUpdateViewLayout() +{ + QAbstractItemView *aiv = qobject_cast(parent()); + // prevent crashes during destruction of the view + if (aiv) { + // dirty hack to call aiv's protected scheduleDelayedItemsLayout() + aiv->setRootIndex(aiv->rootIndex()); + } +} + +void ViewDelegate::contractAll() +{ + for (ExtCItr i = extenders.cbegin(); i != extenders.cend(); i++) { + i.value()->clear(); + i.value()->hide(); + i.value()->deleteLater(); + } + extenders.clear(); +} + +bool ViewDelegate::extended(bt::TorrentInterface *tc) const +{ + return extenders.contains(tc); +} + +void ViewDelegate::hideExtender(bt::TorrentInterface *tc) +{ + ExtItr i = extenders.find(tc); + if (i != extenders.end()) + i.value()->hide(); +} + +void ViewDelegate::paintProgressBar(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const +{ + int progress = index.data().toInt(); + + QStyleOptionProgressBar progressBarOption; + progressBarOption.palette = option.palette; + progressBarOption.state = option.state; + progressBarOption.rect = option.rect; + progressBarOption.minimum = 0; + progressBarOption.maximum = 100; + progressBarOption.progress = progress; + progressBarOption.text = QLocale().toString(progress) + QLatin1Char('%'); + progressBarOption.textVisible = true; + progressBarOption.direction = option.direction; + + QApplication::style()->drawControl(QStyle::CE_ProgressBar, &progressBarOption, painter); +} + +void ViewDelegate::normalPaint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const +{ + if (index.column() == ViewModel::PERCENTAGE) + paintProgressBar(painter, option, index); + else + QStyledItemDelegate::paint(painter, option, index); +} + +} diff --git a/ktorrent/view/viewdelegate.h b/ktorrent/view/viewdelegate.h new file mode 100644 index 0000000..7001271 --- /dev/null +++ b/ktorrent/view/viewdelegate.h @@ -0,0 +1,120 @@ +/* + SPDX-FileCopyrightText: 2009 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KT_VIEWDELEGATE_H +#define KT_VIEWDELEGATE_H + +#include +#include + +class QVBoxLayout; +namespace bt +{ +class TorrentInterface; +} + +namespace kt +{ +class ViewModel; +class View; +class Core; +class Extender; + +/** + Box which contains all the extenders of a widget +*/ +class ExtenderBox : public QWidget +{ +public: + ExtenderBox(QWidget *widget); + ~ExtenderBox() override; + + /// Add an Extender + void add(Extender *ext); + + /// Remove an Extender + void remove(Extender *ext); + + /// Remove extenders similar to ext + void removeSimilar(Extender *ext); + + /// Clear all extenders + void clear(); + + /// Get the number of extenders + int count() const + { + return extenders.count(); + } + +private: + QVBoxLayout *layout; + QList extenders; +}; + +/** + Item delegate which keeps track of of ScanExtenders +*/ +class ViewDelegate : public QStyledItemDelegate +{ + Q_OBJECT +public: + ViewDelegate(Core *core, ViewModel *model, View *parent); + ~ViewDelegate() override; + + /** + Extend a torrent with a widget + */ + void extend(bt::TorrentInterface *tc, Extender *widget, bool close_similar); + + QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override; + void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override; + void updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex &index) const override; + + /** + * Close all extenders and delete all extender widgets. + */ + void contractAll(); + + /// Is an extender being shown for a torrent + bool extended(bt::TorrentInterface *tc) const; + + /// Hide the extender for a torrent + void hideExtender(bt::TorrentInterface *tc); + + /// Does the delegate have extenders + bool hasExtenders() const + { + return !extenders.isEmpty(); + } + +public Q_SLOTS: + /// Close all the extenders of a torrent + void closeExtenders(bt::TorrentInterface *tc); + void closeExtender(bt::TorrentInterface *tc, Extender *ext); + +private Q_SLOTS: + void torrentRemoved(bt::TorrentInterface *tc); + void closeRequested(Extender *ext); + void resized(Extender *ext); + +private: + QSize maybeExtendedSize(const QStyleOptionViewItem &option, const QModelIndex &index) const; + QRect extenderRect(QWidget *extender, const QStyleOptionViewItem &option, const QModelIndex &index) const; + void scheduleUpdateViewLayout(); + void paintProgressBar(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const; + void normalPaint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const; + +private: + ViewModel *model; + QMap extenders; + + typedef QMap::iterator ExtItr; + typedef QMap::const_iterator ExtCItr; +}; + +} + +#endif // KT_VIEWDELEGATE_H diff --git a/ktorrent/view/viewjobtracker.cpp b/ktorrent/view/viewjobtracker.cpp new file mode 100644 index 0000000..65a39e0 --- /dev/null +++ b/ktorrent/view/viewjobtracker.cpp @@ -0,0 +1,50 @@ +/* + SPDX-FileCopyrightText: 2011 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "viewjobtracker.h" +#include "scanextender.h" +#include "view.h" +#include "viewdelegate.h" +#include + +namespace kt +{ +ViewJobTracker::ViewJobTracker(View *parent) + : JobTracker(parent) + , view(parent) +{ +} + +ViewJobTracker::~ViewJobTracker() +{ +} + +void ViewJobTracker::jobUnregistered(bt::Job *j) +{ + ActiveJobs::iterator i = widgets.find(j); + if (i == widgets.end()) + return; + + JobProgressWidget *w = i.value(); + if (w->automaticRemove()) + w->emitCloseRequest(); +} + +void ViewJobTracker::jobRegistered(bt::Job *j) +{ + kt::JobProgressWidget *widget = createJobWidget(j); + view->extend(j->torrent(), widget, j->torrentStatus() == bt::CHECKING_DATA); +} + +kt::JobProgressWidget *ViewJobTracker::createJobWidget(bt::Job *job) +{ + if (job->torrentStatus() == bt::CHECKING_DATA) { + ScanExtender *ext = new ScanExtender(job, nullptr); + widgets[job] = ext; + return ext; + } else + return kt::JobTracker::createJobWidget(job); +} +} diff --git a/ktorrent/view/viewjobtracker.h b/ktorrent/view/viewjobtracker.h new file mode 100644 index 0000000..e75bf74 --- /dev/null +++ b/ktorrent/view/viewjobtracker.h @@ -0,0 +1,35 @@ +/* + SPDX-FileCopyrightText: 2011 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KT_VIEWJOBTRACKER_H +#define KT_VIEWJOBTRACKER_H + +#include + +namespace kt +{ +class View; + +/** + JobTracker for the View + */ +class ViewJobTracker : public kt::JobTracker +{ + Q_OBJECT +public: + ViewJobTracker(View *parent); + ~ViewJobTracker() override; + + void jobUnregistered(bt::Job *j) override; + void jobRegistered(bt::Job *j) override; + kt::JobProgressWidget *createJobWidget(bt::Job *job) override; + +private: + View *view; +}; + +} + +#endif // KT_VIEWJOBTRACKER_H diff --git a/ktorrent/view/viewmodel.cpp b/ktorrent/view/viewmodel.cpp new file mode 100644 index 0000000..bf21dbe --- /dev/null +++ b/ktorrent/view/viewmodel.cpp @@ -0,0 +1,758 @@ +/* + SPDX-FileCopyrightText: 2007 Joris Guisson + SPDX-FileCopyrightText: 2007 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "viewmodel.h" + +#include +#include + +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include + +#include "core.h" +#include "settings.h" +#include "view.h" +#include "viewdelegate.h" + +using namespace bt; + +namespace kt +{ +ViewModel::Item::Item(bt::TorrentInterface *tc) + : tc(tc) +{ + const TorrentStats &s = tc->getStats(); + status = s.status; + bytes_downloaded = s.bytes_downloaded; + total_bytes_to_download = s.total_bytes_to_download; + bytes_uploaded = s.bytes_uploaded; + bytes_left = s.bytes_left_to_download; + download_rate = s.download_rate; + upload_rate = s.upload_rate; + eta = tc->getETA(); + seeders_connected_to = s.seeders_connected_to; + seeders_total = s.seeders_total; + leechers_total = s.leechers_total; + leechers_connected_to = s.leechers_connected_to; + percentage = Percentage(s); + share_ratio = s.shareRatio(); + runtime_dl = tc->getRunningTimeDL(); + runtime_ul = tc->getRunningTimeUL() - tc->getRunningTimeDL(); + hidden = false; + time_added = s.time_added; + highlight = false; +} + +bool ViewModel::Item::update(int row, int sort_column, QModelIndexList &to_update, kt::ViewModel *model) +{ + bool ret = false; + const TorrentStats &s = tc->getStats(); + + const auto update_if_differs = [&](auto &target, const auto &source, int column) { + if (target != source) { + to_update.append(model->index(row, column)); + target = source; + ret |= (sort_column == column); + } + }; + + const auto update_if_differs_float = [&](auto &target, const auto &source, int column) { + if (fabs(target - source) > 0.001) { + to_update.append(model->index(row, column)); + target = source; + ret |= (sort_column == column); + } + }; + + update_if_differs(status, s.status, NAME); + update_if_differs(bytes_downloaded, s.bytes_downloaded, BYTES_DOWNLOADED); + update_if_differs(total_bytes_to_download, s.total_bytes_to_download, TOTAL_BYTES_TO_DOWNLOAD); + update_if_differs(bytes_uploaded, s.bytes_uploaded, BYTES_UPLOADED); + update_if_differs(bytes_left, s.bytes_left, BYTES_LEFT); + update_if_differs(download_rate, s.download_rate, DOWNLOAD_RATE); + update_if_differs(upload_rate, s.upload_rate, UPLOAD_RATE); + update_if_differs(eta, tc->getETA(), ETA); + update_if_differs(seeders_connected_to, s.seeders_connected_to, SEEDERS); + update_if_differs(seeders_total, s.seeders_total, SEEDERS); + update_if_differs(leechers_connected_to, s.leechers_connected_to, LEECHERS); + update_if_differs(leechers_total, s.leechers_total, LEECHERS); + + update_if_differs_float(percentage, Percentage(s), PERCENTAGE); + update_if_differs_float(share_ratio, s.shareRatio(), SHARE_RATIO); + + update_if_differs(runtime_dl, tc->getRunningTimeDL(), DOWNLOAD_TIME); + // clang-format off + const auto rul = (tc->getRunningTimeUL() >= tc->getRunningTimeDL() + ? tc->getRunningTimeUL() - tc->getRunningTimeDL() + : 0); + // clang-format on + update_if_differs(runtime_ul, rul, SEED_TIME); + + return ret; +} + +QVariant ViewModel::Item::data(int col) const +{ + static QLocale locale; + const TorrentStats &s = tc->getStats(); + switch (col) { + case NAME: + return tc->getDisplayName(); + case BYTES_DOWNLOADED: + return BytesToString(bytes_downloaded); + case TOTAL_BYTES_TO_DOWNLOAD: + return BytesToString(total_bytes_to_download); + case BYTES_UPLOADED: + return BytesToString(bytes_uploaded); + case BYTES_LEFT: + return bytes_left > 0 ? BytesToString(bytes_left) : QVariant(); + case DOWNLOAD_RATE: + if (download_rate >= 103 && s.bytes_left_to_download > 0) // lowest "visible" speed, all below will be 0,0 Kb/s + return BytesPerSecToString(download_rate); + else + return QVariant(); + case UPLOAD_RATE: + if (upload_rate >= 103) // lowest "visible" speed, all below will be 0,0 Kb/s + return BytesPerSecToString(upload_rate); + else + return QVariant(); + case ETA: + if (eta == bt::TimeEstimator::NEVER) + return QString(QChar(0x221E)); // infinity + else if (eta != bt::TimeEstimator::ALREADY_FINISHED) + return DurationToString(eta); + else + return QVariant(); + case SEEDERS: + return QString(QString::number(seeders_connected_to) + QLatin1String(" (") + QString::number(seeders_total) + QLatin1Char(')')); + case LEECHERS: + return QString(QString::number(leechers_connected_to) + QLatin1String(" (") + QString::number(leechers_total) + QLatin1Char(')')); + // xgettext: no-c-format + case PERCENTAGE: + return percentage; + case SHARE_RATIO: + return locale.toString(share_ratio, 'f', 2); + case DOWNLOAD_TIME: + return DurationToString(runtime_dl); + case SEED_TIME: + return DurationToString(runtime_ul); + case DOWNLOAD_LOCATION: + return tc->getStats().output_path; + case TIME_ADDED: + return locale.toString(time_added); + default: + return QVariant(); + } +} + +bool ViewModel::Item::lessThan(int col, const Item *other) const +{ + switch (col) { + case NAME: + return QString::localeAwareCompare(tc->getDisplayName(), other->tc->getDisplayName()) < 0; + case BYTES_DOWNLOADED: + return bytes_downloaded < other->bytes_downloaded; + case TOTAL_BYTES_TO_DOWNLOAD: + return total_bytes_to_download < other->total_bytes_to_download; + case BYTES_UPLOADED: + return bytes_uploaded < other->bytes_uploaded; + case BYTES_LEFT: + return bytes_left < other->bytes_left; + case DOWNLOAD_RATE: + return (download_rate < 102 ? 0 : download_rate) < (other->download_rate < 102 ? 0 : other->download_rate); + case UPLOAD_RATE: + return (upload_rate < 102 ? 0 : upload_rate) < (other->upload_rate < 102 ? 0 : other->upload_rate); + case ETA: + return eta < other->eta; + case SEEDERS: + if (seeders_connected_to == other->seeders_connected_to) + return seeders_total < other->seeders_total; + else + return seeders_connected_to < other->seeders_connected_to; + case LEECHERS: + if (leechers_connected_to == other->leechers_connected_to) + return leechers_total < other->leechers_total; + else + return leechers_connected_to < other->leechers_connected_to; + case PERCENTAGE: + return percentage < other->percentage; + case SHARE_RATIO: + return share_ratio < other->share_ratio; + case DOWNLOAD_TIME: + return runtime_dl < other->runtime_dl; + case SEED_TIME: + return runtime_ul < other->runtime_ul; + case DOWNLOAD_LOCATION: + return tc->getStats().output_path < other->tc->getStats().output_path; + case TIME_ADDED: + return time_added < other->time_added; + default: + return false; + } +} + +QVariant ViewModel::Item::color(int col) const +{ + if (col == NAME) { + switch (status) { + case bt::SEEDING: + case bt::SUPERSEEDING: + case bt::DOWNLOADING: + case bt::ALLOCATING_DISKSPACE: + case bt::STALLED: + case bt::CHECKING_DATA: { + if (Settings::highlightTorrentNameByTrackerStatus()) { + // apply additional highlighting to torrent names + const bt::TrackersStatusInfo tsi = tc->getTrackersList()->getTrackersStatusInfo(); + if (tsi.trackers_count) { + if ((tsi.errors + tsi.warnings) == tsi.trackers_count) { + // no any OK statuses + if (tsi.timeout_errors) + return Settings::timeoutTrackerConnectionColor(); + if (tsi.warnings) + return Settings::warningsTrackerConnectionColor(); + return Settings::noTrackerConnectionColor(); + } + } + } + return (status == bt::STALLED || bt::STALLED == bt::CHECKING_DATA) ? Settings::stalledTorrentColor() : Settings::okTorrentColor(); + } + case bt::ERROR: + return Settings::errorTorrentColor(); + case bt::NOT_STARTED: + case bt::STOPPED: + case bt::QUEUED: + case bt::DOWNLOAD_COMPLETE: + case bt::SEEDING_COMPLETE: + default: + return QVariant(); + } + + } else if (col == SHARE_RATIO) { + return share_ratio >= Settings::greenRatio() ? Settings::goodShareRatioColor() : Settings::lowShareRatioColor(); + } else + return QVariant(); +} + +bool ViewModel::Item::visible(Group *group, const QString &filter_string) const +{ + if (group && !group->isMember(tc)) + return false; + + return filter_string.isEmpty() || tc->getDisplayName().contains(filter_string, Qt::CaseInsensitive); +} + +QVariant ViewModel::Item::statusIcon() const +{ + switch (tc->getStats().status) { + case NOT_STARTED: + case STOPPED: + return QIcon::fromTheme(QStringLiteral("kt-stop")); + case SEEDING_COMPLETE: + case DOWNLOAD_COMPLETE: + return QIcon::fromTheme(QStringLiteral("task-complete")); + case SEEDING: + case SUPERSEEDING: + return QIcon::fromTheme(QStringLiteral("go-up")); + case DOWNLOADING: + return QIcon::fromTheme(QStringLiteral("go-down")); + case STALLED: + if (tc->getStats().completed) + return QIcon::fromTheme(QStringLiteral("go-up")); + else + return QIcon::fromTheme(QStringLiteral("go-down")); + case ALLOCATING_DISKSPACE: + return QIcon::fromTheme(QStringLiteral("drive-harddisk")); + case ERROR: + case NO_SPACE_LEFT: + return QIcon::fromTheme(QStringLiteral("dialog-error")); + case QUEUED: + return QIcon::fromTheme(QStringLiteral("download-later")); + case CHECKING_DATA: + return QIcon::fromTheme(QStringLiteral("kt-check-data")); + case PAUSED: + return QIcon::fromTheme(QStringLiteral("kt-pause")); + default: + return QVariant(); + } +} + +//////////////////////////////////////////////////////// + +ViewModel::ViewModel(Core *core, View *parent) + : QAbstractTableModel(parent) + , core(core) + , view(parent) +{ + connect(core, &Core::aboutToQuit, this, &ViewModel::onExit); // model must be in core's thread to be notified in time + connect(core, &Core::torrentAdded, this, &ViewModel::addTorrent); + connect(core, &Core::torrentRemoved, this, &ViewModel::removeTorrent); + sort_column = 0; + sort_order = Qt::AscendingOrder; + group = nullptr; + num_visible = 0; + + const kt::QueueManager *const qman = core->getQueueManager(); + for (bt::TorrentInterface *i : *qman) { + torrents.append(new Item(i)); + num_visible++; + } +} + +ViewModel::~ViewModel() +{ + qDeleteAll(torrents); +} + +void ViewModel::setGroup(Group *g) +{ + group = g; +} + +void ViewModel::addTorrent(bt::TorrentInterface *ti) +{ + Item *i = new Item(ti); + if (Settings::highlightNewTorrents()) { + i->highlight = true; + + // Turn off highlight for previously highlighted torrents + for (Item *item : qAsConst(torrents)) + if (item->highlight) + item->highlight = false; + } + + torrents.append(i); + update(view->viewDelegate(), true); + + // Scroll to new torrent + int idx = 0; + for (Item *item : qAsConst(torrents)) { + if (item->tc == ti) { + view->scrollTo(index(idx, 0)); + break; + } + idx++; + } +} + +void ViewModel::removeTorrent(bt::TorrentInterface *ti) +{ + int idx = 0; + for (Item *item : qAsConst(torrents)) { + if (item->tc == ti) { + removeRow(idx); + update(view->viewDelegate(), true); + break; + } + idx++; + } +} + +void ViewModel::emitDataChanged(int row, int col) +{ + QModelIndex idx = createIndex(row, col); + Q_EMIT dataChanged(idx, idx); + // Q_EMIT dataChanged(createIndex(row,0),createIndex(row,14)); +} + +bool ViewModel::update(ViewDelegate *delegate, bool force_resort) +{ + update_list.clear(); + bool resort = force_resort; + num_visible = 0; + + int row = 0; + for (Item *i : qAsConst(torrents)) { + bool hidden = !i->visible(group, filter_string); + if (!hidden && i->update(row, sort_column, update_list, this)) + resort = true; + + if (hidden != i->hidden) { + i->hidden = hidden; + resort = true; + } + + // hide the extender if there is one shown + if (hidden && delegate->extended(i->tc)) + delegate->hideExtender(i->tc); + + if (!i->hidden) + num_visible++; + row++; + } + + if (resort) { + update_list.clear(); + sort(sort_column, sort_order); + return true; + } + + return false; +} + +void ViewModel::setFilterString(const QString &filter) +{ + filter_string = filter; +} + +int ViewModel::rowCount(const QModelIndex &parent) const +{ + if (parent.isValid()) + return 0; + else + return num_visible; +} + +int ViewModel::columnCount(const QModelIndex &parent) const +{ + if (parent.isValid()) + return 0; + else + return _NUMBER_OF_COLUMNS; +} + +QVariant ViewModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if (orientation != Qt::Horizontal) + return QVariant(); + + if (role == Qt::DisplayRole) { + switch (section) { + case NAME: + return i18n("Name"); + case BYTES_DOWNLOADED: + return i18n("Downloaded"); + case TOTAL_BYTES_TO_DOWNLOAD: + return i18n("Size"); + case BYTES_UPLOADED: + return i18n("Uploaded"); + case BYTES_LEFT: + return i18nc("Bytes left to downloaded", "Left"); + case DOWNLOAD_RATE: + return i18n("Down Speed"); + case UPLOAD_RATE: + return i18n("Up Speed"); + case ETA: + return i18n("Time Left"); + case SEEDERS: + return i18n("Seeders"); + case LEECHERS: + return i18n("Leechers"); + case PERCENTAGE: + // xgettext: no-c-format + return i18n("% Complete"); + case SHARE_RATIO: + return i18n("Share Ratio"); + case DOWNLOAD_TIME: + return i18n("Time Downloaded"); + case SEED_TIME: + return i18n("Time Seeded"); + case DOWNLOAD_LOCATION: + return i18n("Location"); + case TIME_ADDED: + return i18n("Added"); + default: + return QVariant(); + } + } else if (role == Qt::ToolTipRole) { + switch (section) { + case BYTES_DOWNLOADED: + return i18n("How much data we have downloaded of the torrent"); + case TOTAL_BYTES_TO_DOWNLOAD: + return i18n("Total size of the torrent, excluded files are not included"); + case BYTES_UPLOADED: + return i18n("How much data we have uploaded"); + case BYTES_LEFT: + return i18n("How much data left to download"); + case DOWNLOAD_RATE: + return i18n("Current download speed"); + case UPLOAD_RATE: + return i18n("Current upload speed"); + case ETA: + return i18n("How much time is left before the torrent is finished or before the maximum share ratio is reached, if that is enabled"); + case SEEDERS: + return i18n("How many seeders we are connected to (How many seeders there are according to the tracker)"); + case LEECHERS: + return i18n("How many leechers we are connected to (How many leechers there are according to the tracker)"); + // xgettext: no-c-format + case PERCENTAGE: + return i18n("The percentage of data we have of the whole torrent, not including excluded files"); + case SHARE_RATIO: + return i18n("Share ratio is the number of bytes uploaded divided by the number of bytes downloaded"); + case DOWNLOAD_TIME: + return i18n("How long we have been downloading the torrent"); + case SEED_TIME: + return i18n("How long we have been seeding the torrent"); + case DOWNLOAD_LOCATION: + return i18n("The location of the torrent's data on disk"); + case TIME_ADDED: + return i18n("When this torrent was added"); + default: + return QVariant(); + } + } + + return QVariant(); +} + +QModelIndex ViewModel::index(int row, int column, const QModelIndex &parent) const +{ + if (parent.isValid()) + return QModelIndex(); + + if (row < 0 || row >= torrents.count()) + return QModelIndex(); + else + return createIndex(row, column, torrents[row]); +} + +QVariant ViewModel::data(const QModelIndex &index, int role) const +{ + // there is no point checking index.row() < 0 because isValid already does this + if (!index.isValid() || index.row() >= torrents.count()) + return QVariant(); + + Item *item = reinterpret_cast(index.internalPointer()); + if (!item) + return QVariant(); + + if (role == Qt::ForegroundRole) { + return item->color(index.column()); + } else if (role == Qt::DisplayRole) { + return item->data(index.column()); + } else if (role == Qt::EditRole && index.column() == NAME) { + return item->tc->getDisplayName(); + } else if (role == Qt::DecorationRole && index.column() == NAME) { + return item->statusIcon(); + } else if (role == Qt::ToolTipRole && index.column() == NAME) { + QString tooltip; + bt::TorrentInterface *tc = item->tc; + if (tc->loadUrl().isValid()) + tooltip = i18n("%1
Url: %2", tc->getDisplayName(), tc->loadUrl().toDisplayString()); + else + tooltip = tc->getDisplayName(); + + tooltip += QLatin1String("

") + tc->getStats().statusToString(); + if (tc->getTrackersList()->noTrackersReachable()) + tooltip += i18n("

Unable to contact a tracker."); + + return tooltip; + } else if (role == Qt::TextAlignmentRole) { + switch (index.column()) { + case NAME: + case PERCENTAGE: + case DOWNLOAD_LOCATION: + case TIME_ADDED: + return Qt::AlignLeft + Qt::AlignVCenter; + default: + return Qt::AlignRight + Qt::AlignVCenter; + } + } else if (role == Qt::FontRole && item->highlight) { + QFont f = view->font(); + f.setBold(true); + return f; + } + + return QVariant(); +} + +bool ViewModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + if (!index.isValid() || index.row() >= torrents.count() || role != Qt::EditRole || index.column() != NAME) + return false; + + QString name = value.toString(); + Item *item = reinterpret_cast(index.internalPointer()); + if (!item) + return false; + + bt::TorrentInterface *tc = item->tc; + tc->setDisplayName(name); + Q_EMIT dataChanged(index, index); + if (sort_column == NAME) + sort(sort_column, sort_order); + return true; +} + +Qt::ItemFlags ViewModel::flags(const QModelIndex &index) const +{ + if (!index.isValid() || index.row() >= torrents.count()) + return QAbstractTableModel::flags(index) | Qt::ItemIsDropEnabled; + + Qt::ItemFlags flags = QAbstractTableModel::flags(index) | Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled; + if (index.column() == NAME) + flags |= Qt::ItemIsEditable; + + return flags; +} + +QStringList ViewModel::mimeTypes() const +{ + QStringList types; + types << QStringLiteral("application/x-ktorrent-drag-object"); + types << QStringLiteral("text/uri-list"); + return types; +} + +QMimeData *ViewModel::mimeData(const QModelIndexList &indexes) const +{ + QMimeData *mime_data = new QMimeData(); + QByteArray encoded_data; + + QDataStream stream(&encoded_data, QIODevice::WriteOnly); + QStringList hashes; + for (const QModelIndex &index : indexes) { + if (!index.isValid()) + continue; + + const bt::TorrentInterface *ti = torrentFromIndex(index); + if (ti) { + QString hash = ti->getInfoHash().toString(); + if (!hashes.contains(hash)) { + hashes.append(hash); + } + } + } + + for (const QString &s : qAsConst(hashes)) + stream << s; + + mime_data->setData(QStringLiteral("application/x-ktorrent-drag-object"), encoded_data); + return mime_data; +} + +bool ViewModel::dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) +{ + Q_UNUSED(row); + Q_UNUSED(column); + Q_UNUSED(parent); + if (action == Qt::IgnoreAction) + return true; + + if (!data->hasUrls()) + return false; + + const QList files = data->urls(); + for (const QUrl &file : files) { + core->load(file, QString()); + } + + return true; +} + +Qt::DropActions ViewModel::supportedDropActions() const +{ + return Qt::CopyAction | Qt::MoveAction; +} + +void ViewModel::torrentsFromIndexList(const QModelIndexList &idx, QList &tlist) +{ + for (const QModelIndex &i : idx) { + bt::TorrentInterface *tc = torrentFromIndex(i); + if (tc) + tlist.append(tc); + } +} + +bt::TorrentInterface *ViewModel::torrentFromIndex(const QModelIndex &index) const +{ + if (index.isValid() && index.row() < torrents.count()) + return torrents[index.row()]->tc; + else + return nullptr; +} + +bt::TorrentInterface *ViewModel::torrentFromRow(int index) const +{ + if (index < torrents.count() && index >= 0) + return torrents[index]->tc; + else + return nullptr; +} + +void ViewModel::allTorrents(QList &tlist) const +{ + for (Item *item : qAsConst(torrents)) { + if (item->visible(group, filter_string)) + tlist.append(item->tc); + } +} + +bool ViewModel::insertRows(int row, int count, const QModelIndex &parent) +{ + Q_UNUSED(parent); + beginInsertRows(QModelIndex(), row, row + count - 1); + endInsertRows(); + return true; +} + +bool ViewModel::removeRows(int row, int count, const QModelIndex &parent) +{ + Q_UNUSED(parent); + beginRemoveRows(QModelIndex(), row, row + count - 1); + for (int i = 0; i < count; i++) { + Item *item = torrents[row + i]; + delete item; + } + torrents.remove(row, count); + endRemoveRows(); + return true; +} + +void ViewModel::onExit() +{ + // items should be removed before Core delete their tc data. + removeRows(0, rowCount(), QModelIndex()); +} + +class ViewModelItemCmp +{ +public: + ViewModelItemCmp(int col, Qt::SortOrder order) + : col(col) + , order(order) + { + } + + bool operator()(ViewModel::Item *a, ViewModel::Item *b) + { + if (a->hidden) + return false; + else if (b->hidden) + return true; + else if (order == Qt::AscendingOrder) + return a->lessThan(col, b); + else + return b->lessThan(col, a); + } + + int col; + Qt::SortOrder order; +}; + +void ViewModel::sort(int col, Qt::SortOrder order) +{ + sort_column = col; + sort_order = order; + Q_EMIT layoutAboutToBeChanged(); + std::stable_sort(torrents.begin(), torrents.end(), ViewModelItemCmp(col, order)); + Q_EMIT layoutChanged(); + Q_EMIT sorted(); +} +} diff --git a/ktorrent/view/viewmodel.h b/ktorrent/view/viewmodel.h new file mode 100644 index 0000000..736f9a5 --- /dev/null +++ b/ktorrent/view/viewmodel.h @@ -0,0 +1,203 @@ +/* + SPDX-FileCopyrightText: 2007 Joris Guisson + SPDX-FileCopyrightText: 2007 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KTVIEWMODEL_H +#define KTVIEWMODEL_H + +#include +#include + +#include +#include + +namespace bt +{ +class TorrentInterface; +} + +namespace kt +{ +class View; +class ViewDelegate; +class Core; +class Group; + +/** + * @author Joris Guisson + * + * Model for the main torrent view + */ +class ViewModel : public QAbstractTableModel +{ + Q_OBJECT +public: + ViewModel(Core *core, View *parent); + ~ViewModel() override; + + /** + * Set the Group to filter + * @param g The group + */ + void setGroup(Group *g); + + /** + * Update the model, checks if data has changed. + $ @param delegate The ViewDelegate, so we don't hide extended items + * @param force_resort Force a resort + * @return true if the model got resorted + */ + bool update(ViewDelegate *delegate, bool force_resort = false); + + /** + * Set the current filter string + * @param filter The filter string + */ + void setFilterString(const QString &filter); + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + int columnCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant headerData(int section, Qt::Orientation orientation, int role) const override; + QVariant data(const QModelIndex &index, int role) const override; + bool removeRows(int row, int count, const QModelIndex &parent) override; + bool insertRows(int row, int count, const QModelIndex &parent) override; + Qt::ItemFlags flags(const QModelIndex &index) const override; + QStringList mimeTypes() const override; + QMimeData *mimeData(const QModelIndexList &indexes) const override; + bool setData(const QModelIndex &index, const QVariant &value, int role) override; + QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override; + bool dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) override; + Qt::DropActions supportedDropActions() const override; + + /** + * Emit the data changed signal + * @param row The row + * @param col The column + */ + void emitDataChanged(int row, int col); + + /** + * Get a list of torrents from an index list. + * @param idx The index list + * @param tlist The torrent list to fill + */ + void torrentsFromIndexList(const QModelIndexList &idx, QList &tlist); + + /** + * Get a torrent from a model index. + * @param index The model index + * @return The torrent if the index is valid and in the proper range, 0 otherwise + */ + bt::TorrentInterface *torrentFromIndex(const QModelIndex &index) const; + + /** + * Get a torrent from a row. + * @param index The row index + * @return The torrent if the index is valid and in the proper range, 0 otherwise + */ + bt::TorrentInterface *torrentFromRow(int index) const; + + /** + * Get all torrents + * @param tlist The list of torrents to fill + */ + void allTorrents(QList &tlist) const; + + /** + * Visit all visible torrents in the model, and apply an action to them + */ + template void visit(Action &a) + { + for (Item *item : qAsConst(torrents)) { + if (item->visible(group, filter_string)) + if (!a(item->tc)) + break; + } + } + + /// Get the list of indexes which need to be updated + const QModelIndexList &updateList() const + { + return update_list; + } + +public Q_SLOTS: + void addTorrent(bt::TorrentInterface *ti); + void removeTorrent(bt::TorrentInterface *ti); + void sort(int col, Qt::SortOrder order) override; + void onExit(); + +Q_SIGNALS: + void sorted(); + +public: + enum Column { + NAME = 0, + BYTES_DOWNLOADED, + TOTAL_BYTES_TO_DOWNLOAD, + BYTES_LEFT, + BYTES_UPLOADED, + DOWNLOAD_RATE, + UPLOAD_RATE, + ETA, + SEEDERS, + LEECHERS, + PERCENTAGE, + SHARE_RATIO, + DOWNLOAD_TIME, + SEED_TIME, + DOWNLOAD_LOCATION, + TIME_ADDED, + _NUMBER_OF_COLUMNS, + }; + + struct Item { + bt::TorrentInterface *tc; + // cached values to avoid unneeded updates + bt::TorrentStatus status; + bt::Uint64 bytes_downloaded; + bt::Uint64 bytes_uploaded; + bt::Uint64 total_bytes_to_download; + bt::Uint64 bytes_left; + bt::Uint32 download_rate; + bt::Uint32 upload_rate; + bt::Uint32 seeders_total; + bt::Uint32 seeders_connected_to; + bt::Uint32 leechers_total; + bt::Uint32 leechers_connected_to; + double percentage; + float share_ratio; + bt::Uint32 runtime_dl; + bt::Uint32 runtime_ul; + int eta; + bool hidden; + QDateTime time_added; + bool highlight; + + Item(bt::TorrentInterface *tc); + + bool update(int row, int sort_column, QModelIndexList &to_update, ViewModel *model); + QVariant data(int col) const; + QVariant color(int col) const; + QVariant statusIcon() const; + bool lessThan(int col, const Item *other) const; + bool visible(Group *group, const QString &filter_string) const; + }; + +private: + Core *core; + View *view; + QVector torrents; + int sort_column; + Qt::SortOrder sort_order; + Group *group; + int num_visible; + QModelIndexList update_list; + QString filter_string; +}; + +} + +#endif diff --git a/ktorrent/view/viewselectionmodel.cpp b/ktorrent/view/viewselectionmodel.cpp new file mode 100644 index 0000000..b936c7c --- /dev/null +++ b/ktorrent/view/viewselectionmodel.cpp @@ -0,0 +1,22 @@ +/* + SPDX-FileCopyrightText: 2009 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "viewselectionmodel.h" +#include "viewmodel.h" +#include + +using namespace bt; + +namespace kt +{ +ViewSelectionModel::ViewSelectionModel(ViewModel *vm, QObject *parent) + : ItemSelectionModel(vm, parent) +{ +} + +ViewSelectionModel::~ViewSelectionModel() +{ +} +} diff --git a/ktorrent/view/viewselectionmodel.h b/ktorrent/view/viewselectionmodel.h new file mode 100644 index 0000000..6f05765 --- /dev/null +++ b/ktorrent/view/viewselectionmodel.h @@ -0,0 +1,32 @@ +/* + SPDX-FileCopyrightText: 2009 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KTVIEWSELECTIONMODEL_H +#define KTVIEWSELECTIONMODEL_H + +#include + +namespace bt +{ +} + +namespace kt +{ +class ViewModel; + +/** + Custom selection model for View +*/ +class ViewSelectionModel : public ItemSelectionModel +{ + Q_OBJECT +public: + ViewSelectionModel(ViewModel *vm, QObject *parent); + ~ViewSelectionModel() override; +}; + +} + +#endif diff --git a/ktupnptest/CMakeLists.txt b/ktupnptest/CMakeLists.txt new file mode 100644 index 0000000..f95e8a7 --- /dev/null +++ b/ktupnptest/CMakeLists.txt @@ -0,0 +1,15 @@ +add_executable(ktupnptest) + +target_sources(ktupnptest PRIVATE main.cpp upnptestwidget.cpp) + +ki18n_wrap_ui(ktupnptest upnptestwidget.ui) + +target_link_libraries( + ktupnptest + Qt5::Network + KF5::Torrent + KF5::CoreAddons + KF5::I18n +) +install(TARGETS ktupnptest ${INSTALL_TARGETS_DEFAULT_ARGS}) + diff --git a/ktupnptest/main.cpp b/ktupnptest/main.cpp new file mode 100644 index 0000000..10871a3 --- /dev/null +++ b/ktupnptest/main.cpp @@ -0,0 +1,56 @@ +/* + SPDX-FileCopyrightText: 2005-2007 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include +#include + +#include +#include +#include +#include + +#include +#include + +#include "upnptestwidget.h" +#include +#include + +int main(int argc, char **argv) +{ + QApplication app(argc, argv); + KAboutData about(QStringLiteral("ktupnp"), + i18n("KTUPnPTest"), + QStringLiteral("1.0"), + i18n("KTorrent's UPnP test application"), + KAboutLicense::GPL, + i18n("© 2005 - 2007 Joris Guisson and Ivan Vasic"), + QString(), + QStringLiteral("http://www.kde.org/applications/internet/ktorrent/")); + KAboutData::setApplicationData(about); + + QCommandLineParser parser; + parser.addVersionOption(); + parser.addHelpOption(); + about.setupCommandLine(&parser); + parser.process(app); + about.processCommandLine(&parser); + + if (!bt::InitLibKTorrent()) { + fprintf(stderr, "Failed to initialize libktorrent\n"); + return -1; + } + + QString str = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QStringLiteral("/ktorrent"); + if (!str.endsWith(bt::DirSeparator())) + str += bt::DirSeparator(); + + bt::InitLog(str + QStringLiteral("ktupnptest.log")); + UPnPTestWidget *mwnd = new UPnPTestWidget(); + + mwnd->show(); + app.exec(); + return 0; +} diff --git a/ktupnptest/upnptestwidget.cpp b/ktupnptest/upnptestwidget.cpp new file mode 100644 index 0000000..1b8aa25 --- /dev/null +++ b/ktupnptest/upnptestwidget.cpp @@ -0,0 +1,88 @@ +/* + SPDX-FileCopyrightText: 2007 Joris Guisson + SPDX-FileCopyrightText: 2007 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "upnptestwidget.h" +#include +#include +#include + +using namespace bt; + +UPnPTestWidget::UPnPTestWidget(QWidget *parent) + : QWidget(parent) +{ + setupUi(this); + connect(m_find_routers, &QPushButton::clicked, this, &UPnPTestWidget::findRouters); + connect(m_forward, &QPushButton::clicked, this, &UPnPTestWidget::doForward); + connect(m_undo_forward, &QPushButton::clicked, this, &UPnPTestWidget::undoForward); + connect(m_verbose, &QCheckBox::toggled, this, &UPnPTestWidget::verboseModeChecked); + mcast_socket = nullptr; + router = nullptr; + + m_forward->setEnabled(false); + m_undo_forward->setEnabled(false); + m_port->setEnabled(false); + m_protocol->setEnabled(false); + + AddLogMonitor(this); +} + +UPnPTestWidget::~UPnPTestWidget() +{ + delete mcast_socket; +} + +void UPnPTestWidget::doForward() +{ + QString proto = m_protocol->currentText(); + bt::Uint16 port = m_port->value(); + Out(SYS_GEN | LOG_DEBUG) << "Forwarding port " << port << " (" << proto << ")" << endl; + net::Port p(port, proto == QStringLiteral("UDP") ? net::UDP : net::TCP, true); + router->forward(p); +} + +void UPnPTestWidget::undoForward() +{ + QString proto = m_protocol->currentText(); + bt::Uint16 port = m_port->value(); + Out(SYS_GEN | LOG_DEBUG) << "Unforwarding port " << port << " (" << proto << ")" << endl; + net::Port p(port, proto == QStringLiteral("UDP") ? net::UDP : net::TCP, true); + router->undoForward(p); +} + +void UPnPTestWidget::findRouters() +{ + Out(SYS_GEN | LOG_DEBUG) << "Searching for routers ..." << endl; + if (!mcast_socket) { + mcast_socket = new UPnPMCastSocket(m_verbose->isChecked()); + connect(mcast_socket, &UPnPMCastSocket::discovered, this, &UPnPTestWidget::discovered); + } + + mcast_socket->discover(); +} + +void UPnPTestWidget::discovered(bt::UPnPRouter *r) +{ + router = r; + m_router->setText(router->getServer()); + m_forward->setEnabled(true); + m_undo_forward->setEnabled(true); + m_port->setEnabled(true); + m_protocol->setEnabled(true); + router->setVerbose(true); +} + +void UPnPTestWidget::message(const QString &line, unsigned int arg) +{ + Q_UNUSED(arg); + m_text_output->append(line); +} + +void UPnPTestWidget::verboseModeChecked(bool on) +{ + if (mcast_socket) + mcast_socket->setVerbose(on); +} diff --git a/ktupnptest/upnptestwidget.h b/ktupnptest/upnptestwidget.h new file mode 100644 index 0000000..4d7e065 --- /dev/null +++ b/ktupnptest/upnptestwidget.h @@ -0,0 +1,38 @@ +/* + SPDX-FileCopyrightText: 2005-2007 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef UPNPTESTWIDGET_HH +#define UPNPTESTWIDGET_HH + +#include "ui_upnptestwidget.h" +#include +#include + +namespace bt +{ +class UPnPMCastSocket; +class UPnPRouter; +} + +class UPnPTestWidget : public QWidget, public Ui_UPnPTestWidget, public bt::LogMonitorInterface +{ +public: + UPnPTestWidget(QWidget *parent = nullptr); + ~UPnPTestWidget() override; + + void doForward(); + void undoForward(); + void findRouters(); + void discovered(bt::UPnPRouter *r); + void verboseModeChecked(bool on); + +private: + void message(const QString &line, unsigned int arg) override; + + bt::UPnPMCastSocket *mcast_socket; + bt::UPnPRouter *router; +}; + +#endif diff --git a/ktupnptest/upnptestwidget.ui b/ktupnptest/upnptestwidget.ui new file mode 100644 index 0000000..fb0585d --- /dev/null +++ b/ktupnptest/upnptestwidget.ui @@ -0,0 +1,149 @@ + + + UPnPTestWidget + + + + 0 + 0 + 684 + 418 + + + + UPnP Test Application + + + + + + + + + + Router: + + + + + + + + 0 + 0 + + + + QFrame::Panel + + + No routers found. + + + + + + + + + Find Routers + + + + + + + + + + + + + + + + Port: + + + + + + + 1 + + + 65535 + + + 4000 + + + + + + + + + + + Protocol: + + + + + + + + UDP + + + + + TCP + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Forward + + + + + + + Undo Forward + + + + + + + Verbose mode + + + + + + + + + + diff --git a/libktcore/CMakeLists.txt b/libktcore/CMakeLists.txt new file mode 100644 index 0000000..f5340b2 --- /dev/null +++ b/libktcore/CMakeLists.txt @@ -0,0 +1,77 @@ +configure_file(${CMAKE_CURRENT_SOURCE_DIR}/config-ktcore.h.cmake ${CMAKE_BINARY_DIR}/config-ktcore.h) + +include_directories(${CMAKE_BINARY_DIR}) + +add_library(ktcore) +set_target_properties(ktcore PROPERTIES + VERSION 16.0.0 + SOVERSION 16 +) + +target_sources(ktcore PRIVATE + util/mmapfile.cpp + util/itemselectionmodel.cpp + util/stringcompletionmodel.cpp + util/treefiltermodel.cpp + + interfaces/functions.cpp + interfaces/plugin.cpp + interfaces/guiinterface.cpp + interfaces/coreinterface.cpp + interfaces/prefpageinterface.cpp + interfaces/activity.cpp + interfaces/torrentactivityinterface.cpp + + torrent/queuemanager.cpp + torrent/magnetmanager.cpp + torrent/torrentfilemodel.cpp + torrent/torrentfiletreemodel.cpp + torrent/torrentfilelistmodel.cpp + torrent/chunkbar.cpp + torrent/chunkbarrenderer.cpp + torrent/jobtracker.cpp + torrent/jobprogresswidget.cpp + torrent/basicjobprogresswidget.cpp + + groups/group.cpp + groups/torrentgroup.cpp + groups/allgroup.cpp + groups/ungroupedgroup.cpp + groups/groupmanager.cpp + groups/functiongroup.cpp + + dbus/dbus.cpp + dbus/dbustorrent.cpp + dbus/dbusgroup.cpp + dbus/dbussettings.cpp + dbus/dbustorrentfilestream.cpp + + gui/centralwidget.cpp + gui/tabbarwidget.cpp + gui/extender.cpp + + plugin/pluginmanager.cpp + plugin/pluginactivity.cpp + + ktorrent.kcfg +) + +ki18n_wrap_ui(ktcore torrent/basicjobprogresswidget.ui) +kconfig_add_kcfg_files(ktcore settings.kcfgc) +generate_export_header(ktcore BASE_NAME ktcore) + +target_link_libraries(ktcore PUBLIC + KF5::ConfigCore + KF5::CoreAddons + KF5::I18n + KF5::KCMUtils + KF5::Parts + KF5::Solid + KF5::Torrent + KF5::XmlGui +) + +target_include_directories(ktcore PUBLIC "$") + +install(TARGETS ktcore ${INSTALL_TARGETS_DEFAULT_ARGS} LIBRARY NAMELINK_SKIP) + diff --git a/libktcore/config-ktcore.h.cmake b/libktcore/config-ktcore.h.cmake new file mode 100644 index 0000000..8de5501 --- /dev/null +++ b/libktcore/config-ktcore.h.cmake @@ -0,0 +1,24 @@ +#ifndef CONFIG_KTORRENT_H +#define CONFIG_KTORRENT_H + +#cmakedefine HAVE_FOPEN64 1 +#cmakedefine HAVE_FSEEKO64 1 +#cmakedefine HAVE_FSEEKO 1 +#cmakedefine HAVE_FTELLO64 1 +#cmakedefine HAVE_FTELLO 1 +#cmakedefine HAVE_FSTAT64 1 +#cmakedefine HAVE_FTRUNCATE64 1 +#cmakedefine HAVE_LSEEK64 1 +#cmakedefine HAVE_STAT64 1 +#cmakedefine HAVE_MMAP64 1 +#cmakedefine HAVE_MUNMAP64 1 +#cmakedefine HAVE_POSIX_FALLOCATE64 1 +#cmakedefine HAVE_POSIX_FALLOCATE 1 +#cmakedefine HAVE_STATVFS 1 +#cmakedefine HAVE_STATVFS64 1 +#cmakedefine HAVE_XFS_XFS_H 1 +#cmakedefine HAVE___U64 1 +#cmakedefine HAVE___S64 1 + +#endif + diff --git a/libktcore/dbus/dbus.cpp b/libktcore/dbus/dbus.cpp new file mode 100644 index 0000000..3c15273 --- /dev/null +++ b/libktcore/dbus/dbus.cpp @@ -0,0 +1,284 @@ +/* + SPDX-FileCopyrightText: 2007 Joris Guisson + SPDX-FileCopyrightText: 2007 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include +#include +#include + +#include + +#include "dbus.h" +#include "dbusgroup.h" +#include "dbussettings.h" +#include "dbustorrent.h" +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace bt; + +namespace kt +{ +DBus::DBus(GUIInterface *gui, CoreInterface *core, QObject *parent) + : QObject(parent) + , gui(gui) + , core(core) +{ + torrent_map.setAutoDelete(true); + group_map.setAutoDelete(true); + + QDBusConnection::sessionBus().registerObject(QLatin1String("/core"), + this, + QDBusConnection::ExportScriptableSlots | QDBusConnection::ExportScriptableSignals); + + connect(core, &CoreInterface::torrentAdded, this, qOverload(&DBus::torrentAdded)); + connect(core, &CoreInterface::torrentRemoved, this, qOverload(&DBus::torrentRemoved)); + connect(core, &CoreInterface::torrentStoppedByError, this, qOverload(&DBus::torrentStoppedByError)); + connect(core, &CoreInterface::finished, this, qOverload(&DBus::finished)); + connect(core, &CoreInterface::settingsChanged, this, &DBus::settingsChanged); + + // fill the map with torrents + const kt::QueueManager *const qman = core->getQueueManager(); + for (bt::TorrentInterface *i : *qman) { + torrentAdded(i); + } + + connect(qman, &kt::QueueManager::suspendStateChanged, this, &DBus::suspendStateChanged); + + kt::GroupManager *gman = core->getGroupManager(); + connect(gman, &kt::GroupManager::groupAdded, this, &DBus::groupAdded); + connect(gman, &kt::GroupManager::groupRemoved, this, &DBus::groupRemoved); + kt::GroupManager::Itr i = gman->begin(); + while (i != gman->end()) { + if (i->second->groupFlags() & Group::CUSTOM_GROUP) + groupAdded(i->second); + i++; + } + + dbus_settings = new DBusSettings(core, this); +} + +DBus::~DBus() +{ +} + +QStringList DBus::torrents() +{ + QStringList tors; + DBusTorrentItr i = torrent_map.begin(); + while (i != torrent_map.end()) { + tors.append(i->first); + i++; + } + + return tors; +} + +void DBus::start(const QString &info_hash) +{ + DBusTorrent *tc = torrent_map.find(info_hash); + if (!tc) + return; + + core->getQueueManager()->start(tc->torrent()); +} + +void DBus::stop(const QString &info_hash) +{ + DBusTorrent *tc = torrent_map.find(info_hash); + if (!tc) + return; + + core->getQueueManager()->stop(tc->torrent()); +} + +void DBus::startAll() +{ + core->startAll(); +} + +void DBus::stopAll() +{ + core->stopAll(); +} + +void DBus::torrentAdded(bt::TorrentInterface *tc) +{ + DBusTorrent *db = new DBusTorrent(tc, this); + torrent_map.insert(db->infoHash(), db); + torrentAdded(db->infoHash()); +} + +void DBus::torrentRemoved(bt::TorrentInterface *tc) +{ + DBusTorrent *db = torrent_map.find(tc->getInfoHash().toString()); + if (db) { + QString ih = db->infoHash(); + torrentRemoved(ih); + torrent_map.erase(ih); + } +} + +void DBus::finished(bt::TorrentInterface *tc) +{ + DBusTorrent *db = torrent_map.find(tc->getInfoHash().toString()); + if (db) { + QString ih = db->infoHash(); + finished(ih); + } +} + +void DBus::torrentStoppedByError(bt::TorrentInterface *tc, QString msg) +{ + DBusTorrent *db = torrent_map.find(tc->getInfoHash().toString()); + if (db) { + QString ih = db->infoHash(); + torrentStoppedByError(ih, msg); + } +} + +void DBus::load(const QString &url, const QString &group) +{ + core->load(QFile::exists(url) ? QUrl::fromLocalFile(url) : QUrl(url), group); +} + +void DBus::loadSilently(const QString &url, const QString &group) +{ + core->loadSilently(QFile::exists(url) ? QUrl::fromLocalFile(url) : QUrl(url), group); +} + +QStringList DBus::groups() const +{ + QStringList ret; + kt::GroupManager *gman = core->getGroupManager(); + kt::GroupManager::Itr i = gman->begin(); + while (i != gman->end()) { + if (i->second->groupFlags() & Group::CUSTOM_GROUP) + ret << i->first; + i++; + } + return ret; +} + +bool DBus::addGroup(const QString &group) +{ + kt::GroupManager *gman = core->getGroupManager(); + return gman->newGroup(group) != nullptr; +} + +bool DBus::removeGroup(const QString &group) +{ + kt::GroupManager *gman = core->getGroupManager(); + Group *g = gman->find(group); + if (!g) + return false; + + gman->removeGroup(g); + return true; +} + +void DBus::groupAdded(kt::Group *g) +{ + if (g->groupFlags() & Group::CUSTOM_GROUP) + group_map.insert(g, new DBusGroup(g, core->getGroupManager(), this)); +} + +void DBus::groupRemoved(kt::Group *g) +{ + group_map.erase(g); +} + +QObject *DBus::torrent(const QString &info_hash) +{ + return torrent_map.find(info_hash); +} + +QObject *DBus::group(const QString &name) +{ + kt::GroupManager *gman = core->getGroupManager(); + kt::GroupManager::Itr i = gman->begin(); + while (i != gman->end()) { + if (i->first == name) + return group_map.find(i->second); + i++; + } + return nullptr; +} + +void DBus::log(const QString &line) +{ + Out(SYS_GEN | LOG_NOTICE) << line << endl; +} + +void DBus::remove(const QString &info_hash, bool data_to) +{ + DBusTorrent *tc = torrent_map.find(info_hash); + if (!tc) + return; + + core->remove(tc->torrent(), data_to); +} + +void DBus::removeDelayed(const QString &info_hash, bool data_to) +{ + delayed_removal_map.insert(info_hash, data_to); + QTimer::singleShot(500, this, &DBus::delayedTorrentRemoval); +} + +void DBus::delayedTorrentRemoval() +{ + for (QMap::const_iterator i = delayed_removal_map.cbegin(); i != delayed_removal_map.cend(); i++) + remove(i.key(), i.value()); + + delayed_removal_map.clear(); +} + +void DBus::setSuspended(bool suspend) +{ + core->setSuspendedState(suspend); +} + +bool DBus::suspended() +{ + return core->getSuspendedState(); +} + +uint DBus::numTorrentsRunning() const +{ + return core->getNumTorrentsRunning(); +} + +uint DBus::numTorrentsNotRunning() const +{ + return core->getNumTorrentsNotRunning(); +} + +QString DBus::dataDir() const +{ + return kt::DataDir(); +} + +void DBus::orderQueue() +{ + core->getQueueManager()->orderQueue(); +} + +void DBus::reindexQueue() +{ + core->getQueueManager()->reindexQueue(); +} + +QObject *DBus::settings() +{ + return dbus_settings; +} + +} diff --git a/libktcore/dbus/dbus.h b/libktcore/dbus/dbus.h new file mode 100644 index 0000000..526c31d --- /dev/null +++ b/libktcore/dbus/dbus.h @@ -0,0 +1,154 @@ +/* + SPDX-FileCopyrightText: 2007 Joris Guisson + SPDX-FileCopyrightText: 2007 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KT_DBUS_HH +#define KT_DBUS_HH + +#include +#include +#include + +#include +#include +#include +#include + +namespace bt +{ +class TorrentInterface; +} + +namespace kt +{ +class GUIInterface; +class CoreInterface; +class Group; +class DBusSettings; + +/** + * Class which handles DBus calls + * */ +class KTCORE_EXPORT DBus : public QObject +{ + Q_OBJECT + Q_CLASSINFO("D-Bus Interface", "org.ktorrent.core") +public: + DBus(GUIInterface *gui, CoreInterface *core, QObject *parent); + ~DBus() override; + +public Q_SLOTS: + /// Get the names of all torrents + Q_SCRIPTABLE QStringList torrents(); + + /// Start a torrent + Q_SCRIPTABLE void start(const QString &info_hash); + + /// Stop a torrent + Q_SCRIPTABLE void stop(const QString &info_hash); + + /// Start all torrents + Q_SCRIPTABLE void startAll(); + + /// Stop all torrents + Q_SCRIPTABLE void stopAll(); + + /// Load a torrent + Q_SCRIPTABLE void load(const QString &url, const QString &group); + + /// Load a torrent silently + Q_SCRIPTABLE void loadSilently(const QString &url, const QString &group); + + /// Remove a torrent + Q_SCRIPTABLE void remove(const QString &info_hash, bool data_to); + + /// Remove a torrent delayed (should be used from signal handlers) + Q_SCRIPTABLE void removeDelayed(const QString &info_hash, bool data_to); + + /// Set the suspended state + Q_SCRIPTABLE void setSuspended(bool suspend); + + /// Gets the global suspended state + Q_SCRIPTABLE bool suspended(); + + /// Order the Queue + Q_SCRIPTABLE void orderQueue(); + + /// Reindex the queue + Q_SCRIPTABLE void reindexQueue(); + + /// Get all the custom groups + Q_SCRIPTABLE QStringList groups() const; + + /// Add a group + Q_SCRIPTABLE bool addGroup(const QString &group); + + /// Remove a group + Q_SCRIPTABLE bool removeGroup(const QString &group); + + /// Get a torrent (this is for scripting) + Q_SCRIPTABLE QObject *torrent(const QString &info_hash); + + /// Get a group (this is for scripting) + Q_SCRIPTABLE QObject *group(const QString &name); + + /// Get the settings (this is for scripting) + Q_SCRIPTABLE QObject *settings(); + + /// Write something to the log + Q_SCRIPTABLE void log(const QString &line); + + /// Get the number of torrents running. + Q_SCRIPTABLE uint numTorrentsRunning() const; + + /// Get the number of torrents not running. + Q_SCRIPTABLE uint numTorrentsNotRunning() const; + + /// Get the number of torrents not running. + Q_SCRIPTABLE QString dataDir() const; + +private Q_SLOTS: + void torrentAdded(bt::TorrentInterface *tc); + void torrentRemoved(bt::TorrentInterface *tc); + void finished(bt::TorrentInterface *tc); + void torrentStoppedByError(bt::TorrentInterface *tc, QString msg); + void groupAdded(Group *g); + void groupRemoved(Group *g); + void delayedTorrentRemoval(); + +Q_SIGNALS: + /// DBus signal emitted when a torrent has been added + Q_SCRIPTABLE void torrentAdded(const QString &tor); + + /// DBus signal emitted when a torrent has been removed + Q_SCRIPTABLE void torrentRemoved(const QString &tor); + + /// Emitted when torrent is finished + Q_SCRIPTABLE void finished(const QString &tor); + + /// Emitted when a torrent is stopped by an error + Q_SCRIPTABLE void torrentStoppedByError(const QString &tor, const QString &msg); + + /// Emitted when settings are changed in settings dialog + Q_SCRIPTABLE void settingsChanged(); + + /// Emitted when suspended state changes + Q_SCRIPTABLE void suspendStateChanged(bool suspended); + +private: + GUIInterface *gui; + CoreInterface *core; + bt::PtrMap torrent_map; + bt::PtrMap group_map; + QMap delayed_removal_map; + DBusSettings *dbus_settings; + + typedef bt::PtrMap::iterator DBusTorrentItr; + typedef bt::PtrMap::iterator DBusGroupItr; +}; + +} + +#endif diff --git a/libktcore/dbus/dbusgroup.cpp b/libktcore/dbus/dbusgroup.cpp new file mode 100644 index 0000000..2094716 --- /dev/null +++ b/libktcore/dbus/dbusgroup.cpp @@ -0,0 +1,168 @@ +/* + SPDX-FileCopyrightText: 2008 Joris Guisson + SPDX-FileCopyrightText: 2008 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include + +#include "dbusgroup.h" +#include +#include + +namespace kt +{ +static bool ValidCharacter(const QChar &c) +{ + ushort u = c.unicode(); + return (u >= 'a' && u <= 'z') || (u >= 'A' && u <= 'Z') || (u >= '0' && u <= '9') || (u == '_'); +} + +static bool ValidDBusName(const QString &s) +{ + if (s.isEmpty()) + return false; + + QString ret = s; + if (ret[0].isDigit()) + return false; + + for (int i = 0; i < s.length(); i++) { + QChar c = ret[i]; + if (!ValidCharacter(c)) + return false; + } + + return true; +} + +DBusGroup::DBusGroup(Group *g, GroupManager *gman, QObject *parent) + : QObject(parent) + , group(g) + , gman(gman) +{ + QString name = g->groupName(); + if (!ValidDBusName(name)) { + static int invalid_groups = 0; + while (true) { + name = QStringLiteral("group_") + QString::number(invalid_groups++); + if (!gman->find(name)) + break; + } + } + QString path = QStringLiteral("/group/") + name; + QDBusConnection::sessionBus().registerObject(path, this, QDBusConnection::ExportScriptableSlots | QDBusConnection::ExportScriptableSignals); +} + +DBusGroup::~DBusGroup() +{ +} + +QString DBusGroup::name() const +{ + return group->groupName(); +} + +QString DBusGroup::icon() const +{ + return group->groupIconName(); +} + +QString DBusGroup::defaultSaveLocation() const +{ + const Group::Policy &p = group->groupPolicy(); + return p.default_save_location; +} + +void DBusGroup::setDefaultSaveLocation(const QString &dir) +{ + Group::Policy p = group->groupPolicy(); + p.default_save_location = dir; + group->setGroupPolicy(p); + gman->saveGroups(); +} + +QString DBusGroup::defaultMoveOnCompletionLocation() const +{ + const Group::Policy &p = group->groupPolicy(); + return p.default_move_on_completion_location; +} + +void DBusGroup::setDefaultMoveOnCompletionLocation(const QString &dir) +{ + Group::Policy p = group->groupPolicy(); + p.default_move_on_completion_location = dir; + group->setGroupPolicy(p); + gman->saveGroups(); +} + +double DBusGroup::maxShareRatio() const +{ + const Group::Policy &p = group->groupPolicy(); + return p.max_share_ratio; +} + +void DBusGroup::setMaxShareRatio(double ratio) +{ + Group::Policy p = group->groupPolicy(); + p.max_share_ratio = ratio; + group->setGroupPolicy(p); + gman->saveGroups(); +} + +double DBusGroup::maxSeedTime() const +{ + const Group::Policy &p = group->groupPolicy(); + return p.max_seed_time; +} + +void DBusGroup::setMaxSeedTime(double hours) +{ + Group::Policy p = group->groupPolicy(); + p.max_seed_time = hours; + group->setGroupPolicy(p); + gman->saveGroups(); +} + +uint DBusGroup::maxUploadSpeed() const +{ + const Group::Policy &p = group->groupPolicy(); + return p.max_upload_rate; +} + +void DBusGroup::setMaxUploadSpeed(uint speed) +{ + Group::Policy p = group->groupPolicy(); + p.max_upload_rate = speed; + group->setGroupPolicy(p); + gman->saveGroups(); +} + +uint DBusGroup::maxDownloadSpeed() const +{ + const Group::Policy &p = group->groupPolicy(); + return p.max_download_rate; +} + +void DBusGroup::setMaxDownloadSpeed(uint speed) +{ + Group::Policy p = group->groupPolicy(); + p.max_download_rate = speed; + group->setGroupPolicy(p); + gman->saveGroups(); +} + +bool DBusGroup::onlyApplyOnNewTorrents() const +{ + const Group::Policy &p = group->groupPolicy(); + return p.only_apply_on_new_torrents; +} + +void DBusGroup::setOnlyApplyOnNewTorrents(bool on) +{ + Group::Policy p = group->groupPolicy(); + p.only_apply_on_new_torrents = on; + group->setGroupPolicy(p); + gman->saveGroups(); +} +} diff --git a/libktcore/dbus/dbusgroup.h b/libktcore/dbus/dbusgroup.h new file mode 100644 index 0000000..61636df --- /dev/null +++ b/libktcore/dbus/dbusgroup.h @@ -0,0 +1,53 @@ +/* + SPDX-FileCopyrightText: 2008 Joris Guisson + SPDX-FileCopyrightText: 2008 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KTDBUSGROUP_H +#define KTDBUSGROUP_H + +#include + +namespace kt +{ +class Group; +class GroupManager; + +/** + @author +*/ +class DBusGroup : public QObject +{ + Q_OBJECT + Q_CLASSINFO("D-Bus Interface", "org.ktorrent.group") +public: + DBusGroup(Group *g, GroupManager *gman, QObject *parent); + ~DBusGroup() override; + +public Q_SLOTS: + Q_SCRIPTABLE QString name() const; + Q_SCRIPTABLE QString icon() const; + Q_SCRIPTABLE QString defaultSaveLocation() const; + Q_SCRIPTABLE void setDefaultSaveLocation(const QString &dir); + Q_SCRIPTABLE QString defaultMoveOnCompletionLocation() const; + Q_SCRIPTABLE void setDefaultMoveOnCompletionLocation(const QString &dir); + Q_SCRIPTABLE double maxShareRatio() const; + Q_SCRIPTABLE void setMaxShareRatio(double ratio); + Q_SCRIPTABLE double maxSeedTime() const; + Q_SCRIPTABLE void setMaxSeedTime(double hours); + Q_SCRIPTABLE uint maxUploadSpeed() const; + Q_SCRIPTABLE void setMaxUploadSpeed(uint speed); + Q_SCRIPTABLE uint maxDownloadSpeed() const; + Q_SCRIPTABLE void setMaxDownloadSpeed(uint speed); + Q_SCRIPTABLE bool onlyApplyOnNewTorrents() const; + Q_SCRIPTABLE void setOnlyApplyOnNewTorrents(bool on); + +private: + Group *group; + GroupManager *gman; +}; + +} + +#endif diff --git a/libktcore/dbus/dbussettings.cpp b/libktcore/dbus/dbussettings.cpp new file mode 100644 index 0000000..31b226f --- /dev/null +++ b/libktcore/dbus/dbussettings.cpp @@ -0,0 +1,791 @@ +/* + SPDX-FileCopyrightText: 2009 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "dbussettings.h" +#include "settings.h" + +#include + +#include + +namespace kt +{ +DBusSettings::DBusSettings(CoreInterface *core, QObject *parent) + : QObject(parent) + , core(core) +{ + QDBusConnection::sessionBus().registerObject(QStringLiteral("/settings"), + this, + QDBusConnection::ExportScriptableSlots | QDBusConnection::ExportScriptableSignals); +} + +DBusSettings::~DBusSettings() +{ +} + +void DBusSettings::apply() +{ + core->applySettings(); +} + +int DBusSettings::maxDownloads() +{ + return Settings::maxDownloads(); +} + +void DBusSettings::setMaxDownloads(int val) +{ + Settings::setMaxDownloads(val); +} + +int DBusSettings::maxSeeds() +{ + return Settings::maxSeeds(); +} + +void DBusSettings::setMaxSeeds(int val) +{ + Settings::setMaxSeeds(val); +} + +int DBusSettings::startDownloadsOnLowDiskSpace() +{ + return Settings::startDownloadsOnLowDiskSpace(); +} + +void DBusSettings::setStartDownloadsOnLowDiskSpace(int val) +{ + Settings::setStartDownloadsOnLowDiskSpace(val); +} + +int DBusSettings::maxConnections() +{ + return Settings::maxConnections(); +} + +void DBusSettings::setMaxConnections(int val) +{ + Settings::setMaxConnections(val); +} + +int DBusSettings::maxTotalConnections() +{ + return Settings::maxTotalConnections(); +} + +void DBusSettings::setMaxTotalConnections(int val) +{ + Settings::setMaxTotalConnections(val); +} + +int DBusSettings::maxUploadRate() +{ + return Settings::maxUploadRate(); +} + +void DBusSettings::setMaxUploadRate(int val) +{ + Settings::setMaxUploadRate(val); +} + +int DBusSettings::maxDownloadRate() +{ + return Settings::maxDownloadRate(); +} + +void DBusSettings::setMaxDownloadRate(int val) +{ + Settings::setMaxDownloadRate(val); +} + +double DBusSettings::maxRatio() +{ + return Settings::maxRatio(); +} + +void DBusSettings::setMaxRatio(double val) +{ + Settings::setMaxRatio(val); +} + +double DBusSettings::greenRatio() +{ + return Settings::greenRatio(); +} + +void DBusSettings::setGreenRatio(double val) +{ + Settings::setGreenRatio(val); +} + +int DBusSettings::port() +{ + return Settings::port(); +} + +void DBusSettings::setPort(int val) +{ + Settings::setPort(val); +} + +int DBusSettings::udpTrackerPort() +{ + return Settings::udpTrackerPort(); +} + +void DBusSettings::setUdpTrackerPort(int val) +{ + Settings::setUdpTrackerPort(val); +} + +bool DBusSettings::showSystemTrayIcon() +{ + return Settings::showSystemTrayIcon(); +} + +void DBusSettings::setShowSystemTrayIcon(bool val) +{ + Settings::setShowSystemTrayIcon(val); +} + +bool DBusSettings::showSpeedBarInTrayIcon() +{ + return Settings::showSpeedBarInTrayIcon(); +} + +void DBusSettings::setShowSpeedBarInTrayIcon(bool val) +{ + Settings::setShowSpeedBarInTrayIcon(val); +} + +int DBusSettings::downloadBandwidth() +{ + return Settings::downloadBandwidth(); +} + +void DBusSettings::setDownloadBandwidth(int val) +{ + Settings::setDownloadBandwidth(val); +} + +int DBusSettings::uploadBandwidth() +{ + return Settings::uploadBandwidth(); +} + +void DBusSettings::setUploadBandwidth(int val) +{ + Settings::setUploadBandwidth(val); +} + +bool DBusSettings::alwaysMinimizeToSystemTray() +{ + return Settings::alwaysMinimizeToSystemTray(); +} + +void DBusSettings::setAlwaysMinimizeToSystemTray(bool val) +{ + Settings::setAlwaysMinimizeToSystemTray(val); +} + +bool DBusSettings::showPopups() +{ + return Settings::showPopups(); +} + +void DBusSettings::setShowPopups(bool val) +{ + Settings::setShowPopups(val); +} + +bool DBusSettings::keepSeeding() +{ + return Settings::keepSeeding(); +} + +void DBusSettings::setKeepSeeding(bool val) +{ + Settings::setKeepSeeding(val); +} + +QString DBusSettings::tempDir() +{ + return Settings::tempDir(); +} + +void DBusSettings::setTempDir(QString val) +{ + Settings::setTempDir(val); +} + +bool DBusSettings::useSaveDir() +{ + return Settings::useSaveDir(); +} + +void DBusSettings::setUseSaveDir(bool val) +{ + Settings::setUseSaveDir(val); +} + +QString DBusSettings::saveDir() +{ + return Settings::saveDir(); +} + +void DBusSettings::setSaveDir(QString val) +{ + Settings::setSaveDir(val); +} + +bool DBusSettings::useTorrentCopyDir() +{ + return Settings::useTorrentCopyDir(); +} + +void DBusSettings::setUseTorrentCopyDir(bool val) +{ + Settings::setUseTorrentCopyDir(val); +} + +QString DBusSettings::torrentCopyDir() +{ + return Settings::torrentCopyDir(); +} + +void DBusSettings::setTorrentCopyDir(QString val) +{ + Settings::setTorrentCopyDir(val); +} + +bool DBusSettings::useCustomIP() +{ + return Settings::useCustomIP(); +} + +void DBusSettings::setUseCustomIP(bool val) +{ + Settings::setUseCustomIP(val); +} + +QString DBusSettings::lastSaveDir() +{ + return Settings::lastSaveDir(); +} + +void DBusSettings::setLastSaveDir(QString val) +{ + Settings::setLastSaveDir(val); +} + +QString DBusSettings::customIP() +{ + return Settings::customIP(); +} + +void DBusSettings::setCustomIP(QString val) +{ + Settings::setCustomIP(val); +} + +int DBusSettings::guiUpdateInterval() +{ + return Settings::guiUpdateInterval(); +} + +void DBusSettings::setGuiUpdateInterval(int val) +{ + Settings::setGuiUpdateInterval(val); +} + +bool DBusSettings::dhtSupport() +{ + return Settings::dhtSupport(); +} + +void DBusSettings::setDhtSupport(bool val) +{ + Settings::setDhtSupport(val); +} + +int DBusSettings::dhtPort() +{ + return Settings::dhtPort(); +} + +void DBusSettings::setDhtPort(int val) +{ + Settings::setDhtPort(val); +} + +bool DBusSettings::pexEnabled() +{ + return Settings::pexEnabled(); +} + +void DBusSettings::setPexEnabled(bool val) +{ + Settings::setPexEnabled(val); +} + +int DBusSettings::numUploadSlots() +{ + return Settings::numUploadSlots(); +} + +void DBusSettings::setNumUploadSlots(int val) +{ + Settings::setNumUploadSlots(val); +} + +bool DBusSettings::useEncryption() +{ + return Settings::useEncryption(); +} + +void DBusSettings::setUseEncryption(bool val) +{ + Settings::setUseEncryption(val); +} + +bool DBusSettings::allowUnencryptedConnections() +{ + return Settings::allowUnencryptedConnections(); +} + +void DBusSettings::setAllowUnencryptedConnections(bool val) +{ + Settings::setAllowUnencryptedConnections(val); +} + +int DBusSettings::typeOfService() +{ + return Settings::typeOfService(); +} + +void DBusSettings::setTypeOfService(int val) +{ + Settings::setTypeOfService(val); +} + +int DBusSettings::dscp() +{ + return Settings::dscp(); +} + +void DBusSettings::setDscp(int val) +{ + Settings::setDscp(val); +} + +int DBusSettings::maxConnectingSockets() +{ + return Settings::maxConnectingSockets(); +} + +void DBusSettings::setMaxConnectingSockets(int val) +{ + Settings::setMaxConnectingSockets(val); +} + +bool DBusSettings::checkWhenFinished() +{ + return Settings::checkWhenFinished(); +} + +void DBusSettings::setCheckWhenFinished(bool val) +{ + Settings::setCheckWhenFinished(val); +} + +QList DBusSettings::shownColumns() +{ + return Settings::shownColumns(); +} + +void DBusSettings::setShownColumns(QList val) +{ + Settings::setShownColumns(val); +} + +bool DBusSettings::useKDEProxySettings() +{ + return Settings::useKDEProxySettings(); +} + +void DBusSettings::setUseKDEProxySettings(bool val) +{ + Settings::setUseKDEProxySettings(val); +} + +QString DBusSettings::httpProxy() +{ + return Settings::httpProxy(); +} + +void DBusSettings::setHttpProxy(QString val) +{ + Settings::setHttpProxy(val); +} + +int DBusSettings::httpProxyPort() +{ + return Settings::httpProxyPort(); +} + +void DBusSettings::setHttpProxyPort(int val) +{ + Settings::setHttpProxyPort(val); +} + +bool DBusSettings::useProxyForWebSeeds() +{ + return Settings::useProxyForWebSeeds(); +} + +void DBusSettings::setUseProxyForWebSeeds(bool val) +{ + Settings::setUseProxyForWebSeeds(val); +} + +bool DBusSettings::useProxyForTracker() +{ + return Settings::useProxyForTracker(); +} + +void DBusSettings::setUseProxyForTracker(bool val) +{ + Settings::setUseProxyForTracker(val); +} + +bool DBusSettings::socksEnabled() +{ + return Settings::socksEnabled(); +} + +void DBusSettings::setSocksEnabled(bool val) +{ + Settings::setSocksEnabled(val); +} + +QString DBusSettings::socksProxy() +{ + return Settings::socksProxy(); +} + +void DBusSettings::setSocksProxy(QString val) +{ + Settings::setSocksProxy(val); +} + +int DBusSettings::socksPort() +{ + return Settings::socksPort(); +} + +void DBusSettings::setSocksPort(int val) +{ + Settings::setSocksPort(val); +} + +int DBusSettings::socksVersion() +{ + return Settings::socksVersion(); +} + +void DBusSettings::setSocksVersion(int val) +{ + Settings::setSocksVersion(val); +} + +bool DBusSettings::socksUsePassword() +{ + return Settings::socksUsePassword(); +} + +void DBusSettings::setSocksUsePassword(bool val) +{ + Settings::setSocksUsePassword(val); +} + +QString DBusSettings::socksUsername() +{ + return Settings::socksUsername(); +} + +void DBusSettings::setSocksUsername(QString val) +{ + Settings::setSocksUsername(val); +} + +QString DBusSettings::socksPassword() +{ + return Settings::socksPassword(); +} + +void DBusSettings::setSocksPassword(QString val) +{ + Settings::setSocksPassword(val); +} + +bool DBusSettings::diskPrealloc() +{ + return Settings::diskPrealloc(); +} + +void DBusSettings::setDiskPrealloc(bool val) +{ + Settings::setDiskPrealloc(val); +} + +bool DBusSettings::fullDiskPrealloc() +{ + return Settings::fullDiskPrealloc(); +} + +void DBusSettings::setFullDiskPrealloc(bool val) +{ + Settings::setFullDiskPrealloc(val); +} + +int DBusSettings::minDiskSpace() +{ + return Settings::minDiskSpace(); +} + +void DBusSettings::setMinDiskSpace(int val) +{ + Settings::setMinDiskSpace(val); +} + +int DBusSettings::cpuUsage() +{ + return Settings::cpuUsage(); +} + +void DBusSettings::setCpuUsage(int val) +{ + Settings::setCpuUsage(val); +} + +bool DBusSettings::useCompletedDir() +{ + return Settings::useCompletedDir(); +} + +void DBusSettings::setUseCompletedDir(bool val) +{ + Settings::setUseCompletedDir(val); +} + +QString DBusSettings::completedDir() +{ + return Settings::completedDir(); +} + +void DBusSettings::setCompletedDir(QString val) +{ + Settings::setCompletedDir(val); +} + +double DBusSettings::maxSeedTime() +{ + return Settings::maxSeedTime(); +} + +void DBusSettings::setMaxSeedTime(double val) +{ + Settings::setMaxSeedTime(val); +} + +QString DBusSettings::networkInterface() +{ + return Settings::networkInterface(); +} + +void DBusSettings::setNetworkInterface(const QString &val) +{ + Settings::setNetworkInterface(val); +} + +bool DBusSettings::openMultipleTorrentsSilently() +{ + return Settings::openMultipleTorrentsSilently(); +} + +void DBusSettings::setOpenMultipleTorrentsSilently(bool val) +{ + Settings::setOpenMultipleTorrentsSilently(val); +} + +bool DBusSettings::openAllTorrentsSilently() +{ + return Settings::openAllTorrentsSilently(); +} + +void DBusSettings::setOpenAllTorrentsSilently(bool val) +{ + Settings::setOpenAllTorrentsSilently(val); +} + +bool DBusSettings::decreasePriorityOfStalledTorrents() +{ + return Settings::decreasePriorityOfStalledTorrents(); +} + +void DBusSettings::setDecreasePriorityOfStalledTorrents(bool val) +{ + Settings::setDecreasePriorityOfStalledTorrents(val); +} + +int DBusSettings::stallTimer() +{ + return Settings::stallTimer(); +} + +void DBusSettings::setStallTimer(int val) +{ + Settings::setStallTimer(val); +} + +int DBusSettings::previewSizeAudio() +{ + return Settings::previewSizeAudio(); +} + +void DBusSettings::setPreviewSizeAudio(int val) +{ + Settings::setPreviewSizeAudio(val); +} + +int DBusSettings::previewSizeVideo() +{ + return Settings::previewSizeVideo(); +} + +void DBusSettings::setPreviewSizeVideo(int val) +{ + Settings::setPreviewSizeVideo(val); +} + +bool DBusSettings::suppressSleep() +{ + return Settings::suppressSleep(); +} + +void DBusSettings::setSuppressSleep(bool val) +{ + Settings::setSuppressSleep(val); +} + +bool DBusSettings::manuallyControlTorrents() +{ + return Settings::manuallyControlTorrents(); +} + +void DBusSettings::setManuallyControlTorrents(bool val) +{ + Settings::setManuallyControlTorrents(val); +} + +bool DBusSettings::webseedsEnabled() +{ + return Settings::webseedsEnabled(); +} + +void DBusSettings::setWebseedsEnabled(bool val) +{ + Settings::setWebseedsEnabled(val); +} + +bool DBusSettings::lookUpHostnameOfPeers() +{ + return Settings::lookUpHostnameOfPeers(); +} + +void DBusSettings::setLookUpHostnameOfPeers(bool val) +{ + Settings::setLookUpHostnameOfPeers(val); +} + +bool DBusSettings::utpEnabled() +{ + return Settings::utpEnabled(); +} + +void DBusSettings::setUtpEnabled(bool val) +{ + Settings::setUtpEnabled(val); +} + +bool DBusSettings::onlyUseUtp() +{ + return Settings::onlyUseUtp(); +} + +void DBusSettings::setOnlyUseUtp(bool val) +{ + Settings::setOnlyUseUtp(val); +} + +int DBusSettings::primaryTransportProtocol() +{ + return Settings::primaryTransportProtocol(); +} + +void DBusSettings::setPrimaryTransportProtocol(int val) +{ + Settings::setPrimaryTransportProtocol(val); +} + +bool DBusSettings::autoRenameSingleFileTorrents() +{ + return Settings::autoRenameSingleFileTorrents(); +} + +void DBusSettings::setAutoRenameSingleFileTorrents(bool val) +{ + Settings::setAutoRenameSingleFileTorrents(val); +} + +int DBusSettings::numMagnetDownloadingSlots() +{ + return Settings::numMagnetDownloadingSlots(); +} + +void DBusSettings::setNumMagnetDownloadingSlots(int val) +{ + Settings::setNumMagnetDownloadingSlots(val); +} + +bool DBusSettings::requeueMagnets() +{ + return Settings::requeueMagnets(); +} +bool DBusSettings::showTotalSpeedInTitle() +{ + return Settings::showTotalSpeedInTitle(); +} + +void DBusSettings::setShowTotalSpeedInTitle(bool val) +{ + Settings::setShowTotalSpeedInTitle(val); +} + +void DBusSettings::setRequeueMagnets(bool val) +{ + Settings::setRequeueMagnets(val); +} + +int DBusSettings::requeueMagnetsTime() +{ + return Settings::requeueMagnetsTime(); +} + +void DBusSettings::setRequeueMagnetsTime(int val) +{ + Settings::setRequeueMagnetsTime(val); +} +} diff --git a/libktcore/dbus/dbussettings.h b/libktcore/dbus/dbussettings.h new file mode 100644 index 0000000..bab983f --- /dev/null +++ b/libktcore/dbus/dbussettings.h @@ -0,0 +1,186 @@ +/* + SPDX-FileCopyrightText: 2009 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef __DBusSettings__ +#define __DBusSettings__ + +#include +#include +#include + +namespace kt +{ +class CoreInterface; + +class KTCORE_EXPORT DBusSettings : public QObject +{ + Q_OBJECT + Q_CLASSINFO("D-Bus Interface", "org.ktorrent.settings") +public: + DBusSettings(CoreInterface *core, QObject *parent); + ~DBusSettings() override; + +public Q_SLOTS: + Q_SCRIPTABLE int maxDownloads(); + Q_SCRIPTABLE void setMaxDownloads(int val); + Q_SCRIPTABLE int maxSeeds(); + Q_SCRIPTABLE void setMaxSeeds(int val); + Q_SCRIPTABLE int startDownloadsOnLowDiskSpace(); + Q_SCRIPTABLE void setStartDownloadsOnLowDiskSpace(int val); + Q_SCRIPTABLE int maxConnections(); + Q_SCRIPTABLE void setMaxConnections(int val); + Q_SCRIPTABLE int maxTotalConnections(); + Q_SCRIPTABLE void setMaxTotalConnections(int val); + Q_SCRIPTABLE int maxUploadRate(); + Q_SCRIPTABLE void setMaxUploadRate(int val); + Q_SCRIPTABLE int maxDownloadRate(); + Q_SCRIPTABLE void setMaxDownloadRate(int val); + Q_SCRIPTABLE double maxRatio(); + Q_SCRIPTABLE void setMaxRatio(double val); + Q_SCRIPTABLE double greenRatio(); + Q_SCRIPTABLE void setGreenRatio(double val); + Q_SCRIPTABLE int port(); + Q_SCRIPTABLE void setPort(int val); + Q_SCRIPTABLE int udpTrackerPort(); + Q_SCRIPTABLE void setUdpTrackerPort(int val); + Q_SCRIPTABLE bool showSystemTrayIcon(); + Q_SCRIPTABLE void setShowSystemTrayIcon(bool val); + Q_SCRIPTABLE bool showSpeedBarInTrayIcon(); + Q_SCRIPTABLE void setShowSpeedBarInTrayIcon(bool val); + Q_SCRIPTABLE int downloadBandwidth(); + Q_SCRIPTABLE void setDownloadBandwidth(int val); + Q_SCRIPTABLE int uploadBandwidth(); + Q_SCRIPTABLE void setUploadBandwidth(int val); + Q_SCRIPTABLE bool alwaysMinimizeToSystemTray(); + Q_SCRIPTABLE void setAlwaysMinimizeToSystemTray(bool val); + Q_SCRIPTABLE bool showPopups(); + Q_SCRIPTABLE void setShowPopups(bool val); + Q_SCRIPTABLE bool keepSeeding(); + Q_SCRIPTABLE void setKeepSeeding(bool val); + Q_SCRIPTABLE QString tempDir(); + Q_SCRIPTABLE void setTempDir(QString val); + Q_SCRIPTABLE bool useSaveDir(); + Q_SCRIPTABLE void setUseSaveDir(bool val); + Q_SCRIPTABLE QString saveDir(); + Q_SCRIPTABLE void setSaveDir(QString val); + Q_SCRIPTABLE bool useTorrentCopyDir(); + Q_SCRIPTABLE void setUseTorrentCopyDir(bool val); + Q_SCRIPTABLE QString torrentCopyDir(); + Q_SCRIPTABLE void setTorrentCopyDir(QString val); + Q_SCRIPTABLE bool useCustomIP(); + Q_SCRIPTABLE void setUseCustomIP(bool val); + Q_SCRIPTABLE QString lastSaveDir(); + Q_SCRIPTABLE void setLastSaveDir(QString val); + Q_SCRIPTABLE QString customIP(); + Q_SCRIPTABLE void setCustomIP(QString val); + Q_SCRIPTABLE int guiUpdateInterval(); + Q_SCRIPTABLE void setGuiUpdateInterval(int val); + Q_SCRIPTABLE bool dhtSupport(); + Q_SCRIPTABLE void setDhtSupport(bool val); + Q_SCRIPTABLE int dhtPort(); + Q_SCRIPTABLE void setDhtPort(int val); + Q_SCRIPTABLE bool pexEnabled(); + Q_SCRIPTABLE void setPexEnabled(bool val); + Q_SCRIPTABLE int numUploadSlots(); + Q_SCRIPTABLE void setNumUploadSlots(int val); + Q_SCRIPTABLE bool useEncryption(); + Q_SCRIPTABLE void setUseEncryption(bool val); + Q_SCRIPTABLE bool allowUnencryptedConnections(); + Q_SCRIPTABLE void setAllowUnencryptedConnections(bool val); + Q_SCRIPTABLE int typeOfService(); + Q_SCRIPTABLE void setTypeOfService(int val); + Q_SCRIPTABLE int dscp(); + Q_SCRIPTABLE void setDscp(int val); + Q_SCRIPTABLE int maxConnectingSockets(); + Q_SCRIPTABLE void setMaxConnectingSockets(int val); + Q_SCRIPTABLE bool checkWhenFinished(); + Q_SCRIPTABLE void setCheckWhenFinished(bool val); + Q_SCRIPTABLE QList shownColumns(); + Q_SCRIPTABLE void setShownColumns(QList val); + Q_SCRIPTABLE bool useKDEProxySettings(); + Q_SCRIPTABLE void setUseKDEProxySettings(bool val); + Q_SCRIPTABLE QString httpProxy(); + Q_SCRIPTABLE void setHttpProxy(QString val); + Q_SCRIPTABLE int httpProxyPort(); + Q_SCRIPTABLE void setHttpProxyPort(int val); + Q_SCRIPTABLE bool useProxyForWebSeeds(); + Q_SCRIPTABLE void setUseProxyForWebSeeds(bool val); + Q_SCRIPTABLE bool useProxyForTracker(); + Q_SCRIPTABLE void setUseProxyForTracker(bool val); + Q_SCRIPTABLE bool socksEnabled(); + Q_SCRIPTABLE void setSocksEnabled(bool val); + Q_SCRIPTABLE QString socksProxy(); + Q_SCRIPTABLE void setSocksProxy(QString val); + Q_SCRIPTABLE int socksPort(); + Q_SCRIPTABLE void setSocksPort(int val); + Q_SCRIPTABLE int socksVersion(); + Q_SCRIPTABLE void setSocksVersion(int val); + Q_SCRIPTABLE bool socksUsePassword(); + Q_SCRIPTABLE void setSocksUsePassword(bool val); + Q_SCRIPTABLE QString socksUsername(); + Q_SCRIPTABLE void setSocksUsername(QString val); + Q_SCRIPTABLE QString socksPassword(); + Q_SCRIPTABLE void setSocksPassword(QString val); + Q_SCRIPTABLE bool diskPrealloc(); + Q_SCRIPTABLE void setDiskPrealloc(bool val); + Q_SCRIPTABLE bool fullDiskPrealloc(); + Q_SCRIPTABLE void setFullDiskPrealloc(bool val); + Q_SCRIPTABLE int minDiskSpace(); + Q_SCRIPTABLE void setMinDiskSpace(int val); + Q_SCRIPTABLE int cpuUsage(); + Q_SCRIPTABLE void setCpuUsage(int val); + Q_SCRIPTABLE bool useCompletedDir(); + Q_SCRIPTABLE void setUseCompletedDir(bool val); + Q_SCRIPTABLE QString completedDir(); + Q_SCRIPTABLE void setCompletedDir(QString val); + Q_SCRIPTABLE double maxSeedTime(); + Q_SCRIPTABLE void setMaxSeedTime(double val); + Q_SCRIPTABLE QString networkInterface(); + Q_SCRIPTABLE void setNetworkInterface(const QString &val); + Q_SCRIPTABLE bool openMultipleTorrentsSilently(); + Q_SCRIPTABLE void setOpenMultipleTorrentsSilently(bool val); + Q_SCRIPTABLE bool openAllTorrentsSilently(); + Q_SCRIPTABLE void setOpenAllTorrentsSilently(bool val); + Q_SCRIPTABLE bool decreasePriorityOfStalledTorrents(); + Q_SCRIPTABLE void setDecreasePriorityOfStalledTorrents(bool val); + Q_SCRIPTABLE int stallTimer(); + Q_SCRIPTABLE void setStallTimer(int val); + Q_SCRIPTABLE int previewSizeAudio(); + Q_SCRIPTABLE void setPreviewSizeAudio(int val); + Q_SCRIPTABLE int previewSizeVideo(); + Q_SCRIPTABLE void setPreviewSizeVideo(int val); + Q_SCRIPTABLE bool suppressSleep(); + Q_SCRIPTABLE void setSuppressSleep(bool val); + Q_SCRIPTABLE bool manuallyControlTorrents(); + Q_SCRIPTABLE void setManuallyControlTorrents(bool val); + Q_SCRIPTABLE bool webseedsEnabled(); + Q_SCRIPTABLE void setWebseedsEnabled(bool val); + Q_SCRIPTABLE bool lookUpHostnameOfPeers(); + Q_SCRIPTABLE void setLookUpHostnameOfPeers(bool val); + Q_SCRIPTABLE bool utpEnabled(); + Q_SCRIPTABLE void setUtpEnabled(bool val); + Q_SCRIPTABLE bool onlyUseUtp(); + Q_SCRIPTABLE void setOnlyUseUtp(bool val); + Q_SCRIPTABLE int primaryTransportProtocol(); + Q_SCRIPTABLE void setPrimaryTransportProtocol(int val); + Q_SCRIPTABLE bool autoRenameSingleFileTorrents(); + Q_SCRIPTABLE void setAutoRenameSingleFileTorrents(bool val); + Q_SCRIPTABLE int numMagnetDownloadingSlots(); + Q_SCRIPTABLE void setNumMagnetDownloadingSlots(int val); + Q_SCRIPTABLE bool requeueMagnets(); + Q_SCRIPTABLE void setRequeueMagnets(bool val); + Q_SCRIPTABLE int requeueMagnetsTime(); + Q_SCRIPTABLE void setRequeueMagnetsTime(int val); + Q_SCRIPTABLE bool showTotalSpeedInTitle(); + Q_SCRIPTABLE void setShowTotalSpeedInTitle(bool val); + + Q_SCRIPTABLE void apply(); + +private: + CoreInterface *core; +}; +} + +#endif diff --git a/libktcore/dbus/dbustorrent.cpp b/libktcore/dbus/dbustorrent.cpp new file mode 100644 index 0000000..14ba36f --- /dev/null +++ b/libktcore/dbus/dbustorrent.cpp @@ -0,0 +1,487 @@ +/* + SPDX-FileCopyrightText: 2008 Joris Guisson + SPDX-FileCopyrightText: 2008 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include +#include + +#include + +#include "dbustorrent.h" +#include "dbustorrentfilestream.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace bt; + +namespace kt +{ +DBusTorrent::DBusTorrent(bt::TorrentInterface *ti, QObject *parent) + : QObject(parent) + , ti(ti) + , stream(nullptr) +{ + QDBusConnection sb = QDBusConnection::sessionBus(); + QString path = QLatin1String("/torrent/") + ti->getInfoHash().toString(); + QFlags flags = QDBusConnection::ExportScriptableSlots | QDBusConnection::ExportScriptableSignals; + sb.registerObject(path, this, flags); + + connect(ti, &bt::TorrentInterface::finished, this, &DBusTorrent::onFinished); + connect(ti, &bt::TorrentInterface::stoppedByError, this, &DBusTorrent::onStoppedByError); + connect(ti, &bt::TorrentInterface::seedingAutoStopped, this, &DBusTorrent::onSeedingAutoStopped); + connect(ti, &bt::TorrentInterface::corruptedDataFound, this, &DBusTorrent::onCorruptedDataFound); + connect(ti, &bt::TorrentInterface::torrentStopped, this, &DBusTorrent::onTorrentStopped); +} + +DBusTorrent::~DBusTorrent() +{ +} + +QString DBusTorrent::infoHash() const +{ + const bt::SHA1Hash &h = ti->getInfoHash(); + return h.toString(); +} + +QString DBusTorrent::name() const +{ + return ti->getDisplayName(); +} + +bool DBusTorrent::isPrivate() const +{ + return ti->getStats().priv_torrent; +} + +uint DBusTorrent::downloadSpeed() const +{ + return ti->getStats().download_rate; +} + +uint DBusTorrent::uploadSpeed() const +{ + return ti->getStats().upload_rate; +} + +qulonglong DBusTorrent::bytesDownloaded() const +{ + return ti->getStats().bytes_downloaded; +} + +qulonglong DBusTorrent::bytesUploaded() const +{ + return ti->getStats().bytes_uploaded; +} + +qulonglong DBusTorrent::totalSize() const +{ + return ti->getStats().total_bytes; +} + +qulonglong DBusTorrent::bytesLeftToDownload() const +{ + return ti->getStats().bytes_left_to_download; +} + +qulonglong DBusTorrent::bytesLeft() const +{ + return ti->getStats().bytes_left; +} + +qulonglong DBusTorrent::bytesToDownload() const +{ + return ti->getStats().total_bytes_to_download; +} + +uint DBusTorrent::chunks() const +{ + return ti->getStats().total_chunks; +} + +uint DBusTorrent::chunkSize() const +{ + return ti->getStats().chunk_size; +} + +bool DBusTorrent::chunkDownloaded(uint idx) const +{ + return ti->downloadedChunksBitSet().get(idx); +} + +uint DBusTorrent::seedersConnected() const +{ + return ti->getStats().seeders_connected_to; +} + +uint DBusTorrent::seedersTotal() const +{ + return ti->getStats().seeders_total; +} + +uint DBusTorrent::leechersConnected() const +{ + return ti->getStats().leechers_connected_to; +} + +uint DBusTorrent::leechersTotal() const +{ + return ti->getStats().leechers_total; +} + +QString DBusTorrent::currentTracker() const +{ + bt::TrackerInterface *t = ti->getTrackersList()->getCurrentTracker(); + return t ? t->trackerURL().toDisplayString() : QString(); +} + +QStringList DBusTorrent::trackers() const +{ + const QList trackers = ti->getTrackersList()->getTrackers(); + QStringList ret; + for (bt::TrackerInterface *t : trackers) + ret << t->trackerURL().toDisplayString(); + return ret; +} + +void DBusTorrent::changeTracker(const QString &tracker_url) +{ + QUrl url(tracker_url); + ti->getTrackersList()->setCurrentTracker(url); +} + +void DBusTorrent::announce() +{ + ti->updateTracker(); +} + +void DBusTorrent::scrape() +{ + ti->scrapeTracker(); +} + +void DBusTorrent::setTrackerEnabled(const QString &tracker_url, bool enabled) +{ + ti->getTrackersList()->setTrackerEnabled(QUrl(tracker_url), enabled); +} + +bool DBusTorrent::addTracker(const QString &tracker_url) +{ + if (ti->getStats().priv_torrent) + return false; + + return ti->getTrackersList()->addTracker(QUrl(tracker_url), true) != nullptr; +} + +bool DBusTorrent::removeTracker(const QString &tracker_url) +{ + if (ti->getStats().priv_torrent) + return false; + + ti->getTrackersList()->removeTracker(QUrl(tracker_url)); + return true; +} + +void DBusTorrent::restoreDefaultTrackers() +{ + ti->getTrackersList()->restoreDefault(); + ti->updateTracker(); +} + +QStringList DBusTorrent::webSeeds() const +{ + QStringList ws; + for (Uint32 i = 0; i < ti->getNumWebSeeds(); i++) { + const WebSeedInterface *wsi = ti->getWebSeed(i); + ws << wsi->getUrl().toDisplayString(); + } + return ws; +} + +bool DBusTorrent::addWebSeed(const QString &webseed_url) +{ + return ti->addWebSeed(QUrl(webseed_url)); +} + +bool DBusTorrent::removeWebSeed(const QString &webseed_url) +{ + return ti->removeWebSeed(QUrl(webseed_url)); +} + +uint DBusTorrent::numFiles() const +{ + return ti->getNumFiles(); +} + +QString DBusTorrent::dataDir() const +{ + return ti->getDataDir(); +} + +QString DBusTorrent::pathOnDisk() const +{ + return ti->getStats().output_path; +} + +QString DBusTorrent::torDir() const +{ + return ti->getTorDir(); +} + +QByteArray DBusTorrent::stats() const +{ + QByteArray ret; + BEncoder enc(new BEncoderBufferOutput(ret)); + const TorrentStats &s = ti->getStats(); + enc.beginDict(); + enc.write(QByteArrayLiteral("imported_bytes"), s.imported_bytes); + enc.write(QByteArrayLiteral("bytes_downloaded"), s.bytes_downloaded); + enc.write(QByteArrayLiteral("bytes_uploaded"), s.bytes_uploaded); + enc.write(QByteArrayLiteral("bytes_left"), s.bytes_left); + enc.write(QByteArrayLiteral("bytes_left_to_download"), s.bytes_left_to_download); + enc.write(QByteArrayLiteral("total_bytes"), s.total_bytes); + enc.write(QByteArrayLiteral("total_bytes_to_download"), s.total_bytes_to_download); + enc.write(QByteArrayLiteral("download_rate"), s.download_rate); + enc.write(QByteArrayLiteral("upload_rate"), s.upload_rate); + enc.write(QByteArrayLiteral("num_peers"), s.num_peers); + enc.write(QByteArrayLiteral("num_chunks_downloading"), s.num_chunks_downloading); + enc.write(QByteArrayLiteral("total_chunks"), s.total_chunks); + enc.write(QByteArrayLiteral("num_chunks_downloaded"), s.num_chunks_downloaded); + enc.write(QByteArrayLiteral("num_chunks_excluded"), s.num_chunks_excluded); + enc.write(QByteArrayLiteral("num_chunks_left"), s.num_chunks_left); + enc.write(QByteArrayLiteral("chunk_size"), s.chunk_size); + enc.write(QByteArrayLiteral("seeders_total"), s.seeders_total); + enc.write(QByteArrayLiteral("seeders_connected_to"), s.seeders_connected_to); + enc.write(QByteArrayLiteral("leechers_total"), s.leechers_total); + enc.write(QByteArrayLiteral("leechers_connected_to"), s.leechers_connected_to); + enc.write(QByteArrayLiteral("status"), s.statusToString().toUtf8()); + enc.write(QByteArrayLiteral("session_bytes_downloaded"), s.session_bytes_downloaded); + enc.write(QByteArrayLiteral("session_bytes_uploaded"), s.session_bytes_uploaded); + enc.write(QByteArrayLiteral("output_path"), s.output_path.toUtf8()); + enc.write(QByteArrayLiteral("running"), s.running); + enc.write(QByteArrayLiteral("started"), s.started); + enc.write(QByteArrayLiteral("multi_file_torrent"), s.multi_file_torrent); + enc.write(QByteArrayLiteral("stopped_by_error"), s.stopped_by_error); + enc.write(QByteArrayLiteral("max_share_ratio"), s.max_share_ratio); + enc.write(QByteArrayLiteral("max_seed_time"), s.max_seed_time); + enc.write(QByteArrayLiteral("num_corrupted_chunks"), s.num_corrupted_chunks); + const bt::BitSet &bs = ti->downloadedChunksBitSet(); + enc.write(QByteArrayLiteral("downloaded_chunks")); + enc.write(bs.getData(), bs.getNumBytes()); + const bt::BitSet &ebs = ti->excludedChunksBitSet(); + enc.write(QByteArrayLiteral("excluded_chunks")); + enc.write(ebs.getData(), ebs.getNumBytes()); + enc.end(); + return ret; +} + +void DBusTorrent::onFinished(bt::TorrentInterface *tor) +{ + Q_UNUSED(tor) + Q_EMIT finished(this); +} + +void DBusTorrent::onStoppedByError(bt::TorrentInterface *tor, const QString &err) +{ + Q_UNUSED(tor) + Q_EMIT stoppedByError(this, err); +} + +void DBusTorrent::onSeedingAutoStopped(bt::TorrentInterface *tor, bt::AutoStopReason reason) +{ + Q_UNUSED(tor) + QString msg; + switch (reason) { + case bt::MAX_RATIO_REACHED: + msg = i18n("Maximum share ratio reached."); + break; + case bt::MAX_SEED_TIME_REACHED: + msg = i18n("Maximum seed time reached."); + break; + } + Q_EMIT seedingAutoStopped(this, msg); +} + +void DBusTorrent::onCorruptedDataFound(bt::TorrentInterface *tor) +{ + Q_UNUSED(tor) + Q_EMIT corruptedDataFound(this); +} + +void DBusTorrent::onTorrentStopped(bt::TorrentInterface *tor) +{ + Q_UNUSED(tor) + // Q_EMIT torrentStopped(this); //TODO emit string representation of the torrent +} + +QString DBusTorrent::filePath(uint file_index) const +{ + if (file_index >= ti->getNumFiles()) + return QString(); + else + return ti->getTorrentFile(file_index).getPath(); +} + +QString DBusTorrent::filePathOnDisk(uint file_index) const +{ + if (file_index >= ti->getNumFiles()) + return QString(); + else + return ti->getTorrentFile(file_index).getPathOnDisk(); +} + +qulonglong DBusTorrent::fileSize(uint file_index) const +{ + if (file_index >= ti->getNumFiles()) + return 0; + else + return ti->getTorrentFile(file_index).getSize(); +} + +int DBusTorrent::filePriority(uint file_index) const +{ + if (file_index >= ti->getNumFiles()) + return 0; + else + return ti->getTorrentFile(file_index).getPriority(); +} + +void DBusTorrent::setFilePriority(uint file_index, int prio) +{ + if (file_index >= ti->getNumFiles()) + return; + + if (prio > 60 || prio < 10) + return; + + if (prio % 10 != 0) + return; + + ti->getTorrentFile(file_index).setPriority((bt::Priority)prio); +} + +int DBusTorrent::firstChunkOfFile(uint file_index) const +{ + if (file_index >= ti->getNumFiles()) + return 0; + else + return ti->getTorrentFile(file_index).getFirstChunk(); +} + +int DBusTorrent::lastChunkOfFile(uint file_index) const +{ + if (file_index >= ti->getNumFiles()) + return 0; + else + return ti->getTorrentFile(file_index).getLastChunk(); +} + +double DBusTorrent::filePercentage(uint file_index) const +{ + if (file_index >= ti->getNumFiles()) + return 0; + else + return ti->getTorrentFile(file_index).getDownloadPercentage(); +} + +bool DBusTorrent::isMultiMediaFile(uint file_index) const +{ + if (file_index >= ti->getNumFiles()) + return false; + else + return ti->getTorrentFile(file_index).isMultimedia(); +} + +void DBusTorrent::setDoNotDownload(uint file_index, bool dnd) +{ + if (file_index >= ti->getNumFiles()) + return; + + ti->getTorrentFile(file_index).setDoNotDownload(dnd); +} + +int DBusTorrent::priority() const +{ + return ti->getPriority(); +} + +void DBusTorrent::setPriority(int p) +{ + ti->setPriority(p); +} + +void DBusTorrent::setAllowedToStart(bool on) +{ + ti->setAllowedToStart(on); +} + +bool DBusTorrent::isAllowedToStart() const +{ + return ti->isAllowedToStart(); +} + +double DBusTorrent::maxSeedTime() const +{ + return ti->getMaxSeedTime(); +} + +double DBusTorrent::maxShareRatio() const +{ + return ti->getMaxShareRatio(); +} + +void DBusTorrent::setMaxSeedTime(double hours) +{ + ti->setMaxSeedTime(hours); +} + +void DBusTorrent::setMaxShareRatio(double ratio) +{ + ti->setMaxShareRatio(ratio); +} + +double DBusTorrent::seedTime() const +{ + Uint32 dl = ti->getRunningTimeDL(); + Uint32 ul = ti->getRunningTimeUL(); + return (double)(ul - dl) / 3600.0; +} + +double DBusTorrent::shareRatio() const +{ + return ti->getStats().shareRatio(); +} + +bool DBusTorrent::createStream(uint file_index) +{ + delete stream; + + stream = new DBusTorrentFileStream(file_index, this); + if (!stream->ok()) { + delete stream; + stream = nullptr; + return false; + } + + return true; +} + +// streaming only works for one file at once atm, so the index has no effect yet +bool DBusTorrent::removeStream(uint file_index) +{ + Q_UNUSED(file_index); + + delete stream; + stream = nullptr; + return true; +} + +} diff --git a/libktcore/dbus/dbustorrent.h b/libktcore/dbus/dbustorrent.h new file mode 100644 index 0000000..449048e --- /dev/null +++ b/libktcore/dbus/dbustorrent.h @@ -0,0 +1,138 @@ +/* + SPDX-FileCopyrightText: 2008 Joris Guisson + SPDX-FileCopyrightText: 2008 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KTDBUSTORRENT_H +#define KTDBUSTORRENT_H + +#include +#include + +#include +#include + +namespace kt +{ +class DBusTorrentFileStream; + +/** + DBus object to access a torrent +*/ +class DBusTorrent : public QObject +{ + Q_OBJECT + Q_CLASSINFO("D-Bus Interface", "org.ktorrent.torrent") +public: + DBusTorrent(bt::TorrentInterface *ti, QObject *parent); + ~DBusTorrent() override; + + /// Get a pointer to the actual torrent + bt::TorrentInterface *torrent() + { + return ti; + } + +public Q_SLOTS: + Q_SCRIPTABLE QString infoHash() const; + Q_SCRIPTABLE QString name() const; + Q_SCRIPTABLE bool isPrivate() const; + + // Speed + Q_SCRIPTABLE uint downloadSpeed() const; + Q_SCRIPTABLE uint uploadSpeed() const; + + // Data stuff + Q_SCRIPTABLE qulonglong bytesDownloaded() const; + Q_SCRIPTABLE qulonglong bytesUploaded() const; + Q_SCRIPTABLE qulonglong totalSize() const; + Q_SCRIPTABLE qulonglong bytesLeftToDownload() const; + Q_SCRIPTABLE qulonglong bytesLeft() const; + Q_SCRIPTABLE qulonglong bytesToDownload() const; + + // Priority and QM + Q_SCRIPTABLE int priority() const; + Q_SCRIPTABLE void setPriority(int p); + Q_SCRIPTABLE void setAllowedToStart(bool on); + Q_SCRIPTABLE bool isAllowedToStart() const; + + // Chunks + Q_SCRIPTABLE uint chunks() const; + Q_SCRIPTABLE uint chunkSize() const; + Q_SCRIPTABLE bool chunkDownloaded(uint idx) const; + + // Seeders and leechers + Q_SCRIPTABLE uint seedersConnected() const; + Q_SCRIPTABLE uint seedersTotal() const; + Q_SCRIPTABLE uint leechersConnected() const; + Q_SCRIPTABLE uint leechersTotal() const; + + // Tracker stuff + Q_SCRIPTABLE QString currentTracker() const; + Q_SCRIPTABLE QStringList trackers() const; + Q_SCRIPTABLE void setTrackerEnabled(const QString &tracker_url, bool enabled); + Q_SCRIPTABLE void changeTracker(const QString &tracker_url); + Q_SCRIPTABLE void announce(); + Q_SCRIPTABLE void scrape(); + Q_SCRIPTABLE bool addTracker(const QString &tracker_url); + Q_SCRIPTABLE bool removeTracker(const QString &tracker_url); + Q_SCRIPTABLE void restoreDefaultTrackers(); + + // WebSeed stuff + Q_SCRIPTABLE QStringList webSeeds() const; + Q_SCRIPTABLE bool addWebSeed(const QString &webseed_url); + Q_SCRIPTABLE bool removeWebSeed(const QString &webseed_url); + + // Files + Q_SCRIPTABLE uint numFiles() const; + Q_SCRIPTABLE QString dataDir() const; + Q_SCRIPTABLE QString torDir() const; + Q_SCRIPTABLE QString pathOnDisk() const; + Q_SCRIPTABLE QString filePath(uint file_index) const; + Q_SCRIPTABLE QString filePathOnDisk(uint file_index) const; + Q_SCRIPTABLE qulonglong fileSize(uint file_index) const; + Q_SCRIPTABLE int filePriority(uint file_index) const; + Q_SCRIPTABLE void setFilePriority(uint file_index, int prio); + Q_SCRIPTABLE int firstChunkOfFile(uint file_index) const; + Q_SCRIPTABLE int lastChunkOfFile(uint file_index) const; + Q_SCRIPTABLE double filePercentage(uint file_index) const; + Q_SCRIPTABLE bool isMultiMediaFile(uint file_index) const; + Q_SCRIPTABLE void setDoNotDownload(uint file_index, bool dnd); + + // Stats + Q_SCRIPTABLE QByteArray stats() const; + + // Max share ratio and seed time + Q_SCRIPTABLE void setMaxShareRatio(double ratio); + Q_SCRIPTABLE double maxShareRatio() const; + Q_SCRIPTABLE double shareRatio() const; + Q_SCRIPTABLE void setMaxSeedTime(double hours); + Q_SCRIPTABLE double maxSeedTime() const; + Q_SCRIPTABLE double seedTime() const; + + Q_SCRIPTABLE bool createStream(uint file_index); + Q_SCRIPTABLE bool removeStream(uint file_index); + +Q_SIGNALS: + void finished(QObject *tor); + void stoppedByError(QObject *tor, const QString &msg); + void seedingAutoStopped(QObject *tor, const QString &reason); + void corruptedDataFound(QObject *tor); + void torrentStopped(QObject *tor); + +private Q_SLOTS: + void onFinished(bt::TorrentInterface *tor); + void onStoppedByError(bt::TorrentInterface *tor, const QString &err); + void onSeedingAutoStopped(bt::TorrentInterface *tor, bt::AutoStopReason reason); + void onCorruptedDataFound(bt::TorrentInterface *tor); + void onTorrentStopped(bt::TorrentInterface *tor); + +private: + bt::TorrentInterface *ti; + DBusTorrentFileStream *stream; +}; + +} + +#endif diff --git a/libktcore/dbus/dbustorrentfile.cpp b/libktcore/dbus/dbustorrentfile.cpp new file mode 100644 index 0000000..72c4d68 --- /dev/null +++ b/libktcore/dbus/dbustorrentfile.cpp @@ -0,0 +1,76 @@ +/* + SPDX-FileCopyrightText: 2008 Joris Guisson + SPDX-FileCopyrightText: 2008 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "dbustorrentfile.h" + +namespace kt +{ +DBusTorrentFile::DBusTorrentFile(bt::TorrentFileInterface &file, QObject *parent) + : QObject(parent) + , file(file) +{ +} + +DBusTorrentFile::~DBusTorrentFile() +{ +} + +QString DBusTorrentFile::path() const +{ + return file.getPath(); +} + +QString DBusTorrentFile::pathOnDisk() const +{ + return file.getPathOnDisk(); +} + +qulonglong DBusTorrentFile::size() const +{ + return file.getSize(); +} + +int DBusTorrentFile::priority() const +{ + return file.getPriority(); +} + +void DBusTorrentFile::setPriority(int prio) +{ + if (prio > 60 || prio < 10) + return; + + if (prio % 10 != 0) + return; + + file.setPriority((bt::Priority)prio); +} + +void DBusTorrentFile::setDoNotDownload(bool dnd) +{ + file.setDoNotDownload(dnd); +} + +int DBusTorrentFile::firstChunk() const +{ + return file.getFirstChunk(); +} + +int DBusTorrentFile::lastChunk() const +{ + return file.getLastChunk(); +} + +double DBusTorrentFile::percentage() const +{ + return file.getDownloadPercentage(); +} + +bool DBusTorrentFile::isMultiMedia() const +{ + return file.isMultimedia(); +} +} diff --git a/libktcore/dbus/dbustorrentfile.h b/libktcore/dbus/dbustorrentfile.h new file mode 100644 index 0000000..a72cebb --- /dev/null +++ b/libktcore/dbus/dbustorrentfile.h @@ -0,0 +1,44 @@ +/* + SPDX-FileCopyrightText: 2008 Joris Guisson + SPDX-FileCopyrightText: 2008 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KTDBUSTORRENTFILE_H +#define KTDBUSTORRENTFILE_H + +#include +#include + +namespace kt +{ +/** + @author +*/ +class DBusTorrentFile : public QObject +{ + Q_OBJECT + Q_CLASSINFO("D-Bus Interface", "org.ktorrent.torrentfile") +public: + DBusTorrentFile(bt::TorrentFileInterface &file, QObject *parent); + ~DBusTorrentFile(); + +public Q_SLOTS: + Q_SCRIPTABLE QString path() const; + Q_SCRIPTABLE QString pathOnDisk() const; + Q_SCRIPTABLE qulonglong size() const; + Q_SCRIPTABLE int priority() const; + Q_SCRIPTABLE void setPriority(int prio); + Q_SCRIPTABLE int firstChunk() const; + Q_SCRIPTABLE int lastChunk() const; + Q_SCRIPTABLE double percentage() const; + Q_SCRIPTABLE bool isMultiMedia() const; + Q_SCRIPTABLE void setDoNotDownload(bool dnd); + +private: + bt::TorrentFileInterface &file; +}; + +} + +#endif diff --git a/libktcore/dbus/dbustorrentfilestream.cpp b/libktcore/dbus/dbustorrentfilestream.cpp new file mode 100644 index 0000000..601e774 --- /dev/null +++ b/libktcore/dbus/dbustorrentfilestream.cpp @@ -0,0 +1,78 @@ +/* + SPDX-FileCopyrightText: 2010 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "dbustorrentfilestream.h" +#include "dbustorrent.h" + +#include + +#include +#include + +namespace kt +{ +DBusTorrentFileStream::DBusTorrentFileStream(bt::Uint32 file_index, kt::DBusTorrent *tor) + : QObject(tor) + , tor(tor) +{ + QDBusConnection sb = QDBusConnection::sessionBus(); + QString path = QStringLiteral("/torrent/%1/stream").arg(tor->torrent()->getInfoHash().toString()); + QFlags flags = QDBusConnection::ExportScriptableSlots | QDBusConnection::ExportScriptableSignals; + sb.registerObject(path, this, flags); + + stream = tor->torrent()->createTorrentFileStream(file_index, true, this); + if (stream) + stream->open(QIODevice::ReadOnly); +} + +DBusTorrentFileStream::~DBusTorrentFileStream() +{ +} + +qint64 DBusTorrentFileStream::bytesAvailable() const +{ + return stream ? stream->bytesAvailable() : 0; +} + +bt::Uint32 DBusTorrentFileStream::currentChunk() const +{ + return stream ? stream->currentChunk() : 0; +} + +QString DBusTorrentFileStream::path() const +{ + return stream ? stream->path() : QString(); +} + +qint64 DBusTorrentFileStream::pos() const +{ + return stream ? stream->pos() : 0; +} + +QByteArray DBusTorrentFileStream::read(qint64 maxlen) +{ + if (!stream || bytesAvailable() == 0) + return QByteArray(); + + qint64 to_read = std::min(maxlen, bytesAvailable()); + QByteArray ba(to_read, 0); + + qint64 ret = stream->read(ba.data(), to_read); + if (ret < to_read) + ba.resize(ret); + return ba; +} + +bool DBusTorrentFileStream::seek(qint64 pos) +{ + return stream ? stream->seek(pos) : false; +} + +qint64 DBusTorrentFileStream::size() const +{ + return stream ? stream->size() : 0; +} + +} diff --git a/libktcore/dbus/dbustorrentfilestream.h b/libktcore/dbus/dbustorrentfilestream.h new file mode 100644 index 0000000..9973915 --- /dev/null +++ b/libktcore/dbus/dbustorrentfilestream.h @@ -0,0 +1,62 @@ +/* + SPDX-FileCopyrightText: 2010 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KT_DBUSTORRENTFILESTREAM_H +#define KT_DBUSTORRENTFILESTREAM_H + +#include +#include + +namespace kt +{ +class DBusTorrent; + +/** + * DBus interface to a TorrentFileStream + */ +class DBusTorrentFileStream : public QObject +{ + Q_OBJECT + Q_CLASSINFO("D-Bus Interface", "org.ktorrent.torrentfilestream") +public: + DBusTorrentFileStream(bt::Uint32 file_index, DBusTorrent *tor); + ~DBusTorrentFileStream() override; + + /// Was the stream created properly ? + bool ok() const + { + return !stream.isNull(); + } + +public Q_SLOTS: + /// Get the current stream position + Q_SCRIPTABLE qint64 pos() const; + + /// Get the total size + Q_SCRIPTABLE qint64 size() const; + + /// Seek, will fail if attempting to seek to a point which is not downloaded yet + Q_SCRIPTABLE bool seek(qint64 pos); + + /// How many bytes are there available + Q_SCRIPTABLE qint64 bytesAvailable() const; + + /// Get the path of the file + Q_SCRIPTABLE QString path() const; + + /// Get the current chunk relative to the first chunk of the file + Q_SCRIPTABLE bt::Uint32 currentChunk() const; + + /// Read maxlen bytes from the stream + Q_SCRIPTABLE QByteArray read(qint64 maxlen); + +private: + DBusTorrent *tor; + bt::TorrentFileStream::Ptr stream; +}; + +} + +#endif // KT_DBUSTORRENTFILESTREAM_H diff --git a/libktcore/groups/allgroup.cpp b/libktcore/groups/allgroup.cpp new file mode 100644 index 0000000..9d68df3 --- /dev/null +++ b/libktcore/groups/allgroup.cpp @@ -0,0 +1,26 @@ +/* + SPDX-FileCopyrightText: 2007 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "allgroup.h" +#include + +namespace kt +{ +AllGroup::AllGroup() + : Group(i18n("All Torrents"), MIXED_GROUP, QStringLiteral("/all")) +{ + setIconByName(QStringLiteral("folder")); +} + +AllGroup::~AllGroup() +{ +} + +bool AllGroup::isMember(TorrentInterface *tor) +{ + return tor != nullptr; +} + +} diff --git a/libktcore/groups/allgroup.h b/libktcore/groups/allgroup.h new file mode 100644 index 0000000..081adef --- /dev/null +++ b/libktcore/groups/allgroup.h @@ -0,0 +1,27 @@ +/* + SPDX-FileCopyrightText: 2007 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KTALLGROUP_H +#define KTALLGROUP_H + +#include + +namespace kt +{ +/** + @author Joris Guisson +*/ +class AllGroup : public Group +{ +public: + AllGroup(); + ~AllGroup() override; + + bool isMember(TorrentInterface *tor) override; +}; + +} + +#endif diff --git a/libktcore/groups/functiongroup.cpp b/libktcore/groups/functiongroup.cpp new file mode 100644 index 0000000..e3fc92e --- /dev/null +++ b/libktcore/groups/functiongroup.cpp @@ -0,0 +1,11 @@ +/* + SPDX-FileCopyrightText: 2008 Joris Guisson + SPDX-FileCopyrightText: 2008 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "functiongroup.h" + +namespace kt +{ +} diff --git a/libktcore/groups/functiongroup.h b/libktcore/groups/functiongroup.h new file mode 100644 index 0000000..5b7bb72 --- /dev/null +++ b/libktcore/groups/functiongroup.h @@ -0,0 +1,43 @@ +/* + SPDX-FileCopyrightText: 2008 Joris Guisson + SPDX-FileCopyrightText: 2008 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KTFUNCTIONGROUP_H +#define KTFUNCTIONGROUP_H + +#include "group.h" + +namespace kt +{ +typedef bool (*IsMemberFunction)(TorrentInterface *tor); + +/** + Group which calls a function pointer to test for membership +*/ +template class FunctionGroup : public Group +{ +public: + FunctionGroup(const QString &name, const QString &icon, int flags, const QString &path) + : Group(name, flags, path) + { + setIconByName(icon); + } + + ~FunctionGroup() override + { + } + + bool isMember(TorrentInterface *tor) override + { + if (!tor) + return false; + else + return fn(tor); + } +}; + +} + +#endif diff --git a/libktcore/groups/group.cpp b/libktcore/groups/group.cpp new file mode 100644 index 0000000..6733589 --- /dev/null +++ b/libktcore/groups/group.cpp @@ -0,0 +1,83 @@ +/* + SPDX-FileCopyrightText: 2007 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "group.h" +#include + +namespace kt +{ +Group::Policy::Policy() +{ + max_share_ratio = max_seed_time = 0.0f; + max_download_rate = max_upload_rate = 0; + only_apply_on_new_torrents = false; +} + +Group::Group(const QString &name, int flags, const QString &path) + : name(name) + , flags(flags) + , path(path) + , running(0) + , total(0) +{ +} + +Group::~Group() +{ +} + +void Group::save(bt::BEncoder *) +{ +} + +void Group::load(bt::BDictNode *) +{ +} + +void Group::setIconByName(const QString &in) +{ + icon_name = in; + // KF5 icon = SmallIcon(in); +} + +void Group::rename(const QString &nn) +{ + name = nn; +} + +void Group::torrentRemoved(TorrentInterface *) +{ +} + +void Group::removeTorrent(TorrentInterface *) +{ +} + +void Group::addTorrent(TorrentInterface *, bool) +{ +} + +void Group::setGroupPolicy(const Policy &p) +{ + policy = p; + policyChanged(); +} + +void Group::policyChanged() +{ +} + +void Group::updateCount(QueueManager *qman) +{ + total = running = 0; + for (bt::TorrentInterface *tor : qAsConst(*qman)) { + if (isMember(tor)) { + total++; + if (tor->getStats().running) + running++; + } + } +} +} diff --git a/libktcore/groups/group.h b/libktcore/groups/group.h new file mode 100644 index 0000000..4998d09 --- /dev/null +++ b/libktcore/groups/group.h @@ -0,0 +1,198 @@ +/* + SPDX-FileCopyrightText: 2007 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KTGROUP_H +#define KTGROUP_H + +#include +#include + +#include +#include + +namespace bt +{ +class BEncoder; +class BDictNode; +class TorrentInterface; +} + +namespace kt +{ +class QueueManager; +using bt::TorrentInterface; + +/** + * @author Joris Guisson + * + * Base class for all groups. Subclasses should only implement the + * isMember function, but can also provide save and load + * functionality. + */ +class KTCORE_EXPORT Group : public QObject +{ + Q_OBJECT +public: + enum Properties { + UPLOADS_ONLY_GROUP = 1, + DOWNLOADS_ONLY_GROUP = 2, + MIXED_GROUP = 3, + CUSTOM_GROUP = 4, + }; + + struct KTCORE_EXPORT Policy { + QString default_save_location; + QString default_move_on_completion_location; + float max_share_ratio; + float max_seed_time; + bt::Uint32 max_upload_rate; + bt::Uint32 max_download_rate; + bool only_apply_on_new_torrents; + + Policy(); + }; + + /** + * Create a new group. + * @param name The name of the group + * @param flags Properties of the group + * @param path Path in the group tree (e.g /all/downloads/foo, last item in path should be the groups internal name) + */ + Group(const QString &name, int flags, const QString &path); + ~Group() override; + + /// See if this is a standard group. + bool isStandardGroup() const + { + return !(flags & CUSTOM_GROUP); + } + + /// Get the group flags + int groupFlags() const + { + return flags; + } + + /** + * Rename the group. + * @param nn The new name + */ + void rename(const QString &nn); + + /** + * Set the group icon by name. + * @param in The icon name + */ + void setIconByName(const QString &in); + + /// Get the name of the group + const QString &groupName() const + { + return name; + } + + /// Get the icon of the group + const QIcon &groupIcon() const + { + return icon; + } + + /// Name of the group icon + const QString &groupIconName() const + { + return icon_name; + } + + /// Get the group policy + const Policy &groupPolicy() const + { + return policy; + } + + /// Path in the group tree + const QString &groupPath() const + { + return path; + } + + /// Get the number of running torrents + int runningTorrents() const + { + return running; + } + + /// Total torrents + int totalTorrents() const + { + return total; + } + + /// Set the group policy + void setGroupPolicy(const Policy &p); + + /** + * Save the torrents.The torrents should be save in a bencoded file. + * @param enc The BEncoder + */ + virtual void save(bt::BEncoder *enc); + + /** + * Load the torrents of the group from a BDictNode. + * @param n The BDictNode + */ + virtual void load(bt::BDictNode *n); + + /** + * Test if a torrent is a member of this group. + * @param tor The torrent + */ + virtual bool isMember(TorrentInterface *tor) = 0; + + /** + * The torrent has been removed and is about to be deleted. + * Subclasses should make sure that they don't have dangling + * pointers to this torrent. + * @param tor The torrent + */ + virtual void torrentRemoved(TorrentInterface *tor); + + /** + * Subclasses should implement this, if they want to have torrents added to them. + * @param tor The torrent + * @param new_torrent Indicates whether this is a newly created or opened torrent + */ + virtual void addTorrent(TorrentInterface *tor, bool new_torrent); + + /** + * Subclasses should implement this, if they want to have torrents removed from them. + * @param tor The torrent + */ + virtual void removeTorrent(TorrentInterface *tor); + + /** + * Called when the policy has been changed. + */ + virtual void policyChanged(); + + /** + * Update the running and total count + * @param qman The QueueManager + **/ + void updateCount(QueueManager *qman); + +protected: + QString name; + QIcon icon; + QString icon_name; + int flags; + Policy policy; + QString path; + int running; + int total; +}; + +} + +#endif diff --git a/libktcore/groups/groupmanager.cpp b/libktcore/groups/groupmanager.cpp new file mode 100644 index 0000000..d667008 --- /dev/null +++ b/libktcore/groups/groupmanager.cpp @@ -0,0 +1,328 @@ +/* + SPDX-FileCopyrightText: 2005 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "groupmanager.h" + +#include + +#include "allgroup.h" +#include "functiongroup.h" +#include "torrentgroup.h" +#include "ungroupedgroup.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace bt; + +namespace kt +{ +bool upload(TorrentInterface *tor) +{ + return tor->getStats().completed; +} + +bool download(TorrentInterface *tor) +{ + return !tor->getStats().completed; +} + +bool queued(TorrentInterface *tor) +{ + return tor->getStats().status == bt::QUEUED; +} + +bool stalled(TorrentInterface *tor) +{ + return tor->getStats().status == bt::STALLED; +} + +bool error(TorrentInterface *tor) +{ + return tor->getStats().status == bt::ERROR; +} + +bool not_running(TorrentInterface *tor) +{ + return tor->getStats().running == false; +} + +bool running(TorrentInterface *tor) +{ + return tor->getStats().running == true; +} + +bool active(TorrentInterface *tor) +{ + const bt::TorrentStats &s = tor->getStats(); + return (s.upload_rate >= 100 || s.download_rate >= 100); +} + +bool passive(TorrentInterface *tor) +{ + return !active(tor); +} + +template bool member(TorrentInterface *tor) +{ + return A(tor) && B(tor); +} + +GroupManager::GroupManager() +{ + groups.setAutoDelete(true); + + all = new AllGroup(); + groups.insert(all->groupName(), all); + + QList defaults; + // uploads tree + defaults << new FunctionGroup(i18n("Uploads"), QStringLiteral("go-up"), Group::UPLOADS_ONLY_GROUP, QStringLiteral("/all/uploads")); + defaults << new FunctionGroup>(i18n("Running Uploads"), + QStringLiteral("kt-start"), + Group::UPLOADS_ONLY_GROUP, + QStringLiteral("/all/uploads/running")); + defaults << new FunctionGroup>(i18n("Not Running Uploads"), + QStringLiteral("kt-stop"), + Group::UPLOADS_ONLY_GROUP, + QStringLiteral("/all/uploads/not_running")); + + // downloads tree + defaults << new FunctionGroup(i18n("Downloads"), QStringLiteral("go-down"), Group::DOWNLOADS_ONLY_GROUP, QStringLiteral("/all/downloads")); + defaults << new FunctionGroup>(i18n("Running Downloads"), + QStringLiteral("kt-start"), + Group::DOWNLOADS_ONLY_GROUP, + QStringLiteral("/all/downloads/running")); + defaults << new FunctionGroup>(i18n("Not Running Downloads"), + QStringLiteral("kt-stop"), + Group::DOWNLOADS_ONLY_GROUP, + QStringLiteral("/all/downloads/not_running")); + + defaults << new FunctionGroup(i18n("Active Torrents"), QStringLiteral("network-connect"), Group::MIXED_GROUP, QStringLiteral("/all/active")); + defaults << new FunctionGroup>(i18n("Active Downloads"), + QStringLiteral("go-down"), + Group::DOWNLOADS_ONLY_GROUP, + QStringLiteral("/all/active/downloads")); + defaults << new FunctionGroup>(i18n("Active Uploads"), + QStringLiteral("go-up"), + Group::UPLOADS_ONLY_GROUP, + QStringLiteral("/all/active/uploads")); + + defaults << new FunctionGroup(i18n("Passive Torrents"), QStringLiteral("network-disconnect"), Group::MIXED_GROUP, QStringLiteral("/all/passive")); + defaults << new FunctionGroup>(i18n("Passive Downloads"), + QStringLiteral("go-down"), + Group::DOWNLOADS_ONLY_GROUP, + QStringLiteral("/all/passive/downloads")); + defaults << new FunctionGroup>(i18n("Passive Uploads"), + QStringLiteral("go-up"), + Group::UPLOADS_ONLY_GROUP, + QStringLiteral("/all/passive/uploads")); + defaults << new UngroupedGroup(this); + + for (Group *g : qAsConst(defaults)) + groups.insert(g->groupName(), g); +} + +GroupManager::~GroupManager() +{ +} + +Group *GroupManager::newGroup(const QString &name) +{ + if (groups.find(name)) + return nullptr; + + TorrentGroup *g = new TorrentGroup(name); + connect(g, &TorrentGroup::torrentAdded, this, &GroupManager::customGroupChanged); + connect(g, qOverload(&TorrentGroup::torrentRemoved), this, &GroupManager::customGroupChanged); + groups.insert(name, g); + Q_EMIT groupAdded(g); + return g; +} + +void GroupManager::removeGroup(Group *g) +{ + if (canRemove(g)) { + Q_EMIT groupRemoved(g); + groups.setAutoDelete(false); + groups.erase(g->groupName()); + groups.setAutoDelete(true); + g->deleteLater(); + } +} + +bool GroupManager::canRemove(const Group *g) const +{ + return g->groupFlags() & Group::CUSTOM_GROUP; +} + +Group *GroupManager::find(const QString &name) +{ + return groups.find(name); +} + +QStringList GroupManager::customGroupNames() +{ + QStringList groupNames; + Itr it = groups.begin(); + + while (it != end()) { + if (it->second->groupFlags() & Group::CUSTOM_GROUP) + groupNames << it->first; + ++it; + } + + return groupNames; +} + +void GroupManager::saveGroups() +{ + QString fn = kt::DataDir() + QStringLiteral("groups"); + bt::File fptr; + if (!fptr.open(fn, QStringLiteral("wb"))) { + bt::Out(SYS_GEN | LOG_DEBUG) << "Cannot open " << fn << " : " << fptr.errorString() << bt::endl; + return; + } + + try { + bt::BEncoder enc(&fptr); + + enc.beginList(); + for (CItr i = groups.begin(); i != groups.end(); i++) { + if (i->second->groupFlags() & Group::CUSTOM_GROUP) + i->second->save(&enc); + } + enc.end(); + } catch (bt::Error &err) { + bt::Out(SYS_GEN | LOG_DEBUG) << "Error : " << err.toString() << endl; + return; + } +} + +void GroupManager::loadGroups() +{ + QString fn = kt::DataDir() + QStringLiteral("groups"); + bt::File fptr; + if (!fptr.open(fn, QStringLiteral("rb"))) { + bt::Out(SYS_GEN | LOG_DEBUG) << "Cannot open " << fn << " : " << fptr.errorString() << bt::endl; + return; + } + + bt::BNode *n = nullptr; + try { + Uint32 fs = bt::FileSize(fn); + QByteArray data(fs, 0); + fptr.read(data.data(), fs); + + BDecoder dec(data, false); + n = dec.decode(); + if (!n || n->getType() != bt::BNode::LIST) + throw bt::Error(QStringLiteral("groups file corrupt")); + + BListNode *ln = (BListNode *)n; + for (Uint32 i = 0; i < ln->getNumChildren(); i++) { + BDictNode *dn = ln->getDict(i); + if (!dn) + continue; + + TorrentGroup *g = new TorrentGroup(QStringLiteral("dummy")); + connect(g, &TorrentGroup::torrentAdded, this, &GroupManager::customGroupChanged); + connect(g, qOverload(&TorrentGroup::torrentRemoved), this, &GroupManager::customGroupChanged); + + try { + g->load(dn); + } catch (...) { + delete g; + throw; + } + + if (!find(g->groupName())) + groups.insert(g->groupName(), g); + else + delete g; + } + + delete n; + } catch (bt::Error &err) { + bt::Out(SYS_GEN | LOG_DEBUG) << "Error : " << err.toString() << endl; + delete n; + return; + } +} + +void GroupManager::torrentRemoved(TorrentInterface *ti) +{ + for (CItr i = groups.begin(); i != groups.end(); i++) { + i->second->torrentRemoved(ti); + } +} + +void GroupManager::renameGroup(const QString &old_name, const QString &new_name) +{ + Group *g = find(old_name); + if (!g) + return; + + groups.setAutoDelete(false); + groups.erase(old_name); + g->rename(new_name); + groups.insert(new_name, g); + groups.setAutoDelete(true); + saveGroups(); + + Q_EMIT groupRenamed(g); +} + +void GroupManager::addDefaultGroup(Group *g) +{ + if (find(g->groupName())) + return; + + groups.insert(g->groupName(), g); + Q_EMIT groupAdded(g); +} + +void GroupManager::removeDefaultGroup(Group *g) +{ + if (g) { + groupRemoved(g); + groups.erase(g->groupName()); + } +} + +void GroupManager::torrentsLoaded(QueueManager *qman) +{ + for (CItr i = groups.begin(); i != groups.end(); i++) { + if (i->second->groupFlags() & Group::CUSTOM_GROUP) { + TorrentGroup *tg = dynamic_cast(i->second); + if (tg) + tg->loadTorrents(qman); + } + } +} + +Group *GroupManager::findByPath(const QString &path) +{ + for (CItr i = groups.begin(); i != groups.end(); i++) { + if (i->second->groupPath() == path) + return i->second; + } + + return nullptr; +} + +void GroupManager::updateCount(QueueManager *qman) +{ + for (CItr i = groups.begin(); i != groups.end(); i++) + i->second->updateCount(qman); +} + +} diff --git a/libktcore/groups/groupmanager.h b/libktcore/groups/groupmanager.h new file mode 100644 index 0000000..09e8b75 --- /dev/null +++ b/libktcore/groups/groupmanager.h @@ -0,0 +1,157 @@ +/* + SPDX-FileCopyrightText: 2005 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KTGROUPMANAGER_H +#define KTGROUPMANAGER_H + +#include + +#include +#include +#include + +namespace bt +{ +class TorrentInterface; +} + +namespace kt +{ +class QueueManager; + +/** + * @author Joris Guisson + * + * Manages all user created groups and the standard groups. + */ +class KTCORE_EXPORT GroupManager : public QObject +{ + Q_OBJECT +public: + GroupManager(); + ~GroupManager() override; + + /** + * Update the count of all groups + * @param qman The QueueManager + **/ + void updateCount(QueueManager *qman); + + /** + * Find a group given it's path + * @param path Path of the group + * @return :Group* The Group or 0 + **/ + Group *findByPath(const QString &path); + + /** + * Create a new user created group. + * @param name Name of the group + * @return Pointer to the group or nullptr, if another group already exists with the same name. + */ + Group *newGroup(const QString &name); + + /** + * Remove a user crated group + * @param g The group + */ + void removeGroup(Group *g); + + /** + * Add a new default group. + * @param g The group + */ + void addDefaultGroup(Group *g); + + /** + * Remove a default group. + * @param g The group + */ + void removeDefaultGroup(Group *g); + + /// Get the group off all torrents + Group *allGroup() + { + return all; + } + + typedef bt::PtrMap::iterator Itr; + typedef bt::PtrMap::const_iterator CItr; + + Itr begin() + { + return groups.begin(); + } + Itr end() + { + return groups.end(); + } + + CItr begin() const + { + return groups.begin(); + } + CItr end() const + { + return groups.end(); + } + + /// Find Group given a name + Group *find(const QString &name); + + /// Return the custom group names + QStringList customGroupNames(); + + /** + * Save the groups to a file. + */ + void saveGroups(); + + /** + * Load the groups from a file + */ + void loadGroups(); + + /** + * See if we can remove a group. + * @param g The group + * @return true on any user created group, false on the standard ones + */ + bool canRemove(const Group *g) const; + + /** + * A torrent has been removed. This function checks all groups and + * removes the torrent from it. + * @param ti The torrent + */ + void torrentRemoved(bt::TorrentInterface *ti); + + /** + * Rename a group. + * @param old_name The old name + * @param new_name The new name + */ + void renameGroup(const QString &old_name, const QString &new_name); + + /** + Torrents have been loaded update all custom groups. + @param qman The QueueManager + */ + void torrentsLoaded(QueueManager *qman); + +Q_SIGNALS: + void groupRenamed(Group *g); + void groupAdded(Group *g); + void groupRemoved(Group *g); + void customGroupChanged(); + +private: + bt::PtrMap groups; + Group *all; +}; + +} + +#endif diff --git a/libktcore/groups/torrentgroup.cpp b/libktcore/groups/torrentgroup.cpp new file mode 100644 index 0000000..d1f22e0 --- /dev/null +++ b/libktcore/groups/torrentgroup.cpp @@ -0,0 +1,191 @@ +/* + SPDX-FileCopyrightText: 2007 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "torrentgroup.h" +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace bt; + +namespace kt +{ +TorrentGroup::TorrentGroup(const QString &name) + : Group(name, MIXED_GROUP | CUSTOM_GROUP, QLatin1String("/all/custom/") + name) +{ + setIconByName(QStringLiteral("application-x-bittorrent")); +} + +TorrentGroup::~TorrentGroup() +{ +} + +bool TorrentGroup::isMember(TorrentInterface *tor) +{ + return torrents.count(tor) > 0; +} + +void TorrentGroup::add(TorrentInterface *tor) +{ + torrents.insert(tor); +} + +void TorrentGroup::remove(TorrentInterface *tor) +{ + torrents.erase(tor); +} + +void TorrentGroup::save(bt::BEncoder *enc) +{ + enc->beginDict(); + enc->write(QByteArrayLiteral("name")); + enc->write(name.toLocal8Bit()); + enc->write(QByteArrayLiteral("icon")); + enc->write(icon_name.toLocal8Bit()); + enc->write(QByteArrayLiteral("hashes")); + enc->beginList(); + std::set::iterator i = torrents.begin(); + while (i != torrents.end()) { + TorrentInterface *tc = *i; + // write the info hash, because that will be unique for each torrent + const bt::SHA1Hash &h = tc->getInfoHash(); + enc->write(h.getData(), 20); + i++; + } + std::set::iterator j = hashes.begin(); + while (j != hashes.end()) { + enc->write(j->getData(), 20); + j++; + } + enc->end(); + enc->write(QByteArrayLiteral("policy")); + enc->beginDict(); + enc->write(QByteArrayLiteral("default_save_location")); + enc->write(policy.default_save_location.toUtf8()); + enc->write(QByteArrayLiteral("max_share_ratio")); + enc->write(QByteArray::number(policy.max_share_ratio)); + enc->write(QByteArrayLiteral("max_seed_time")); + enc->write(QByteArray::number(policy.max_seed_time)); + enc->write(QByteArrayLiteral("max_upload_rate")); + enc->write(policy.max_upload_rate); + enc->write(QByteArrayLiteral("max_download_rate")); + enc->write(policy.max_download_rate); + enc->write(QByteArrayLiteral("only_apply_on_new_torrents")); + enc->write((bt::Uint32)(policy.only_apply_on_new_torrents ? 1 : 0)); + enc->write(QByteArrayLiteral("default_move_on_completion_location")); + enc->write(policy.default_move_on_completion_location.toUtf8()); + enc->end(); + enc->end(); +} + +void TorrentGroup::load(bt::BDictNode *dn) +{ + name = QString::fromLocal8Bit(dn->getByteArray("name")); + setIconByName(QString::fromLocal8Bit(dn->getByteArray("icon"))); + BListNode *ln = dn->getList("hashes"); + if (!ln) + return; + + path = QLatin1String("/all/custom/") + name; + + for (Uint32 i = 0; i < ln->getNumChildren(); i++) { + QByteArray ba = ln->getByteArray(i); + if (ba.size() != 20) + continue; + + hashes.insert(SHA1Hash((const Uint8 *)ba.data())); + } + + if (BDictNode *gp = dn->getDict(QByteArrayLiteral("policy"))) { + // load the group policy + if (gp->getValue(QByteArrayLiteral("default_save_location"))) { + policy.default_save_location = gp->getString(QByteArrayLiteral("default_save_location"), nullptr); + if (policy.default_save_location.length() == 0) + policy.default_save_location = QString(); // make sure that 0 length strings are loaded as null strings + } + + if (gp->getValue(QByteArrayLiteral("default_move_on_completion_location"))) { + policy.default_move_on_completion_location = gp->getString(QByteArrayLiteral("default_move_on_completion_location"), nullptr); + if (policy.default_move_on_completion_location.length() == 0) + policy.default_move_on_completion_location = QString(); // make sure that 0 length strings are loaded as null strings + } + + if (gp->getValue(QByteArrayLiteral("max_share_ratio"))) + policy.max_share_ratio = gp->getString(QByteArrayLiteral("max_share_ratio"), nullptr).toFloat(); + + if (gp->getValue(QByteArrayLiteral("max_seed_time"))) + policy.max_seed_time = gp->getString(QByteArrayLiteral("max_seed_time"), nullptr).toFloat(); + + if (gp->getValue(QByteArrayLiteral("max_upload_rate"))) + policy.max_upload_rate = gp->getInt(QByteArrayLiteral("max_upload_rate")); + + if (gp->getValue(QByteArrayLiteral("max_download_rate"))) + policy.max_download_rate = gp->getInt(QByteArrayLiteral("max_download_rate")); + + if (gp->getValue(QByteArrayLiteral("only_apply_on_new_torrents"))) + policy.only_apply_on_new_torrents = gp->getInt(QByteArrayLiteral("only_apply_on_new_torrents")); + } +} + +void TorrentGroup::torrentRemoved(TorrentInterface *tor) +{ + removeTorrent(tor); +} + +void TorrentGroup::removeTorrent(TorrentInterface *tor) +{ + torrents.erase(tor); + torrentRemoved(this); +} + +void TorrentGroup::addTorrent(TorrentInterface *tor, bool new_torrent) +{ + torrents.insert(tor); + // apply group policy if needed + if (policy.only_apply_on_new_torrents && !new_torrent) + return; + + if (bt::Exists(policy.default_move_on_completion_location)) + tor->setMoveWhenCompletedDir(policy.default_move_on_completion_location); + tor->setMaxShareRatio(policy.max_share_ratio); + tor->setMaxSeedTime(policy.max_seed_time); + tor->setTrafficLimits(policy.max_upload_rate * 1024, policy.max_download_rate * 1024); + + torrentAdded(this); +} + +void TorrentGroup::policyChanged() +{ + if (policy.only_apply_on_new_torrents) + return; + + std::set::iterator i = torrents.begin(); + while (i != torrents.end()) { + TorrentInterface *tor = *i; + tor->setMaxShareRatio(policy.max_share_ratio); + tor->setMaxSeedTime(policy.max_seed_time); + tor->setTrafficLimits(policy.max_upload_rate * 1024, policy.max_download_rate * 1024); + i++; + } +} + +void TorrentGroup::loadTorrents(QueueManager *qman) +{ + QueueManager::iterator i = qman->begin(); + while (i != qman->end()) { + if (hashes.count((*i)->getInfoHash()) > 0) + torrents.insert(*i); + i++; + } + + hashes.clear(); +} + +} diff --git a/libktcore/groups/torrentgroup.h b/libktcore/groups/torrentgroup.h new file mode 100644 index 0000000..26b82af --- /dev/null +++ b/libktcore/groups/torrentgroup.h @@ -0,0 +1,53 @@ +/* + SPDX-FileCopyrightText: 2007 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KTTORRENTGROUP_H +#define KTTORRENTGROUP_H + +#include +#include +#include + +namespace kt +{ +class QueueManager; + +/** + @author Joris Guisson +*/ +class KTCORE_EXPORT TorrentGroup : public Group +{ + Q_OBJECT +public: + TorrentGroup(const QString &name); + ~TorrentGroup() override; + + bool isMember(TorrentInterface *tor) override; + void save(bt::BEncoder *enc) override; + void load(bt::BDictNode *n) override; + void torrentRemoved(TorrentInterface *tor) override; + void removeTorrent(TorrentInterface *tor) override; + void addTorrent(TorrentInterface *tor, bool new_torrent) override; + void policyChanged() override; + + void add(TorrentInterface *tor); + void remove(TorrentInterface *tor); + void loadTorrents(QueueManager *qman); + +Q_SIGNALS: + /// Emitted when a torrent has been added + void torrentAdded(Group *g); + + /// Emitted when a torrent has been removed + void torrentRemoved(Group *g); + +private: + std::set torrents; + std::set hashes; +}; + +} + +#endif diff --git a/libktcore/groups/ungroupedgroup.cpp b/libktcore/groups/ungroupedgroup.cpp new file mode 100644 index 0000000..bf643ac --- /dev/null +++ b/libktcore/groups/ungroupedgroup.cpp @@ -0,0 +1,34 @@ +/* + SPDX-FileCopyrightText: 2008 Joris Guisson + SPDX-FileCopyrightText: 2008 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "ungroupedgroup.h" +#include "groupmanager.h" + +#include + +namespace kt +{ +UngroupedGroup::UngroupedGroup(GroupManager *gman) + : Group(i18n("Ungrouped Torrents"), MIXED_GROUP, QStringLiteral("/all/ungrouped")) + , gman(gman) +{ + setIconByName(QStringLiteral("application-x-bittorrent")); +} + +UngroupedGroup::~UngroupedGroup() +{ +} + +bool UngroupedGroup::isMember(TorrentInterface *tor) +{ + for (GroupManager::CItr i = gman->begin(); i != gman->end(); i++) + if ((i->second->groupFlags() & Group::CUSTOM_GROUP) && i->second->isMember(tor)) + return false; + + return true; +} + +} diff --git a/libktcore/groups/ungroupedgroup.h b/libktcore/groups/ungroupedgroup.h new file mode 100644 index 0000000..7103876 --- /dev/null +++ b/libktcore/groups/ungroupedgroup.h @@ -0,0 +1,33 @@ +/* + SPDX-FileCopyrightText: 2008 Joris Guisson + SPDX-FileCopyrightText: 2008 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KTUNGROUPEDGROUP_H +#define KTUNGROUPEDGROUP_H + +#include + +namespace kt +{ +class GroupManager; + +/** + @author +*/ +class UngroupedGroup : public Group +{ +public: + UngroupedGroup(GroupManager *gman); + ~UngroupedGroup() override; + + bool isMember(TorrentInterface *tor) override; + +private: + GroupManager *gman; +}; + +} + +#endif diff --git a/libktcore/gui/centralwidget.cpp b/libktcore/gui/centralwidget.cpp new file mode 100644 index 0000000..2653024 --- /dev/null +++ b/libktcore/gui/centralwidget.cpp @@ -0,0 +1,110 @@ +/* + SPDX-FileCopyrightText: 2009 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include +#include +#include + +#include "centralwidget.h" +#include + +namespace kt +{ +CentralWidget::CentralWidget(QWidget *parent) + : QStackedWidget(parent) +{ + activity_switching_group = new QActionGroup(this); + connect(activity_switching_group, &QActionGroup::triggered, this, &CentralWidget::switchActivity); +} + +CentralWidget::~CentralWidget() +{ +} + +void CentralWidget::loadState(KSharedConfigPtr cfg) +{ + KConfigGroup g = cfg->group("MainWindow"); + int idx = g.readEntry("current_activity", 0); + Activity *act = (Activity *)widget(idx); + if (act) + setCurrentActivity(act); + + const QList actions = activity_switching_group->actions(); + for (QAction *a : actions) { + if (a->data().value() == act || act == nullptr) { + a->setChecked(true); + break; + } + } + + for (QAction *a : actions) { + a->setPriority((QAction::Priority)g.readEntry(QLatin1String("Priority_") + a->objectName(), (int)QAction::NormalPriority)); + } +} + +void CentralWidget::saveState(KSharedConfigPtr cfg) +{ + KConfigGroup g = cfg->group("MainWindow"); + g.writeEntry("current_activity", currentIndex()); + + const QList actions = activity_switching_group->actions(); + for (QAction *a : actions) { + g.writeEntry(QLatin1String("Priority_") + a->objectName(), (int)a->priority()); + } +} + +QAction *CentralWidget::addActivity(Activity *act) +{ + QAction *a = new QAction(QIcon::fromTheme(act->icon()), act->name(), this); + // act->name() is i18n'ed, use as uniq id + a->setObjectName(act->icon() + QLatin1String("_wght_") + QString::number(act->weight())); + activity_switching_group->addAction(a); + a->setCheckable(true); + a->setToolTip(act->toolTip()); + a->setData(QVariant::fromValue(act)); + addWidget(act); + return a; +} + +void CentralWidget::removeActivity(Activity *act) +{ + const QList actions = activity_switching_group->actions(); + for (QAction *a : actions) { + if (a->data().value() == act) { + activity_switching_group->removeAction(a); + a->deleteLater(); + break; + } + } + removeWidget(act); +} + +void CentralWidget::setCurrentActivity(Activity *act) +{ + setCurrentWidget(act); +} + +Activity *CentralWidget::currentActivity() +{ + return (Activity *)currentWidget(); +} + +QList CentralWidget::activitySwitchingActions() +{ + return activity_switching_group->actions(); +} + +void CentralWidget::switchActivity(QAction *action) +{ + for (int i = 0; i < count(); i++) { + Activity *act = (Activity *)widget(i); + if (action->data().value() == act) { + changeActivity(act); + break; + } + } +} + +} diff --git a/libktcore/gui/centralwidget.h b/libktcore/gui/centralwidget.h new file mode 100644 index 0000000..346e511 --- /dev/null +++ b/libktcore/gui/centralwidget.h @@ -0,0 +1,64 @@ +/* + SPDX-FileCopyrightText: 2009 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KT_CENTRALWIDGET_H +#define KT_CENTRALWIDGET_H + +#include +#include +#include +#include + +#include + +namespace kt +{ +class Activity; + +/** + * The CentralWidget holds the widget stack. + */ +class KTCORE_EXPORT CentralWidget : public QStackedWidget +{ + Q_OBJECT +public: + CentralWidget(QWidget *parent); + ~CentralWidget() override; + + /// Add an Activity + QAction *addActivity(Activity *act); + + /// Remove an Activity (doesn't delete it) + void removeActivity(Activity *act); + + /// Set the current activity + void setCurrentActivity(Activity *act); + + /// Get the current activity + Activity *currentActivity(); + + /// Load the state of the widget + void loadState(KSharedConfigPtr cfg); + + /// Save the state of the widget + void saveState(KSharedConfigPtr cfg); + + /// Get the list of actions to switch between activities + QList activitySwitchingActions(); + +private Q_SLOTS: + void switchActivity(QAction *action); + +Q_SIGNALS: + /// Emitted when the current Activity needs to be changed + void changeActivity(Activity *act); + +private: + QActionGroup *activity_switching_group; +}; + +} + +#endif // KT_CENTRALWIDGET_H diff --git a/libktcore/gui/extender.cpp b/libktcore/gui/extender.cpp new file mode 100644 index 0000000..05a47a9 --- /dev/null +++ b/libktcore/gui/extender.cpp @@ -0,0 +1,19 @@ +/* + SPDX-FileCopyrightText: 2010 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "extender.h" + +namespace kt +{ +Extender::Extender(bt::TorrentInterface *tc, QWidget *parent) + : QWidget(parent) + , tc(tc) +{ +} + +Extender::~Extender() +{ +} +} diff --git a/libktcore/gui/extender.h b/libktcore/gui/extender.h new file mode 100644 index 0000000..920422b --- /dev/null +++ b/libktcore/gui/extender.h @@ -0,0 +1,51 @@ +/* + SPDX-FileCopyrightText: 2010 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KT_EXTENDER_H +#define KT_EXTENDER_H + +#include +#include + +namespace bt +{ +class TorrentInterface; +} + +namespace kt +{ +/** + * Base class for all extender widgets + */ +class KTCORE_EXPORT Extender : public QWidget +{ + Q_OBJECT +public: + Extender(bt::TorrentInterface *tc, QWidget *parent); + ~Extender() override; + + /// Get the torrent of this extender + bt::TorrentInterface *torrent() + { + return tc; + } + + /// Is this similar to another extender + virtual bool similar(Extender *ext) const = 0; + +Q_SIGNALS: + /// Should be emitted by an extender when it wants to close itself + void closeRequest(Extender *ext); + + /// Should be emitted when an extender is resized + void resized(Extender *ext); + +protected: + bt::TorrentInterface *tc; +}; + +} + +#endif // KT_EXTENDER_H diff --git a/libktcore/gui/tabbarwidget.cpp b/libktcore/gui/tabbarwidget.cpp new file mode 100644 index 0000000..4062977 --- /dev/null +++ b/libktcore/gui/tabbarwidget.cpp @@ -0,0 +1,228 @@ +/* + SPDX-FileCopyrightText: 2009 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "tabbarwidget.h" + +#include +#include +#include + +#include +#include +#include + +namespace kt +{ +ActionGroup::ActionGroup(QObject *parent) + : QObject(parent) +{ +} + +ActionGroup::~ActionGroup() +{ +} + +void ActionGroup::addAction(QAction *act) +{ + actions.append(act); + connect(act, &QAction::toggled, this, &ActionGroup::toggled); +} + +void ActionGroup::removeAction(QAction *act) +{ + actions.removeAll(act); + disconnect(act, &QAction::toggled, this, &ActionGroup::toggled); +} + +void ActionGroup::toggled(bool on) +{ + QAction *act = qobject_cast(sender()); + if (!act) + return; + + for (QAction *a : qAsConst(actions)) { + if (a != act) + a->setChecked(false); + } + + act->setChecked(on); + Q_EMIT actionTriggered(act); +} + +TabBarWidget::TabBarWidget(QSplitter *splitter, QWidget *parent) + : QWidget(parent) + , widget_stack(nullptr) + , shrunken(false) +{ + QVBoxLayout *layout = new QVBoxLayout(this); + layout->setSpacing(0); + layout->setMargin(0); + tab_bar = new KToolBar(this); + tab_bar->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); + connect(tab_bar, &QToolBar::toolButtonStyleChanged, this, &TabBarWidget::toolButtonStyleChanged); + action_group = new ActionGroup(this); + connect(action_group, &ActionGroup::actionTriggered, this, &TabBarWidget::onActionTriggered); + widget_stack = new QStackedWidget(splitter); + splitter->addWidget(widget_stack); + layout->addWidget(tab_bar); + + QSizePolicy tsp = sizePolicy(); + QSizePolicy wsp = widget_stack->sizePolicy(); + + tsp.setVerticalPolicy(QSizePolicy::Fixed); + wsp.setVerticalPolicy(QSizePolicy::Expanding); + + widget_stack->setSizePolicy(wsp); + setSizePolicy(tsp); + shrink(); +} + +TabBarWidget::~TabBarWidget() +{ +} + +void TabBarWidget::addTab(QWidget *ti, const QString &text, const QString &icon, const QString &tooltip) +{ + QAction *act = tab_bar->addAction(QIcon::fromTheme(icon), text); + act->setCheckable(true); + act->setToolTip(tooltip); + act->setChecked(widget_stack->count() == 0 && !shrunken); + widget_stack->addWidget(ti); + action_group->addAction(act); + widget_to_action.insert(ti, act); + show(); +} + +void TabBarWidget::removeTab(QWidget *ti) +{ + QMap::iterator itr = widget_to_action.find(ti); + if (itr == widget_to_action.end()) + return; + + tab_bar->removeAction(itr.value()); + action_group->removeAction(itr.value()); + itr.value()->deleteLater(); + if (widget_stack->currentWidget() == ti) { + ti->hide(); + widget_stack->removeWidget(ti); + ti->setParent(nullptr); + } else { + widget_stack->removeWidget(ti); + ti->setParent(nullptr); + } + + if (widget_stack->count() == 0) { + widget_stack->hide(); + hide(); + } else { + QMap::iterator itr = widget_to_action.find(widget_stack->currentWidget()); + if (itr != widget_to_action.end()) { + QAction *act = itr.value(); + act->setChecked(true); + } + } +} + +void TabBarWidget::changeTabIcon(QWidget *ti, const QString &icon) +{ + QMap::iterator itr = widget_to_action.find(ti); + if (itr == widget_to_action.end()) + return; + + itr.value()->setIcon(QIcon::fromTheme(icon)); +} + +void TabBarWidget::changeTabText(QWidget *ti, const QString &text) +{ + QMap::iterator itr = widget_to_action.find(ti); + if (itr == widget_to_action.end()) + return; + + itr.value()->setText(text); +} + +void TabBarWidget::shrink() +{ + widget_stack->hide(); + shrunken = true; +} + +void TabBarWidget::unshrink() +{ + widget_stack->show(); + shrunken = false; +} + +void TabBarWidget::onActionTriggered(QAction *act) +{ + QWidget *ti = nullptr; + QMap::iterator i = widget_to_action.begin(); + while (i != widget_to_action.end() && !ti) { + if (i.value() == act) + ti = i.key(); + i++; + } + + if (!ti) + return; + + if (ti == widget_stack->currentWidget()) { + // it is the current tab + if (act->isChecked()) + unshrink(); + else + shrink(); + } else { + // change the current in stack + widget_stack->setCurrentWidget(ti); + if (shrunken) + unshrink(); + } +} + +void TabBarWidget::saveState(KSharedConfigPtr cfg, const QString &group) +{ + QWidget *current = widget_stack->currentWidget(); + + KConfigGroup g = cfg->group(group); + g.writeEntry("shrunken", shrunken); + if (current) + g.writeEntry("current_tab", widget_to_action[current]->text()); +} + +void TabBarWidget::loadState(KSharedConfigPtr cfg, const QString &group) +{ + KConfigGroup g = cfg->group(group); + + bool tmp = g.readEntry("shrunken", true); + if (tmp != shrunken) { + if (tmp) + shrink(); + else + unshrink(); + } + + QString ctab = g.readPathEntry("current_tab", QString()); + for (QMap::const_iterator i = widget_to_action.cbegin(); i != widget_to_action.cend(); i++) { + if (i.value()->text() == ctab) { + widget_stack->setCurrentWidget(i.key()); + i.value()->setChecked(!tmp); + break; + } + } +} + +void TabBarWidget::toolButtonStyleChanged(Qt::ToolButtonStyle style) +{ + if (style != Qt::ToolButtonTextBesideIcon) + QTimer::singleShot(0, this, &TabBarWidget::setToolButtonStyle); +} + +void TabBarWidget::setToolButtonStyle() +{ + tab_bar->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); +} + +} diff --git a/libktcore/gui/tabbarwidget.h b/libktcore/gui/tabbarwidget.h new file mode 100644 index 0000000..ed3b509 --- /dev/null +++ b/libktcore/gui/tabbarwidget.h @@ -0,0 +1,86 @@ +/* + SPDX-FileCopyrightText: 2009 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef TABBARWIDGET_H +#define TABBARWIDGET_H + +#include +#include +#include +#include +#include + +#include + +#include "ktcore_export.h" + +namespace kt +{ +class ActionGroup; + +class KTCORE_EXPORT TabBarWidget : public QWidget +{ + Q_OBJECT +public: + TabBarWidget(QSplitter *splitter, QWidget *parent); + ~TabBarWidget() override; + + /// Add a tab to the TabBarWidget + void addTab(QWidget *w, const QString &text, const QString &icon, const QString &tooltip); + + /// Remove a tab from the TabBarWidget + void removeTab(QWidget *w); + + /// Save current status of sidebar, called at exit + void saveState(KSharedConfigPtr cfg, const QString &group); + + /// Restore status from config, called at startup + void loadState(KSharedConfigPtr cfg, const QString &group); + + /// Change the text of a tab + void changeTabText(QWidget *w, const QString &text); + + /// Change the icon of a tab + void changeTabIcon(QWidget *w, const QString &icon); + +private Q_SLOTS: + void onActionTriggered(QAction *act); + void toolButtonStyleChanged(Qt::ToolButtonStyle style); + void setToolButtonStyle(); + +private: + void shrink(); + void unshrink(); + +private: + QToolBar *tab_bar; + ActionGroup *action_group; + QStackedWidget *widget_stack; + bool shrunken; + QMap widget_to_action; +}; + +class ActionGroup : public QObject +{ + Q_OBJECT +public: + ActionGroup(QObject *parent = nullptr); + ~ActionGroup() override; + + void addAction(QAction *act); + void removeAction(QAction *act); + +private Q_SLOTS: + void toggled(bool on); + +Q_SIGNALS: + void actionTriggered(QAction *a); + +private: + QList actions; +}; +} + +#endif // TABBARWIDGET_H diff --git a/libktcore/interfaces/activity.cpp b/libktcore/interfaces/activity.cpp new file mode 100644 index 0000000..a036a1e --- /dev/null +++ b/libktcore/interfaces/activity.cpp @@ -0,0 +1,73 @@ +/* + SPDX-FileCopyrightText: 2009 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "activity.h" + +#include +#include +#include + +namespace kt +{ +ActivityPart::ActivityPart(Activity *parent) + : KParts::Part(parent) +{ +} + +ActivityPart::~ActivityPart() +{ +} + +void ActivityPart::setXMLGUIFile(const QString &xml_gui) +{ + setXMLFile(xml_gui, true); +} + +QMenu *ActivityPart::menu(const QString &name) +{ + return qobject_cast(factory()->container(name, this)); +} + +Activity::Activity(const QString &name, const QString &icon, int weight, QWidget *parent) + : QWidget(parent) + , activity_name(name) + , activity_icon(icon) + , activity_weight(weight) + , activity_part(nullptr) +{ +} + +Activity::~Activity() +{ +} + +void Activity::setXMLGUIFile(const QString &xml_file) +{ + if (!activity_part) + activity_part = new ActivityPart(this); + + activity_part->setXMLGUIFile(xml_file); +} + +void Activity::setName(const QString &name) +{ + activity_name = name; + nameChanged(this, name); +} + +void Activity::setIcon(const QString &icon) +{ + activity_icon = icon; + iconChanged(this, icon); +} + +bool Activity::lessThan(Activity *l, Activity *r) +{ + if (l->weight() == r->weight()) + return QString::compare(l->name(), r->name()) < 0; // KF5 QCollator + else + return l->weight() < r->weight(); +} +} diff --git a/libktcore/interfaces/activity.h b/libktcore/interfaces/activity.h new file mode 100644 index 0000000..9ff196c --- /dev/null +++ b/libktcore/interfaces/activity.h @@ -0,0 +1,100 @@ +/* + SPDX-FileCopyrightText: 2009 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef ACTIVITY_H +#define ACTIVITY_H + +#include +#include +#include + +#include + +class QMenu; + +namespace kt +{ +class Activity; + +/** + Part of an Activity +*/ +class KTCORE_EXPORT ActivityPart : public KParts::Part +{ + Q_OBJECT +public: + ActivityPart(Activity *parent); + ~ActivityPart() override; + + /// Set the XML GUI file of the part + void setXMLGUIFile(const QString &xml_gui); + + /// Get a menu described in the XML of the part + QMenu *menu(const QString &name); +}; + +/** + * Base class for all activities. + */ +class KTCORE_EXPORT Activity : public QWidget +{ + Q_OBJECT +public: + Activity(const QString &name, const QString &icon, int weight, QWidget *parent); + ~Activity() override; + + /// Get the name of the activity + const QString &name() const + { + return activity_name; + } + + /// Get the icon name + const QString &icon() const + { + return activity_icon; + } + + /// Get the part + ActivityPart *part() const + { + return activity_part; + } + + /// Set the name + void setName(const QString &name); + + /// Set the icon + void setIcon(const QString &icon); + + /// Get the weight + int weight() const + { + return activity_weight; + } + + static bool lessThan(Activity *l, Activity *r); + +protected: + /** + Activities wishing to provide toolbar and menu entries, should + call this function to set the XML GUI description. + @param xml_file The XMLGUI file + */ + void setXMLGUIFile(const QString &xml_file); + +Q_SIGNALS: + void nameChanged(Activity *a, const QString &name); + void iconChanged(Activity *a, const QString &icon); + +private: + QString activity_name; + QString activity_icon; + int activity_weight; + ActivityPart *activity_part; +}; +} + +#endif // ACTIVITY_H diff --git a/libktcore/interfaces/coreinterface.cpp b/libktcore/interfaces/coreinterface.cpp new file mode 100644 index 0000000..a358185 --- /dev/null +++ b/libktcore/interfaces/coreinterface.cpp @@ -0,0 +1,17 @@ +/* + SPDX-FileCopyrightText: 2005 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "coreinterface.h" + +namespace kt +{ +CoreInterface::CoreInterface() +{ +} + +CoreInterface::~CoreInterface() +{ +} +} diff --git a/libktcore/interfaces/coreinterface.h b/libktcore/interfaces/coreinterface.h new file mode 100644 index 0000000..9b625c0 --- /dev/null +++ b/libktcore/interfaces/coreinterface.h @@ -0,0 +1,271 @@ +/* + SPDX-FileCopyrightText: 2005 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KTCOREINTERFACE_H +#define KTCOREINTERFACE_H + +#include +#include + +#include +#include + +namespace bt +{ +class TorrentInterface; +class MagnetLink; +class TorrentCreator; +} + +namespace kt +{ +/// Stats struct +struct CurrentStats { + bt::Uint32 download_speed; + bt::Uint32 upload_speed; + bt::Uint64 bytes_downloaded; + bt::Uint64 bytes_uploaded; +}; + +struct MagnetLinkLoadOptions { + bool silently; + QString group; + QString location; + QString move_on_completion; +}; + +class MagnetManager; +class QueueManager; +class GroupManager; +class DBus; + +/** + * @author Joris Guisson + * @brief Interface for plugins to communicate with the application's core + * + * This interface provides the plugin with the functionality to modify + * the applications core, the core is responsible for managing all + * TorrentControl objects. + */ +class KTCORE_EXPORT CoreInterface : public QObject +{ + Q_OBJECT +public: + CoreInterface(); + ~CoreInterface() override; + + /** + * Set whether or not we should keep seeding after + * a download has finished. + * @param ks Keep seeding yes or no + */ + virtual void setKeepSeeding(bool ks) = 0; + + /** + * Change the data dir. This involves copying + * all data from the old dir to the new. + * This can offcourse go horribly wrong, therefore + * if it doesn't succeed it returns false + * and leaves everything where it supposed to be. + * @param new_dir The new directory + */ + virtual bool changeDataDir(const QString &new_dir) = 0; + + /** + * Start all, takes into account the maximum number of downloads. + */ + virtual void startAll() = 0; + + /** + * Stop all torrents. + */ + virtual void stopAll() = 0; + + /** + * Start a torrent, takes into account the maximum number of downloads. + * @param tc The TorrentControl + */ + virtual void start(bt::TorrentInterface *tc) = 0; + + /** + * Start a list of torrents. + * @param todo The list of torrents + */ + virtual void start(QList &todo) = 0; + + /** + * Stop a torrent, may start another download if it hasn't been started. + * @param tc The TorrentControl + * @param user true if user stopped the torrent, false otherwise + */ + virtual void stop(bt::TorrentInterface *tc) = 0; + + /** + * Stop a list of torrents. + * @param todo The list of torrents + */ + virtual void stop(QList &todo) = 0; + + /** + * Pause a torrent + * @param tc The torrent + */ + virtual void pause(bt::TorrentInterface *tc) = 0; + + /** + * Pause a list of torrents. + * @param todo The list of torrents + */ + virtual void pause(QList &todo) = 0; + + /// Get CurrentStats structure + virtual CurrentStats getStats() = 0; + + /** + * Switch the port + * @param port The new port + * @return true if we can, false otherwise + */ + virtual bool changePort(bt::Uint16 port) = 0; + + /// Get the number of torrents running (including seeding torrents). + virtual bt::Uint32 getNumTorrentsRunning() const = 0; + + /// Get the number of torrents not running. + virtual bt::Uint32 getNumTorrentsNotRunning() const = 0; + + /** + * Load a torrent file. Pops up an error dialog + * if something goes wrong. Will ask the user for a save location, or use + * the default. + * @param url The torrent file + * @param group Group to add torrent to + */ + virtual void load(const QUrl &url, const QString &group) = 0; + + /** + * Load a torrent file. Pops up an error dialog + * if something goes wrong. Will ask the user for a save location, or use + * the default. This will not popup a file selection dialog for multi file torrents. + * @param url The torrent file + * @param group Group to add torrent to + */ + virtual void loadSilently(const QUrl &url, const QString &group) = 0; + + /** + * Load a torrent using a byte array + * @param data Data of the torrent + * @param url URL of the torrent + * @param group Group to use + * @param savedir Directory to save to + * @return The loaded TorrentInterface or 0 on failure + */ + virtual bt::TorrentInterface *load(const QByteArray &data, const QUrl &url, const QString &group, const QString &savedir) = 0; + + /** + * Load a torrent using a byte array silently + * @param data Data of the torrent + * @param url URL of the torrent + * @param group Group to use + * @param savedir Directory to save to + * @return The loaded TorrentInterface or 0 on failure + */ + virtual bt::TorrentInterface *loadSilently(const QByteArray &data, const QUrl &url, const QString &group, const QString &savedir) = 0; + + /** + * Remove a download.This will delete all temp + * data from this TorrentControl And delete the + * TorrentControl itself. It can also potentially + * start a new download (when one is waiting to be downloaded). + * @param tc The torrent + * @param data_to Whether or not to delete the file data to + */ + virtual void remove(bt::TorrentInterface *tc, bool data_to) = 0; + + /** + * Remove a list of downloads. + * @param todo The torrent list + * @param data_to Whether or not to delete the file data to + */ + virtual void remove(QList &todo, bool data_to) = 0; + + /** + * Find the next free torX dir. + * @return Path to the dir (including the torX part) + */ + virtual QString findNewTorrentDir() const = 0; + + /** + * Load an existing torrent, which has already a properly set up torX dir. + * @param tor_dir The torX dir + */ + virtual void loadExistingTorrent(const QString &tor_dir) = 0; + + /** + * Sets global suspended state for all torrents (QueueManager) and stopps all torrents. + * No torrents will be automatically started/stopped. + */ + virtual void setSuspendedState(bool suspend) = 0; + + /// Gets the globla suspended state + virtual bool getSuspendedState() = 0; + + /// Get the QueueManager + virtual kt::QueueManager *getQueueManager() = 0; + + /// Get the GroupManager + virtual kt::GroupManager *getGroupManager() = 0; + + /// Get the MagnetManager + virtual kt::MagnetManager *getMagnetManager() = 0; + + /// Get a pointer to the external interface object (for dbus and scripting) + virtual DBus *getExternalInterface() = 0; + + /// Apply all settings + virtual void applySettings() = 0; + + /// Load a magnet link + virtual void load(const bt::MagnetLink &mlink, const MagnetLinkLoadOptions &options) = 0; + + /// Create a torrent (Note: hash calculation should be finished, and torrent should have been saved) + virtual bt::TorrentInterface *createTorrent(bt::TorrentCreator *tc, bool seed) = 0; + +Q_SIGNALS: + /** + * A bt::TorrentInterface was added + * @param tc + */ + void torrentAdded(bt::TorrentInterface *tc); + + /** + * A TorrentInterface was removed + * @param tc + */ + void torrentRemoved(bt::TorrentInterface *tc); + + /** + * A TorrentInterface has finished downloading. + * @param tc + */ + void finished(bt::TorrentInterface *tc); + + /** + * Torrent download is stopped by error + * @param tc TorrentInterface + * @param msg Error message + */ + void torrentStoppedByError(bt::TorrentInterface *tc, QString msg); + + /** + * Signal emmitted when the settings have been changed in the settings dialog. + * Plugins interested in this should update their internal states. + * */ + void settingsChanged(); +}; + +} + +#endif diff --git a/libktcore/interfaces/functions.cpp b/libktcore/interfaces/functions.cpp new file mode 100644 index 0000000..15affcb --- /dev/null +++ b/libktcore/interfaces/functions.cpp @@ -0,0 +1,155 @@ +/* + SPDX-FileCopyrightText: 2005 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "functions.h" + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "settings.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace bt; + +namespace kt +{ +QString DataDir(CreationMode mode) +{ + QString dataDirPath = QStandardPaths::writableLocation(QStandardPaths::DataLocation); + if (mode == CreateIfNotExists) { + QFileInfo fileInfo(dataDirPath); + if (!fileInfo.exists()) { + QString ktorrent4DataFolder = QDir::homePath() + QLatin1String("/.kde/share/apps/ktorrent"); + if (!QFile::exists(ktorrent4DataFolder)) { + ktorrent4DataFolder = QDir::homePath() + QLatin1String("/.kde4/share/apps/ktorrent"); + if (!QFile::exists(ktorrent4DataFolder)) + ktorrent4DataFolder.clear(); + } + if (ktorrent4DataFolder.isEmpty() || !QFile::rename(ktorrent4DataFolder, dataDirPath)) + fileInfo.dir().mkdir(fileInfo.fileName()); + } + } + // if (!str.endsWith(bt::DirSeparator())) + return dataDirPath + bt::DirSeparator(); + // else + // return str; +} + +Uint16 RandomGoodPort() +{ + Uint16 start = 50000; + while (true) { + Uint16 port = start + QRandomGenerator::global()->bounded(10000); + if (port != Settings::port() && port != Settings::dhtPort() && port != Settings::udpTrackerPort()) + return port; + } +} + +void ApplySettings() +{ + PeerManager::connectionLimits().setLimits(Settings::maxTotalConnections(), Settings::maxConnections()); + net::SocketMonitor::setDownloadCap(Settings::maxDownloadRate() * 1024); + net::SocketMonitor::setUploadCap(Settings::maxUploadRate() * 1024); + net::SocketMonitor::setSleepTime(Settings::cpuUsage()); + mse::EncryptedPacketSocket::setTOS(Settings::dscp() << 2); + bt::PeerConnector::setMaxActive(Settings::maxConnectingSockets()); + + // Check for port conflicts + if (Settings::port() == Settings::udpTrackerPort()) + Settings::setUdpTrackerPort(RandomGoodPort()); + + if (Settings::port() == Settings::dhtPort()) + Settings::setDhtPort(RandomGoodPort()); + + UDPTrackerSocket::setPort(Settings::udpTrackerPort()); + Choker::setNumUploadSlots(Settings::numUploadSlots()); + + dht::DHTBase &ht = Globals::instance().getDHT(); + if (Settings::dhtSupport() && !ht.isRunning()) { + ht.start(kt::DataDir() + QLatin1String("dht_table"), kt::DataDir() + QLatin1String("dht_key"), Settings::dhtPort()); + } else if (!Settings::dhtSupport() && ht.isRunning()) { + ht.stop(); + } else if (Settings::dhtSupport() && ht.getPort() != Settings::dhtPort()) { + Out(SYS_GEN | LOG_NOTICE) << "Restarting DHT with new port " << Settings::dhtPort() << endl; + ht.stop(); + ht.start(kt::DataDir() + QLatin1String("dht_table"), kt::DataDir() + QLatin1String("dht_key"), Settings::dhtPort()); + } + + UTPex::setEnabled(Settings::pexEnabled()); + + if (Settings::useEncryption()) { + ServerInterface::enableEncryption(Settings::allowUnencryptedConnections()); + } else { + ServerInterface::disableEncryption(); + } + + if (Settings::useCustomIP()) + Tracker::setCustomIP(Settings::customIP()); + else + Tracker::setCustomIP(QString()); + + QString proxy = Settings::httpProxy(); + + bt::HTTPTracker::setProxyEnabled(!Settings::useKDEProxySettings() && Settings::useProxyForTracker()); + bt::HTTPTracker::setProxy(proxy, Settings::httpProxyPort()); + bt::WebSeed::setProxy(proxy, Settings::httpProxyPort()); + bt::WebSeed::setProxyEnabled(!Settings::useKDEProxySettings() && Settings::useProxyForWebSeeds()); + bt::Cache::setPreallocationEnabled(Settings::diskPrealloc()); + bt::Cache::setPreallocateFully(Settings::fullDiskPrealloc()); + + bt::TorrentControl::setDataCheckWhenCompleted(Settings::checkWhenFinished()); + bt::TorrentControl::setMinimumDiskSpace(Settings::minDiskSpace()); + bt::SetNetworkInterface(Settings::networkInterface()); + net::Socks::setSocksEnabled(Settings::socksEnabled()); + net::Socks::setSocksVersion(Settings::socksVersion()); + net::Socks::setSocksServerAddress(Settings::socksProxy(), Settings::socksPort()); + if (Settings::socksUsePassword()) + net::Socks::setSocksAuthentication(Settings::socksUsername(), Settings::socksPassword()); + else + net::Socks::setSocksAuthentication(QString(), QString()); + + bt::ChunkManager::setPreviewSizes(Settings::previewSizeAudio() * 1024, Settings::previewSizeVideo() * 1024); + bt::QueueManagerInterface::setQueueManagerEnabled(!Settings::manuallyControlTorrents()); + bt::Downloader::setUseWebSeeds(Settings::webseedsEnabled()); + bt::Peer::setResolveHostnames(Settings::lookUpHostnameOfPeers()); +} + +QString TorrentFileFilter(bool all_files_included) +{ + QString ret = i18nc("*.torrent", "Torrents") + QLatin1String(" (*.torrent)"); + if (all_files_included) + ret += QLatin1String(";;") + i18n("All files") + QLatin1String(" (*)"); + return ret; +} + +} diff --git a/libktcore/interfaces/functions.h b/libktcore/interfaces/functions.h new file mode 100644 index 0000000..4aa9cd2 --- /dev/null +++ b/libktcore/interfaces/functions.h @@ -0,0 +1,30 @@ +/* + SPDX-FileCopyrightText: 2005 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef FUNCTIONS_H +#define FUNCTIONS_H + +#include +#include +#include + +namespace kt +{ +enum CreationMode { + DoNotCheckDirPresence, + CreateIfNotExists, +}; +/// Get the data directory of ktorrent (~/.local/share/ktorrent most of the time) +KTCORE_EXPORT QString DataDir(CreationMode mode = DoNotCheckDirPresence); + +/// Apply all settings +KTCORE_EXPORT void ApplySettings(); + +/// Get the filter string for torrent files used file dialogs +KTCORE_EXPORT QString TorrentFileFilter(bool all_files_included); + +} + +#endif diff --git a/libktcore/interfaces/guiinterface.cpp b/libktcore/interfaces/guiinterface.cpp new file mode 100644 index 0000000..61ea8e2 --- /dev/null +++ b/libktcore/interfaces/guiinterface.cpp @@ -0,0 +1,18 @@ +/* + SPDX-FileCopyrightText: 2005 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "guiinterface.h" +#include + +namespace kt +{ +GUIInterface::GUIInterface() +{ +} + +GUIInterface::~GUIInterface() +{ +} +} diff --git a/libktcore/interfaces/guiinterface.h b/libktcore/interfaces/guiinterface.h new file mode 100644 index 0000000..de58412 --- /dev/null +++ b/libktcore/interfaces/guiinterface.h @@ -0,0 +1,115 @@ +/* + SPDX-FileCopyrightText: 2005 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KTGUIINTERFACE_H +#define KTGUIINTERFACE_H + +#include +#include + +class QString; +class QProgressBar; +class KMainWindow; + +namespace KIO +{ +class Job; +} + +namespace kt +{ +class PrefPageInterface; +class Plugin; +class GUIInterface; +class Activity; +class TorrentActivityInterface; + +/** + * Base class for the status bar + * */ +class KTCORE_EXPORT StatusBarInterface +{ +public: + virtual ~StatusBarInterface() + { + } + + /// Show a message on the statusbar for some period of time + virtual void message(const QString &msg) = 0; + + /// Create a progress bar and put it on the right side of the statusbar + virtual QProgressBar *createProgressBar() = 0; + + /// Remove a progress bar created with createProgressBar (pb will be deleteLater'ed) + virtual void removeProgressBar(QProgressBar *pb) = 0; +}; + +/** + * @author Joris Guisson + * @brief Interface to modify the GUI + * + * This interface allows plugins and others to modify the GUI. + */ +class KTCORE_EXPORT GUIInterface +{ +public: + GUIInterface(); + virtual ~GUIInterface(); + + /// Get a pointer to the main window + virtual KMainWindow *getMainWindow() = 0; + + /// Add an activity + virtual void addActivity(Activity *act) = 0; + + /// Remove an activity + virtual void removeActivity(Activity *act) = 0; + + /// Set the current activity + virtual void setCurrentActivity(Activity *act) = 0; + + /** + * Add a page to the preference dialog. + * @param page The page + */ + virtual void addPrefPage(PrefPageInterface *page) = 0; + + /** + * Remove a page from the preference dialog. + * @param page The page + */ + virtual void removePrefPage(PrefPageInterface *page) = 0; + + /** + * Merge the GUI of a plugin. + * @param p The Plugin + */ + virtual void mergePluginGui(Plugin *p) = 0; + + /** + * Remove the GUI of a plugin. + * @param p The Plugin + */ + virtual void removePluginGui(Plugin *p) = 0; + + /// Show an error message box + virtual void errorMsg(const QString &err) = 0; + + /// Show an error message for a KIO job which went wrong + virtual void errorMsg(KIO::Job *j) = 0; + + /// Show an information dialog + virtual void infoMsg(const QString &info) = 0; + + /// Get the status bar + virtual StatusBarInterface *getStatusBar() = 0; + + /// Get the torrent activity + virtual TorrentActivityInterface *getTorrentActivity() = 0; +}; + +} + +#endif diff --git a/libktcore/interfaces/plugin.cpp b/libktcore/interfaces/plugin.cpp new file mode 100644 index 0000000..ba2f70d --- /dev/null +++ b/libktcore/interfaces/plugin.cpp @@ -0,0 +1,29 @@ +/* + SPDX-FileCopyrightText: 2005 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "plugin.h" + +namespace kt +{ +Plugin::Plugin(QObject *parent) + : KParts::Plugin(parent) +{ + core = nullptr; + gui = nullptr; + loaded = false; +} + +Plugin::~Plugin() +{ +} + +void Plugin::guiUpdate() +{ +} + +void Plugin::shutdown(bt::WaitJob *) +{ +} +} diff --git a/libktcore/interfaces/plugin.h b/libktcore/interfaces/plugin.h new file mode 100644 index 0000000..b5d2699 --- /dev/null +++ b/libktcore/interfaces/plugin.h @@ -0,0 +1,140 @@ +/* + SPDX-FileCopyrightText: 2005 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KTPLUGIN_H +#define KTPLUGIN_H + +#include +#include +#include + +namespace bt +{ +class WaitJob; +} + +namespace kt +{ +class CoreInterface; +class GUIInterface; + +/** + * @author Joris Guisson + * @brief Base class for all plugins + * + * This is the base class for all plugins. Plugins should implement + * the load and unload methods, any changes made in load must be undone in + * unload. + * + * It's also absolutely forbidden to do any complex initialization in the constructor + * (setting an int to 0 is ok, creating widgets isn't). + * Only the name, author and description may be set in the constructor. + */ +class KTCORE_EXPORT Plugin : public KParts::Plugin +{ + Q_OBJECT +public: + Plugin(QObject *parent); + ~Plugin() override; + + /** + * This gets called, when the plugin gets loaded by KTorrent. + * Any changes made here must be later made undone, when unload is + * called. + * Upon error a bt::Error should be thrown. And the plugin should remain + * in an uninitialized state. The Error contains an error message, which will + * get show to the user. + */ + virtual void load() = 0; + + /** + * Gets called when the plugin gets unloaded. + * Should undo anything load did. + */ + virtual void unload() = 0; + + /** + * For plugins who need to update something, the same time as the + * GUI updates. + */ + virtual void guiUpdate(); + + /** + * This should be implemented by plugins who need finish of some stuff which might take some time. + * These operations must be finished or killed by a timeout before we can proceed with unloading the plugin. + * @param job The WaitJob which monitors the plugin + */ + virtual void shutdown(bt::WaitJob *job); + + /// Get a pointer to the CoreInterface + CoreInterface *getCore() + { + return core; + } + + /// Get a const pointer to the CoreInterface + const CoreInterface *getCore() const + { + return core; + } + + /** + * Set the core, used by PluginManager to set the pointer + * to the core. + * @param c Pointer to the core + */ + void setCore(CoreInterface *c) + { + core = c; + } + + /// Get a pointer to the CoreInterface + GUIInterface *getGUI() + { + return gui; + } + + /// Get a const pointer to the CoreInterface + const GUIInterface *getGUI() const + { + return gui; + } + + /** + * Set the core, used by PluginManager to set the pointer + * to the core. + * @param c Pointer to the core + */ + void setGUI(GUIInterface *c) + { + gui = c; + } + + /// See if the plugin is loaded + bool isLoaded() const + { + return loaded; + } + + /// Check whether the plugin matches the version of KT + virtual bool versionCheck(const QString &version) const = 0; + + /// Returns the name of the parent part the GUI of the plugin should be created in + virtual QString parentPart() const + { + return QStringLiteral("ktorrent"); + } + +private: + CoreInterface *core; + GUIInterface *gui; + bool loaded; + + friend class PluginManager; +}; + +} + +#endif diff --git a/libktcore/interfaces/prefpageinterface.cpp b/libktcore/interfaces/prefpageinterface.cpp new file mode 100644 index 0000000..b3e25c3 --- /dev/null +++ b/libktcore/interfaces/prefpageinterface.cpp @@ -0,0 +1,33 @@ +/* + SPDX-FileCopyrightText: 2005 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "prefpageinterface.h" + +namespace kt +{ +PrefPageInterface::PrefPageInterface(KConfigSkeleton *cfg, const QString &name, const QString &icon, QWidget *parent) + : QWidget(parent) + , cfg(cfg) + , name(name) + , icon(icon) +{ +} + +PrefPageInterface::~PrefPageInterface() +{ +} + +void PrefPageInterface::loadSettings() +{ +} + +void PrefPageInterface::loadDefaults() +{ +} + +void PrefPageInterface::updateSettings() +{ +} +} diff --git a/libktcore/interfaces/prefpageinterface.h b/libktcore/interfaces/prefpageinterface.h new file mode 100644 index 0000000..2d814f1 --- /dev/null +++ b/libktcore/interfaces/prefpageinterface.h @@ -0,0 +1,75 @@ +/* + SPDX-FileCopyrightText: 2005 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef PREFPAGEINTERFACE_H +#define PREFPAGEINTERFACE_H + +#include +#include + +class KConfigSkeleton; + +namespace kt +{ +/** + * @author Ivan Vasic + * @brief Interface to add configuration dialog page. + * + * This interface allows plugins and others to add their own pages in Configuration dialog. + */ +class KTCORE_EXPORT PrefPageInterface : public QWidget +{ + Q_OBJECT +public: + PrefPageInterface(KConfigSkeleton *cfg, const QString &name, const QString &icon, QWidget *parent); + ~PrefPageInterface() override; + + /** + * Initialize the settings. + * Called by the settings dialog when it is created. + */ + virtual void loadSettings(); + + /** + * Load default settings. + * Called when the defaults button is pressed in the settings dialog. + */ + virtual void loadDefaults(); + + /** + * Called when user presses OK or apply. + */ + virtual void updateSettings(); + + KConfigSkeleton *config() + { + return cfg; + } + const QString &pageName() + { + return name; + } + const QString &pageIcon() + { + return icon; + } + + /// Override if there are custom widgets outside which have changed + virtual bool customWidgetsChanged() + { + return false; + } + +Q_SIGNALS: + /// Emitted when buttons need to be updated + void updateButtons(); + +private: + KConfigSkeleton *cfg; + QString name; + QString icon; +}; +} +#endif diff --git a/libktcore/interfaces/torrentactivityinterface.cpp b/libktcore/interfaces/torrentactivityinterface.cpp new file mode 100644 index 0000000..d504023 --- /dev/null +++ b/libktcore/interfaces/torrentactivityinterface.cpp @@ -0,0 +1,34 @@ +/* + SPDX-FileCopyrightText: 2009 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "torrentactivityinterface.h" + +namespace kt +{ +TorrentActivityInterface::TorrentActivityInterface(const QString &name, const QString &icon, QWidget *parent) + : Activity(name, icon, 0, parent) +{ +} + +TorrentActivityInterface::~TorrentActivityInterface() +{ +} + +void TorrentActivityInterface::notifyViewListeners(bt::TorrentInterface *tc) +{ + for (ViewListener *vl : qAsConst(listeners)) + vl->currentTorrentChanged(tc); +} + +void TorrentActivityInterface::addViewListener(ViewListener *vl) +{ + listeners.append(vl); +} + +void TorrentActivityInterface::removeViewListener(ViewListener *vl) +{ + listeners.removeAll(vl); +} +} diff --git a/libktcore/interfaces/torrentactivityinterface.h b/libktcore/interfaces/torrentactivityinterface.h new file mode 100644 index 0000000..7b6a5a2 --- /dev/null +++ b/libktcore/interfaces/torrentactivityinterface.h @@ -0,0 +1,84 @@ +/* + SPDX-FileCopyrightText: 2009 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef TORRENTACTIVITYINTERFACE_H +#define TORRENTACTIVITYINTERFACE_H + +#include +#include + +namespace bt +{ +class TorrentInterface; +} + +namespace kt +{ +class View; +class Group; + +/** + * Small interface for classes who want to know when + * current torrent in the gui changes. + */ +class KTCORE_EXPORT ViewListener +{ +public: + ViewListener() + { + } + virtual ~ViewListener() + { + } + + virtual void currentTorrentChanged(bt::TorrentInterface *tc) = 0; +}; + +/** + Interface for the TorrentActivity class +*/ +class KTCORE_EXPORT TorrentActivityInterface : public Activity +{ +public: + TorrentActivityInterface(const QString &name, const QString &icon, QWidget *parent); + ~TorrentActivityInterface() override; + + /// Add a view listener. + void addViewListener(ViewListener *vl); + + /// Remove a view listener + void removeViewListener(ViewListener *vl); + + /// Get the current torrent. + virtual const bt::TorrentInterface *getCurrentTorrent() const = 0; + + /// Get the current torrent + virtual bt::TorrentInterface *getCurrentTorrent() = 0; + + /// Update all actions + virtual void updateActions() = 0; + + /// Add a tool widget to the activity + virtual void addToolWidget(QWidget *widget, const QString &text, const QString &icon, const QString &tooltip) = 0; + + /// Remove a tool widget + virtual void removeToolWidget(QWidget *widget) = 0; + + /// Add a new custom group + virtual Group *addNewGroup() = 0; + +protected: + /** + * Notifies all view listeners of the change in the current downloading TorrentInterface + * @param tc Pointer to current TorrentInterface + */ + void notifyViewListeners(bt::TorrentInterface *tc); + +private: + QList listeners; +}; +} + +#endif // TORRENTACTIVITYINTERFACE_H diff --git a/libktcore/ktorrent.kcfg b/libktcore/ktorrent.kcfg new file mode 100644 index 0000000..6967eeb --- /dev/null +++ b/libktcore/ktorrent.kcfg @@ -0,0 +1,354 @@ + + + + + + + + 5 + 0 + + + + 10 + 0 + + + + 0 + + + + + 120 + 0 + + + + 800 + 0 + + + + 0 + 0 + + + + 0 + 0 + + + + 0 + 0 + + + 0.8 + 0 + + + + 6881 + 0 + 65535 + + + + 8881 + 0 + 65535 + + + + true + + + + false + + + + false + + + + 500 + 0 + 1000000 + + + + 500 + 0 + 1000000 + + + + false + + + + true + + + + true + + + + + + + false + + + + + + + false + + + + + + + false + + + + QString() + + + + QString() + + + + 500 + 5000 + 1000 + + + + false + + + + 7881 + 1 + 65535 + + + true + + + + 2 + 2 + 100 + + + + false + + + + true + + + 8 + 0 + 255 + + + 0 + 0 + 63 + + + 50 + 10 + 500 + + + false + + + + + + true + + + QString() + + + 8080 + 1 + 65535 + + + true + + + true + + + false + + + QString() + + + 1080 + 1 + 65535 + + + 4 + 5 + 5 + + + false + + + QString() + + + QString() + + + true + + + false + + + + 100 + 10 + 10000 + + + 50 + 1 + 250 + + + + false + + + + + + + 0 + 0 + + + + + false + + + false + + + false + + + 1 + 15 + + + QColor(40, 205, 40) + + + QColor(255, 174, 0) + + + QColor(Qt::red) + + + true + + + QColor(40, 205, 40) + + + QColor(255, 80, 0) + + + QColor(0, 170, 110) + + + QColor(Qt::red) + + + QColor(40, 205, 40) + + + QColor(Qt::red) + + + 16 + 256 + + + 16 + 2048 + + + https://newtrackon.com/api/stable + + + true + + + false + + + true + + + true + + + false + + + false + + + 0 + 0 + 1 + + + false + + + true + + + 1 + 100 + 5 + + + true + + + 1 + 60 + 5 + + + diff --git a/libktcore/ktversion.h b/libktcore/ktversion.h new file mode 100644 index 0000000..6c50634 --- /dev/null +++ b/libktcore/ktversion.h @@ -0,0 +1,19 @@ +/* + SPDX-FileCopyrightText: 2005 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#ifndef KTVERSION_HH +#define KTVERSION_HH + +#include "util/constants.h" +#include + +namespace kt +{ +const bt::Uint32 MAJOR = VERSION_MAJOR; +const bt::Uint32 MINOR = VERSION_MINOR; +const bt::Uint32 RELEASE = VERSION_MICRO; +const bt::VersionType VERSION_TYPE = bt::NORMAL; +} + +#endif diff --git a/libktcore/plugin/pluginactivity.cpp b/libktcore/plugin/pluginactivity.cpp new file mode 100644 index 0000000..c698f4d --- /dev/null +++ b/libktcore/plugin/pluginactivity.cpp @@ -0,0 +1,54 @@ +/* + SPDX-FileCopyrightText: 2005 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include +#include +#include + +#include +#include + +#include "pluginactivity.h" +#include "pluginmanager.h" +#include "settings.h" +#include +#include + +using namespace bt; + +namespace kt +{ +PluginActivity::PluginActivity(PluginManager *pman) + : Activity(i18n("Plugins"), QStringLiteral("plugins"), 5, nullptr) + , pman(pman) +{ + QVBoxLayout *layout = new QVBoxLayout(this); + layout->setMargin(0); + pmw = new KPluginSelector(this); + connect(pmw, &KPluginSelector::changed, this, &PluginActivity::changed); + connect(pmw, &KPluginSelector::configCommitted, this, &PluginActivity::changed); + layout->addWidget(pmw); +} + +PluginActivity::~PluginActivity() +{ +} + +void PluginActivity::updatePluginList() +{ + pmw->addPlugins(pman->pluginInfoList(), KPluginSelector::IgnoreConfigFile, i18n("Plugins")); +} + +void PluginActivity::update() +{ + pmw->updatePluginsState(); + pman->loadPlugins(); +} + +void PluginActivity::changed() +{ + update(); +} +} diff --git a/libktcore/plugin/pluginactivity.h b/libktcore/plugin/pluginactivity.h new file mode 100644 index 0000000..4bed0ff --- /dev/null +++ b/libktcore/plugin/pluginactivity.h @@ -0,0 +1,42 @@ +/* + SPDX-FileCopyrightText: 2005 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KTPLUGINACTIVITY_H +#define KTPLUGINACTIVITY_H + +#include +#include + +class KPluginSelector; + +namespace kt +{ +class PluginManager; + +/** + * @author Joris Guisson + * + * Pref page which allows to load and unload plugins. + */ +class PluginActivity : public Activity +{ + Q_OBJECT +public: + PluginActivity(PluginManager *pman); + ~PluginActivity() override; + + void updatePluginList(); + void update(); +private Q_SLOTS: + void changed(); + +private: + PluginManager *pman; + KPluginSelector *pmw; +}; + +} + +#endif diff --git a/libktcore/plugin/pluginmanager.cpp b/libktcore/plugin/pluginmanager.cpp new file mode 100644 index 0000000..2b2c992 --- /dev/null +++ b/libktcore/plugin/pluginmanager.cpp @@ -0,0 +1,185 @@ +/* + SPDX-FileCopyrightText: 2005 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "pluginmanager.h" + +#include +#include + +#include +#include +#include + +#include "pluginactivity.h" +#include +#include +#include +#include +#include +#include + +using namespace bt; + +namespace kt +{ +PluginManager::PluginManager(CoreInterface *core, GUIInterface *gui) + : core(core) + , gui(gui) +{ + prefpage = nullptr; + loaded.setAutoDelete(true); +} + +PluginManager::~PluginManager() +{ + delete prefpage; +} + +void PluginManager::loadPluginList() +{ + pluginsMetaData = KPluginLoader::findPlugins(QStringLiteral("ktorrent")); + if (pluginsMetaData.isEmpty()) { + // simple workaround for the situation i have in debian --Nick + QStringList paths = QCoreApplication::libraryPaths(); + if (paths.isEmpty()) + paths << QLatin1String("/usr/lib/x86_64-linux-gnu/plugins"); + + QCoreApplication::addLibraryPath(paths.first().remove(QLatin1String("qt5/"))); + pluginsMetaData = KPluginLoader::findPlugins(QStringLiteral("ktorrent")); + } + + for (const KPluginMetaData &module : qAsConst(pluginsMetaData)) { + KPluginInfo pi(module); + pi.setConfig(KSharedConfig::openConfig()->group(pi.pluginName())); + pi.load(); + + plugins << pi; + } + + if (!prefpage) { + prefpage = new PluginActivity(this); + gui->addActivity(prefpage); + } + + prefpage->updatePluginList(); + loadPlugins(); + prefpage->update(); +} + +void PluginManager::loadPlugins() +{ + int idx = 0; + for (auto i = plugins.begin(); i != plugins.end(); i++) { + KPluginInfo &pi = *i; + if (loaded.contains(idx) && !pi.isPluginEnabled()) { + // unload it + unload(pi, idx); + pi.save(); + } else if (!loaded.contains(idx) && pi.isPluginEnabled()) { + // load it + load(pi, idx); + pi.save(); + } + idx++; + } +} + +void PluginManager::load(const KPluginInfo &pi, int idx) +{ + Q_UNUSED(pi) + KPluginLoader loader(pluginsMetaData.at(idx).fileName()); + KPluginFactory *factory = loader.factory(); + if (!factory) + return; + + Plugin *plugin = factory->create(); + if (!plugin) { + Out(SYS_GEN | LOG_NOTICE) << QStringLiteral("Creating instance of plugin %1 failed !").arg(pluginsMetaData.at(idx).fileName()) << endl; + return; + } + + if (!plugin->versionCheck(QStringLiteral(VERSION))) { + Out(SYS_GEN | LOG_NOTICE) << QStringLiteral("Plugin %1 version does not match KTorrent version, unloading it.").arg(pluginsMetaData.at(idx).fileName()) + << endl; + + delete plugin; + } else { + plugin->setCore(core); + plugin->setGUI(gui); + plugin->load(); + gui->mergePluginGui(plugin); + plugin->loaded = true; + loaded.insert(idx, plugin, true); + } +} + +void PluginManager::unload(const KPluginInfo &pi, int idx) +{ + Q_UNUSED(pi) + + Plugin *p = loaded.find(idx); + if (!p) + return; + + // first shut it down properly + bt::WaitJob *wjob = new WaitJob(2000); + try { + p->shutdown(wjob); + if (wjob->needToWait()) + bt::WaitJob::execute(wjob); + else + delete wjob; + } catch (Error &err) { + Out(SYS_GEN | LOG_NOTICE) << "Error when unloading plugin: " << err.toString() << endl; + } + + gui->removePluginGui(p); + p->unload(); + p->loaded = false; + loaded.erase(idx); +} + +void PluginManager::unloadAll() +{ + // first properly shutdown all plugins + bt::WaitJob *wjob = new WaitJob(2000); + try { + bt::PtrMap::iterator i = loaded.begin(); + while (i != loaded.end()) { + Plugin *p = i->second; + p->shutdown(wjob); + i++; + } + if (wjob->needToWait()) + bt::WaitJob::execute(wjob); + else + delete wjob; + } catch (Error &err) { + Out(SYS_GEN | LOG_NOTICE) << "Error when unloading all plugins: " << err.toString() << endl; + } + + // then unload them + bt::PtrMap::iterator i = loaded.begin(); + while (i != loaded.end()) { + Plugin *p = i->second; + gui->removePluginGui(p); + p->unload(); + p->loaded = false; + i++; + } + loaded.clear(); +} + +void PluginManager::updateGuiPlugins() +{ + bt::PtrMap::iterator i = loaded.begin(); + while (i != loaded.end()) { + Plugin *p = i->second; + p->guiUpdate(); + i++; + } +} + +} diff --git a/libktcore/plugin/pluginmanager.h b/libktcore/plugin/pluginmanager.h new file mode 100644 index 0000000..02df19f --- /dev/null +++ b/libktcore/plugin/pluginmanager.h @@ -0,0 +1,83 @@ +/* + SPDX-FileCopyrightText: 2005 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KTPLUGINMANAGER_H +#define KTPLUGINMANAGER_H + +#include +#include +#include + +#include +#include + +#include +#include +#include + +namespace kt +{ +class CoreInterface; +class GUIInterface; +class PluginActivity; + +/** + * @author Joris Guisson + * @brief Class to manage plugins + * + * This class manages all plugins. Plugins are stored in a map + */ +class KTCORE_EXPORT PluginManager +{ + KPluginInfo::List plugins; + QVector pluginsMetaData; + CoreInterface *core; + GUIInterface *gui; + PluginActivity *prefpage; + bt::PtrMap loaded; + +public: + PluginManager(CoreInterface *core, GUIInterface *gui); + ~PluginManager(); + + /** + * Get the plugin info list. + */ + const KPluginInfo::List &pluginInfoList() const + { + return plugins; + } + + /** + * Load the list of plugins. + * This basically uses KTrader to get a list of available plugins, and + * loads those, but does not initialize them. We will consider a plugin loaded + * when it's load method is called. + */ + void loadPluginList(); + + /** + * Check the PluginInfo of each plugin and unload or load it if necessary + */ + void loadPlugins(); + + /** + * Update all plugins who need a periodical GUI update. + */ + void updateGuiPlugins(); + + /** + * Unload all plugins. + */ + void unloadAll(); + +private: + void load(const KPluginInfo &pi, int idx); + void unload(const KPluginInfo &pi, int idx); +}; + +} + +#endif diff --git a/libktcore/settings.kcfgc b/libktcore/settings.kcfgc new file mode 100644 index 0000000..b24db18 --- /dev/null +++ b/libktcore/settings.kcfgc @@ -0,0 +1,8 @@ +# Code generation options for kconfig_compiler +File=ktorrent.kcfg +ClassName=Settings +Singleton=true +Mutators=true +Visibility=KTCORE_EXPORT +IncludeFiles=ktcore_export.h +# will create the necessary code for setting those variables diff --git a/libktcore/torrent/basicjobprogresswidget.cpp b/libktcore/torrent/basicjobprogresswidget.cpp new file mode 100644 index 0000000..6db85c3 --- /dev/null +++ b/libktcore/torrent/basicjobprogresswidget.cpp @@ -0,0 +1,79 @@ +/* + SPDX-FileCopyrightText: 2010 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "basicjobprogresswidget.h" + +#include +#include +#include + +#include + +#include + +namespace kt +{ +BasicJobProgressWidget::BasicJobProgressWidget(bt::Job *job, QWidget *parent) + : JobProgressWidget(job, parent) +{ + setupUi(this); + job_description->clear(); + job_title->clear(); + job_speed->clear(); + msg->clear(); + msg->setVisible(false); +} + +BasicJobProgressWidget::~BasicJobProgressWidget() +{ +} + +void BasicJobProgressWidget::description(const QString &title, const QPair &field1, const QPair &field2) +{ + job_title->setText(title); + job_description->setText(i18n("%1: %2
%3: %4", field1.first, field1.second, field2.first, field2.second)); + resized(this); +} + +void BasicJobProgressWidget::infoMessage(const QString &plain, const QString &rich) +{ + Q_UNUSED(plain); + msg->setText(rich); + msg->setVisible(true); + resized(this); +} + +void BasicJobProgressWidget::warning(const QString &plain, const QString &rich) +{ + Q_UNUSED(plain); + msg->setText(i18n("Warning: %1", rich)); + msg->setVisible(true); + resized(this); +} + +void BasicJobProgressWidget::totalAmount(KJob::Unit unit, qulonglong amount) +{ + Q_UNUSED(unit); + progress->setMaximum(amount); +} + +void BasicJobProgressWidget::processedAmount(KJob::Unit unit, qulonglong amount) +{ + Q_UNUSED(unit); + progress->setValue(amount); +} + +void BasicJobProgressWidget::percent(long unsigned int percent) +{ + progress->setValue(percent); + progress->setMaximum(100); +} + +void BasicJobProgressWidget::speed(long unsigned int value) +{ + job_speed->setText(bt::BytesPerSecToString(value)); +} + +} diff --git a/libktcore/torrent/basicjobprogresswidget.h b/libktcore/torrent/basicjobprogresswidget.h new file mode 100644 index 0000000..218431a --- /dev/null +++ b/libktcore/torrent/basicjobprogresswidget.h @@ -0,0 +1,41 @@ +/* + SPDX-FileCopyrightText: 2010 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KT_BASICJOBPROGRESSWIDGET_H +#define KT_BASICJOBPROGRESSWIDGET_H + +#include "ui_basicjobprogresswidget.h" +#include + +namespace kt +{ +/** + Basic JobProgressWidget, showing a progress bar and the description + */ +class BasicJobProgressWidget : public kt::JobProgressWidget, public Ui_BasicJobProgressWidget +{ + Q_OBJECT +public: + BasicJobProgressWidget(bt::Job *job, QWidget *parent); + ~BasicJobProgressWidget() override; + + void description(const QString &title, const QPair &field1, const QPair &field2) override; + void infoMessage(const QString &plain, const QString &rich) override; + void warning(const QString &plain, const QString &rich) override; + void totalAmount(KJob::Unit unit, qulonglong amount) override; + void processedAmount(KJob::Unit unit, qulonglong amount) override; + void percent(long unsigned int percent) override; + void speed(long unsigned int value) override; + + bool similar(Extender *ext) const override + { + Q_UNUSED(ext); + return false; + } +}; + +} + +#endif // KT_BASICJOBPROGRESSWIDGET_H diff --git a/libktcore/torrent/basicjobprogresswidget.ui b/libktcore/torrent/basicjobprogresswidget.ui new file mode 100644 index 0000000..47f1d96 --- /dev/null +++ b/libktcore/torrent/basicjobprogresswidget.ui @@ -0,0 +1,67 @@ + + + BasicJobProgressWidget + + + + 0 + 0 + 400 + 95 + + + + + + + + 75 + true + true + + + + TextLabel + + + + + + + TextLabel + + + + + + + + + 24 + + + + + + + TextLabel + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + TextLabel + + + + + + + + diff --git a/libktcore/torrent/chunkbar.cpp b/libktcore/torrent/chunkbar.cpp new file mode 100644 index 0000000..b2c0fc2 --- /dev/null +++ b/libktcore/torrent/chunkbar.cpp @@ -0,0 +1,134 @@ +/* + SPDX-FileCopyrightText: 2005 Joris Guisson + SPDX-FileCopyrightText: 2005 Vincent Wagelaar + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include + +#include "chunkbar.h" +#include "chunkbarrenderer.h" +#include +#include +#include +#include + +using namespace bt; +using namespace kt; + +namespace kt +{ +#if 0 // KF5 +static void FillAndFrameBlack(QImage* image, const QColor& color, int size) +{ + image->fill(color.rgb()); + for (int i = 0; i < size; i++) { + image->setPixel(0, i, 0); + image->setPixel(size - 1, i, 0); + image->setPixel(i, 0, 0); + image->setPixel(i, size - 1, 0); + } +} + +static void InitializeToolTipImages(ChunkBar* bar) +{ + static bool images_initialized = false; + if (images_initialized) + return; + images_initialized = true; + + Q3MimeSourceFactory* factory = Q3MimeSourceFactory::defaultFactory(); + + QImage excluded(16, 16, QImage::Format_RGB32); + FillAndFrameBlack(&excluded, bar->palette().color(QPalette::Active, QPalette::Mid), 16); + factory->setImage("excluded_color", excluded); + + QImage available(16, 16, QImage::Format_RGB32); + FillAndFrameBlack(&available, bar->palette().color(QPalette::Active, QPalette::Highlight), 16); + factory->setImage("available_color", available); + + QImage unavailable(16, 16, QImage::Format_RGB32); + FillAndFrameBlack(&unavailable, bar->palette().color(QPalette::Active, QPalette::Base), 16); + factory->setImage("unavailable_color", unavailable); +} +#endif + +ChunkBar::ChunkBar(QWidget *parent) + : QFrame(parent) +{ + setFrameShape(StyledPanel); + setFrameShadow(Sunken); + setLineWidth(3); + setMidLineWidth(3); + +#if 0 // KF5 + InitializeToolTipImages(this); + setToolTip(i18n("  - Downloaded Chunks
" + "  - Chunks to Download
" + "  - Excluded Chunks")); +#endif +} + +ChunkBar::~ChunkBar() +{ +} + +void ChunkBar::updateBar(bool force) +{ + const BitSet &bs = getBitSet(); + QSize s = contentsRect().size(); + + bool changed = !(curr == bs); + + if (changed || pixmap.isNull() || pixmap.width() != s.width() || force) { + pixmap = QPixmap(s); + pixmap.fill(palette().color(QPalette::Active, QPalette::Base)); + QPainter painter(&pixmap); + drawBarContents(&painter); + update(); + } +} + +void ChunkBar::paintEvent(QPaintEvent *ev) +{ + QFrame::paintEvent(ev); + QPainter p(this); + drawContents(&p); +} + +void ChunkBar::drawContents(QPainter *p) +{ + // first draw background + bool enable = isEnabled(); + p->setBrush(palette().color(enable ? QPalette::Active : QPalette::Inactive, QPalette::Base)); + p->setPen(Qt::NoPen); // p->setPen(QPen(Qt::red)); + p->drawRect(contentsRect()); + if (enable) + p->drawPixmap(contentsRect(), pixmap); +} + +void ChunkBar::drawBarContents(QPainter *p) +{ + Uint32 w = contentsRect().width(); + const BitSet &bs = getBitSet(); + curr = bs; + QColor highlight_color = palette().color(QPalette::Active, QPalette::Highlight); + if (bs.allOn()) + drawAllOn(p, highlight_color, contentsRect()); + else if (curr.getNumBits() > w) + drawMoreChunksThenPixels(p, bs, highlight_color, contentsRect()); + else + drawEqual(p, bs, highlight_color, contentsRect()); +} + +} diff --git a/libktcore/torrent/chunkbar.h b/libktcore/torrent/chunkbar.h new file mode 100644 index 0000000..6e8ecff --- /dev/null +++ b/libktcore/torrent/chunkbar.h @@ -0,0 +1,54 @@ +/* + SPDX-FileCopyrightText: 2005-2007 Joris Guisson + SPDX-FileCopyrightText: 2005-2007 Vincent Wagelaar + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef CHUNKBAR_H +#define CHUNKBAR_H + +#include +#include + +#include "chunkbarrenderer.h" +#include +#include + +class QPainter; + +namespace bt +{ +class BitSet; +} + +namespace kt +{ +/** + * @author Joris Guisson, Vincent Wagelaar + * + * Bar which displays BitSets, subclasses need to fill the BitSet. + * BitSets can represent which chunks are downloaded, which chunks are available + * and which chunks are excluded. + */ +class KTCORE_EXPORT ChunkBar : public QFrame, public ChunkBarRenderer +{ + Q_OBJECT +public: + ChunkBar(QWidget *parent); + ~ChunkBar() override; + + virtual const bt::BitSet &getBitSet() const = 0; + void drawContents(QPainter *p); + virtual void updateBar(bool force = false); + +protected: + virtual void drawBarContents(QPainter *p); + void paintEvent(QPaintEvent *ev) override; + +protected: + bt::BitSet curr; + QPixmap pixmap; +}; +} + +#endif diff --git a/libktcore/torrent/chunkbarrenderer.cpp b/libktcore/torrent/chunkbarrenderer.cpp new file mode 100644 index 0000000..0d020dd --- /dev/null +++ b/libktcore/torrent/chunkbarrenderer.cpp @@ -0,0 +1,134 @@ +/* + SPDX-FileCopyrightText: 2008 Joris Guisson + SPDX-FileCopyrightText: 2008 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include + +#include "chunkbarrenderer.h" +#include + +using namespace bt; + +namespace kt +{ +struct Range { + Uint32 first, last; + int fac; +}; + +ChunkBarRenderer::ChunkBarRenderer() +{ +} + +ChunkBarRenderer::~ChunkBarRenderer() +{ +} + +void ChunkBarRenderer::drawEqual(QPainter *p, const BitSet &bs, const QColor &color, const QRect &contents_rect) +{ + // p->setPen(QPen(colorGroup().highlight(),1,Qt::SolidLine)); + QColor c = color; + + Uint32 w = contents_rect.width(); + double scale = 1.0; + Uint32 total_chunks = bs.getNumBits(); + if (total_chunks != w) + scale = (double)w / total_chunks; + + p->setPen(QPen(c, 1, Qt::SolidLine)); + p->setBrush(c); + + QVector rs; + + for (Uint32 i = 0; i < bs.getNumBits(); i++) { + if (!bs.get(i)) + continue; + + if (rs.empty()) { + Range r = {i, i, 0}; + rs.append(r); + } else { + Range &l = rs.last(); + if (l.last == i - 1) { + l.last = i; + } else { + Range r = {i, i, 0}; + rs.append(r); + } + } + } + + QRect r = contents_rect; + + for (auto i = rs.constBegin(); i != rs.constEnd(); ++i) { + const Range &ra = *i; + int rw = ra.last - ra.first + 1; + p->drawRect((int)(scale * ra.first), 0, (int)(rw * scale), r.height()); + } +} + +void ChunkBarRenderer::drawMoreChunksThenPixels(QPainter *p, const BitSet &bs, const QColor &color, const QRect &contents_rect) +{ + Uint32 w = contents_rect.width(); + double chunks_per_pixel = (double)bs.getNumBits() / w; + QVector rs; + + for (Uint32 i = 0; i < w; i++) { + Uint32 num_dl = 0; + Uint32 jStart = (Uint32)(i * chunks_per_pixel); + Uint32 jEnd = (Uint32)((i + 1) * chunks_per_pixel + 0.5); + for (Uint32 j = jStart; j < jEnd; j++) + if (bs.get(j)) + num_dl++; + + if (num_dl == 0) + continue; + + int fac = int(100 * ((double)num_dl / (jEnd - jStart)) + 0.5); + if (rs.empty()) { + Range r = {i, i, fac}; + rs.append(r); + } else { + Range &l = rs.last(); + if (l.last == i - 1 && l.fac == fac) { + l.last = i; + } else { + Range r = {i, i, fac}; + rs.append(r); + } + } + } + + QRect r = contents_rect; + + for (auto i = rs.constBegin(); i != rs.constEnd(); ++i) { + const Range &ra = *i; + int rw = ra.last - ra.first + 1; + int fac = ra.fac; + QColor c = color; + if (fac < 100) { + // do some rounding off + if (fac <= 25) + fac = 25; + else if (fac <= 50) + fac = 45; + else + fac = 65; + c = color.lighter(200 - fac); + } + p->setPen(QPen(c, 1, Qt::SolidLine)); + p->setBrush(c); + p->drawRect(ra.first, 0, rw, r.height()); + } +} + +void ChunkBarRenderer::drawAllOn(QPainter *p, const QColor &color, const QRect &contents_rect) +{ + p->setPen(QPen(color, 1, Qt::SolidLine)); + p->setBrush(color); + QSize s = contents_rect.size(); + p->drawRect(0, 0, s.width(), s.height()); +} +} diff --git a/libktcore/torrent/chunkbarrenderer.h b/libktcore/torrent/chunkbarrenderer.h new file mode 100644 index 0000000..54e6f62 --- /dev/null +++ b/libktcore/torrent/chunkbarrenderer.h @@ -0,0 +1,35 @@ +/* + SPDX-FileCopyrightText: 2008 Joris Guisson + SPDX-FileCopyrightText: 2008 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KTCHUNKBARRENDERER_H +#define KTCHUNKBARRENDERER_H + +#include + +namespace bt +{ +class BitSet; +} + +namespace kt +{ +/** + Class which renders a chunkbar to a a QPainter +*/ +class KTCORE_EXPORT ChunkBarRenderer +{ +public: + ChunkBarRenderer(); + ~ChunkBarRenderer(); + + void drawEqual(QPainter *p, const bt::BitSet &bs, const QColor &color, const QRect &contents_rect); + void drawMoreChunksThenPixels(QPainter *p, const bt::BitSet &bs, const QColor &color, const QRect &contents_rect); + void drawAllOn(QPainter *p, const QColor &color, const QRect &contents_rect); +}; + +} + +#endif diff --git a/libktcore/torrent/jobprogresswidget.cpp b/libktcore/torrent/jobprogresswidget.cpp new file mode 100644 index 0000000..254573f --- /dev/null +++ b/libktcore/torrent/jobprogresswidget.cpp @@ -0,0 +1,27 @@ +/* + SPDX-FileCopyrightText: 2010 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "jobprogresswidget.h" +#include + +namespace kt +{ +JobProgressWidget::JobProgressWidget(bt::Job *job, QWidget *parent) + : Extender(job->torrent(), parent) + , job(job) + , automatic_remove(true) +{ +} + +JobProgressWidget::~JobProgressWidget() +{ +} + +void JobProgressWidget::emitCloseRequest() +{ + closeRequest(this); +} + +} diff --git a/libktcore/torrent/jobprogresswidget.h b/libktcore/torrent/jobprogresswidget.h new file mode 100644 index 0000000..b4b12e1 --- /dev/null +++ b/libktcore/torrent/jobprogresswidget.h @@ -0,0 +1,71 @@ +/* + SPDX-FileCopyrightText: 2010 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KT_JOBPROGRESSWIDGET_H +#define KT_JOBPROGRESSWIDGET_H + +#include +#include + +#include +#include +#include + +namespace kt +{ +/** + * Base class for widgets displaying the progress of a job + */ +class KTCORE_EXPORT JobProgressWidget : public Extender +{ + Q_OBJECT +public: + JobProgressWidget(bt::Job *job, QWidget *parent); + ~JobProgressWidget() override; + + /// Update the description + virtual void description(const QString &title, const QPair &field1, const QPair &field2) = 0; + + /// Show an informational message + virtual void infoMessage(const QString &plain, const QString &rich) = 0; + + /// Show a warning message + virtual void warning(const QString &plain, const QString &rich) = 0; + + /// The total amount of unit has changed + virtual void totalAmount(KJob::Unit unit, qulonglong amount) = 0; + + /// The processed amount has changed + virtual void processedAmount(KJob::Unit unit, qulonglong amount) = 0; + + /// The percentage has changed + virtual void percent(long unsigned int percent) = 0; + + /// The speed has changed + virtual void speed(long unsigned int value) = 0; + + /// Emit the close request so the ViewDelegate will clean things up + void emitCloseRequest(); + + /// Whether or not to automatically remove the widget + bool automaticRemove() const + { + return automatic_remove; + } + +protected: + void setAutomaticRemove(bool ar) + { + automatic_remove = ar; + } + +protected: + bt::Job *job; + bool automatic_remove; +}; + +} + +#endif // KT_JOBPROGRESSWIDGET_H diff --git a/libktcore/torrent/jobtracker.cpp b/libktcore/torrent/jobtracker.cpp new file mode 100644 index 0000000..1234d0d --- /dev/null +++ b/libktcore/torrent/jobtracker.cpp @@ -0,0 +1,141 @@ +/* + SPDX-FileCopyrightText: 2010 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "jobtracker.h" +#include "basicjobprogresswidget.h" + +namespace kt +{ +JobTracker::JobTracker(QObject *parent) + : KJobTrackerInterface(parent) +{ + bt::Job::setJobTracker(this); +} + +JobTracker::~JobTracker() +{ + bt::Job::setJobTracker(nullptr); +} + +void JobTracker::registerJob(KJob *job) +{ + bt::Job *j = dynamic_cast(job); + if (!j) + return; + + KJobTrackerInterface::registerJob(job); + jobRegistered(j); +} + +void JobTracker::unregisterJob(KJob *job) +{ + bt::Job *j = dynamic_cast(job); + if (!j) + return; + + KJobTrackerInterface::unregisterJob(job); + jobUnregistered(j); + widgets.remove(j); +} + +JobProgressWidget *JobTracker::createJobWidget(bt::Job *job) +{ + JobProgressWidget *p = new BasicJobProgressWidget(job, nullptr); + widgets[job] = p; + return p; +} + +void JobTracker::finished(KJob *job) +{ + KJobTrackerInterface::finished(job); +} + +void JobTracker::suspended(KJob *job) +{ + KJobTrackerInterface::suspended(job); +} + +void JobTracker::resumed(KJob *job) +{ + KJobTrackerInterface::resumed(job); +} + +void JobTracker::description(KJob *job, const QString &title, const QPair &field1, const QPair &field2) +{ + bt::Job *j = dynamic_cast(job); + if (!j) + return; + + ActiveJobs::iterator i = widgets.find(j); + if (i != widgets.end()) + i.value()->description(title, field1, field2); +} + +void JobTracker::infoMessage(KJob *job, const QString &plain, const QString &rich) +{ + bt::Job *j = dynamic_cast(job); + if (!j) + return; + + ActiveJobs::iterator i = widgets.find(j); + if (i != widgets.end()) + i.value()->infoMessage(plain, rich); +} + +void JobTracker::warning(KJob *job, const QString &plain, const QString &rich) +{ + bt::Job *j = dynamic_cast(job); + if (!j) + return; + + ActiveJobs::iterator i = widgets.find(j); + if (i != widgets.end()) + i.value()->warning(plain, rich); +} + +void JobTracker::totalAmount(KJob *job, KJob::Unit unit, qulonglong amount) +{ + bt::Job *j = dynamic_cast(job); + if (!j) + return; + + ActiveJobs::iterator i = widgets.find(j); + if (i != widgets.end()) + i.value()->totalAmount(unit, amount); +} + +void JobTracker::processedAmount(KJob *job, KJob::Unit unit, qulonglong amount) +{ + bt::Job *j = dynamic_cast(job); + if (!j) + return; + + ActiveJobs::iterator i = widgets.find(j); + if (i != widgets.end()) + i.value()->processedAmount(unit, amount); +} + +void JobTracker::percent(KJob *job, long unsigned int percent) +{ + bt::Job *j = dynamic_cast(job); + if (!j) + return; + + ActiveJobs::iterator i = widgets.find(j); + if (i != widgets.end()) + i.value()->percent(percent); +} + +void JobTracker::speed(KJob *job, long unsigned int value) +{ + bt::Job *j = dynamic_cast(job); + if (!j) + return; + + ActiveJobs::iterator i = widgets.find(j); + if (i != widgets.end()) + i.value()->speed(value); +} +} diff --git a/libktcore/torrent/jobtracker.h b/libktcore/torrent/jobtracker.h new file mode 100644 index 0000000..1c81789 --- /dev/null +++ b/libktcore/torrent/jobtracker.h @@ -0,0 +1,59 @@ +/* + SPDX-FileCopyrightText: 2010 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KT_JOBTRACKER_H +#define KT_JOBTRACKER_H + +#include + +#include +#include + +namespace kt +{ +class JobProgressWidget; + +/** + JobTracker for bt::Job's + */ +class KTCORE_EXPORT JobTracker : public KJobTrackerInterface +{ + Q_OBJECT +public: + JobTracker(QObject *parent); + ~JobTracker() override; + + void registerJob(KJob *job) override; + void unregisterJob(KJob *job) override; + + /// A job has been registered + virtual void jobRegistered(bt::Job *j) = 0; + + /// A job has been unregistered + virtual void jobUnregistered(bt::Job *j) = 0; + + /// Create a widget for a job + virtual JobProgressWidget *createJobWidget(bt::Job *job); + +protected: + void finished(KJob *job) override; + void suspended(KJob *job) override; + void resumed(KJob *job) override; + void description(KJob *job, const QString &title, const QPair &field1, const QPair &field2) override; + void infoMessage(KJob *job, const QString &plain, const QString &rich) override; + void warning(KJob *job, const QString &plain, const QString &rich) override; + void totalAmount(KJob *job, KJob::Unit unit, qulonglong amount) override; + void processedAmount(KJob *job, KJob::Unit unit, qulonglong amount) override; + void percent(KJob *job, long unsigned int percent) override; + void speed(KJob *job, long unsigned int value) override; + +protected: + typedef QMap ActiveJobs; + ActiveJobs widgets; +}; + +} + +#endif // KT_JOBTRACKER_H diff --git a/libktcore/torrent/magnetmanager.cpp b/libktcore/torrent/magnetmanager.cpp new file mode 100644 index 0000000..3555323 --- /dev/null +++ b/libktcore/torrent/magnetmanager.cpp @@ -0,0 +1,537 @@ +/* + SPDX-FileCopyrightText: 2014 Juan Palacios + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "magnetmanager.h" + +#include +#include + +#include +#include + +#include +#include +#include +#include +#include + +using namespace bt; + +namespace kt +{ +DownloadSlot::DownloadSlot(QObject *parent) + : magnetIdx(-1) + , timerDuration(0) +{ + timer = new QTimer(parent); + timer->setSingleShot(true); + timer->setInterval(timerDuration); + connect(timer, &QTimer::timeout, this, &DownloadSlot::onTimeout); +} + +DownloadSlot::~DownloadSlot() +{ + delete timer; +} + +void DownloadSlot::setTimerDuration(unsigned int duration) +{ + timerDuration = duration; + timer->setInterval(duration); +} + +void DownloadSlot::startTimer() +{ + timer->start(); +} + +void DownloadSlot::stopTimer() +{ + timer->stop(); +} + +void DownloadSlot::reset() +{ + stopTimer(); + setTimerDuration(timerDuration); + magnetIdx = -1; +} + +void DownloadSlot::setMagnetIndex(int index) +{ + magnetIdx = index; +} + +int DownloadSlot::getMagnetIndex() const +{ + return magnetIdx; +} + +bool DownloadSlot::isTimerActived() const +{ + return timer->isActive(); +} + +bool DownloadSlot::isOccupied() const +{ + return magnetIdx < 0; +} + +void DownloadSlot::onTimeout() +{ + Q_EMIT timeout(magnetIdx); +} + +//--------------------------------------------------- + +MagnetManager::MagnetManager(QObject *parent) + : QObject(parent) + , useSlotTimer(true) + , timerDuration(180000) + , usedDownloadingSlots() + , freeDownloadingSlots() + , magnetQueue() + , stoppedList() + , magnetHashes() + , stoppedHashes() +{ + setDownloadingSlots(1); +} + +MagnetManager::~MagnetManager() +{ + for (DownloadSlot *slot : qAsConst(usedDownloadingSlots)) + delete slot; + + for (DownloadSlot *slot : qAsConst(freeDownloadingSlots)) + delete slot; +} + +void MagnetManager::addMagnet(const bt::MagnetLink &mlink, const kt::MagnetLinkLoadOptions &options, bool stopped) +{ + if (magnetHashes.contains(mlink.infoHash())) + return; // Already managed, do nothing + + MagnetDownloader *md = new MagnetDownloader(mlink, options, this); + connect(md, &MagnetDownloader::foundMetadata, this, &MagnetManager::onDownloadFinished); + + int updateIndex = 0; + int updateCount = 0; + if (stopped) { + stoppedList.append(md); + stoppedHashes.insert(mlink.infoHash()); + magnetHashes.insert(mlink.infoHash()); + + updateIndex = magnetHashes.size() - 1; + updateCount = 1; + } else { + magnetQueue.append(md); + magnetHashes.insert(mlink.infoHash()); + + int nextIndex = startNextQueuedMagnets(); + if (nextIndex >= 0) + updateIndex = nextIndex; + else + updateIndex = magnetQueue.size() - 1; + updateCount = magnetHashes.size() - updateIndex; + } + Q_EMIT updateQueue(updateIndex, updateCount); +} + +void MagnetManager::removeMagnets(bt::Uint32 idx, bt::Uint32 count) +{ + if (idx >= (Uint32)magnetHashes.size() || count < 1) + return; + + while (count > 0 && idx < (Uint32)magnetHashes.size()) { + MagnetDownloader *md = nullptr; + Uint32 magnetQueueSize = magnetQueue.size(); + if (idx < magnetQueueSize) { + md = magnetQueue.at(idx); + magnetQueue.removeAt(idx); + if (md->running()) + freeDownloadSlot(idx); + } else { + int stoppedIdx = idx - magnetQueueSize; + md = stoppedList.at(stoppedIdx); + stoppedList.removeAt(stoppedIdx); + stoppedHashes.remove(md->magnetLink().infoHash()); + } + magnetHashes.remove(md->magnetLink().infoHash()); + md->deleteLater(); + + --count; + } + + int updateIndex = startNextQueuedMagnets(); + if (updateIndex < 0) + updateIndex = idx; + + Q_EMIT updateQueue(updateIndex, magnetHashes.size() - updateIndex); +} + +void MagnetManager::start(bt::Uint32 idx, bt::Uint32 count) +{ + Uint32 magnetQueueSize = magnetQueue.size(); + if (idx + count < magnetQueueSize || count < 1) + return; + + if (idx < magnetQueueSize) { // jump to stopped magnets + int alreadyStarted = magnetQueueSize - idx; + idx += alreadyStarted; + count -= alreadyStarted; + } + + Uint32 updateIndex = idx; + Uint32 updateCount = 0; + + int stoppedIdx = idx - magnetQueueSize; + Uint32 totalMagnets = magnetHashes.size(); + while (count > 0 && idx < totalMagnets) { + MagnetDownloader *md = stoppedList.at(stoppedIdx); + stoppedList.removeAt(stoppedIdx); + stoppedHashes.remove(md->magnetLink().infoHash()); + magnetQueue.append(md); + + --count; + ++idx; + ++updateCount; + } + + int startedIdx = startNextQueuedMagnets(); + if (startedIdx >= 0) + updateIndex = startedIdx; + + if (updateCount > 0) + Q_EMIT updateQueue(updateIndex, updateCount); +} + +void MagnetManager::stop(bt::Uint32 idx, bt::Uint32 count) +{ + Uint32 magnetQueueSize = magnetQueue.size(); + if (idx >= magnetQueueSize || count < 1) + return; + + if (idx + count >= magnetQueueSize) + count -= (idx + count) - magnetQueueSize; // do not include already stopped magnets + + Uint32 updateCount = count; + Uint32 updateIndex = idx; + + while (count > 0) { + MagnetDownloader *md = magnetQueue.at(idx); + if (md->running()) { + md->stop(); + freeDownloadSlot(idx); + } + magnetQueue.removeAt(idx); + stoppedList.append(md); + stoppedHashes.insert(md->magnetLink().infoHash()); + + --count; + } + + int startedIdx = startNextQueuedMagnets(); + if (startedIdx >= 0) + updateIndex = startedIdx; + + if (updateCount > 0) + Q_EMIT updateQueue(updateIndex, updateCount); +} + +bool MagnetManager::isStopped(bt::Uint32 idx) const +{ + if (idx < (Uint32)magnetQueue.size()) + return false; + + return true; +} + +void MagnetManager::setDownloadingSlots(bt::Uint32 count) +{ + int updateIndex = 0; + int updateCount = 0; + int totalSlots = usedDownloadingSlots.size() + freeDownloadingSlots.size(); + int slotsToAdd = count - totalSlots; + if (slotsToAdd > 0) { // add new slots + for (int i = 0; i < slotsToAdd; ++i) { + DownloadSlot *slot = new DownloadSlot(); + slot->setTimerDuration(timerDuration); + freeDownloadingSlots.push_back(slot); + connect(slot, &DownloadSlot::timeout, this, &MagnetManager::onSlotTimeout); + } + updateIndex = startNextQueuedMagnets(); + updateCount = slotsToAdd; + } else { // remove slots + int slotsToRemove = std::abs(slotsToAdd); + + // try to remove free slots + if (!freeDownloadingSlots.isEmpty()) { + while (slotsToRemove > 0 && !freeDownloadingSlots.isEmpty()) { + DownloadSlot *slot = freeDownloadingSlots.front(); + freeDownloadingSlots.pop_front(); + delete slot; + + --slotsToRemove; + } + } + + if (slotsToRemove == 0) { + // all removed slots where unused so we don't need to emit an updateQueue signal + updateIndex = -1; + } else { // remove used slots + updateIndex = usedDownloadingSlots.size() - slotsToRemove; + if (updateIndex < 0) + updateIndex = 0; + updateCount = slotsToRemove; + + while (slotsToRemove > 0 && !usedDownloadingSlots.isEmpty()) { + DownloadSlot *slot = usedDownloadingSlots.back(); + usedDownloadingSlots.pop_back(); + slot->stopTimer(); + magnetQueue.at(slot->getMagnetIndex())->stop(); + delete slot; + + --slotsToRemove; + } + } + } + if (updateIndex >= 0) + Q_EMIT updateQueue(updateIndex, updateCount); +} + +void MagnetManager::setUseSlotTimer(bool value) +{ + useSlotTimer = value; + + if (!useSlotTimer) { + for (DownloadSlot *slot : qAsConst(usedDownloadingSlots)) { + slot->stopTimer(); + slot->setTimerDuration(timerDuration); + } + } else { + for (DownloadSlot *slot : qAsConst(usedDownloadingSlots)) { + if (!slot->isTimerActived()) { + slot->setTimerDuration(timerDuration); + slot->startTimer(); + } + } + } +} + +void MagnetManager::setTimerDuration(bt::Uint32 duration) +{ + timerDuration = duration * 60000; // convert to milliseconds + for (DownloadSlot *slot : qAsConst(usedDownloadingSlots)) + slot->setTimerDuration(timerDuration); + + for (DownloadSlot *slot : qAsConst(freeDownloadingSlots)) + slot->setTimerDuration(timerDuration); + + Q_EMIT updateQueue(0, usedDownloadingSlots.size()); +} + +void MagnetManager::update() +{ + for (DownloadSlot *slot : qAsConst(usedDownloadingSlots)) + magnetQueue.at(slot->getMagnetIndex())->update(); + + Q_EMIT updateQueue(0, usedDownloadingSlots.size()); +} + +void MagnetManager::loadMagnets(const QString &file) +{ + QFile fptr(file); + if (!fptr.open(QIODevice::ReadOnly)) { + Out(SYS_GEN | LOG_NOTICE) << "Failed to open " << file << " : " << fptr.errorString() << endl; + return; + } + + QByteArray magnet_data = fptr.readAll(); + if (magnet_data.size() == 0) + return; + + BDecoder decoder(magnet_data, 0, false); + BNode *node = nullptr; + try { + node = decoder.decode(); + if (!node || node->getType() != BNode::LIST) + throw Error(QStringLiteral("Corrupted magnet file")); + + BListNode *ml = (BListNode *)node; + for (Uint32 i = 0; i < ml->getNumChildren(); i++) { + BDictNode *dict = ml->getDict(i); + MagnetLink mlink(dict->getString(QByteArrayLiteral("magnet"), nullptr)); + MagnetLinkLoadOptions options; + bool stopped = dict->getInt(QByteArrayLiteral("stopped")) == 1; + options.silently = dict->getInt(QByteArrayLiteral("silent")) == 1; + + if (dict->keys().contains("group")) + options.group = dict->getString(QByteArrayLiteral("group"), nullptr); + if (dict->keys().contains("location")) + options.location = dict->getString(QByteArrayLiteral("location"), nullptr); + if (dict->keys().contains("move_on_completion")) + options.move_on_completion = dict->getString(QByteArrayLiteral("move_on_completion"), nullptr); + + addMagnet(mlink, options, stopped); + } + } catch (Error &err) { + Out(SYS_GEN | LOG_NOTICE) << "Failed to load " << file << " : " << err.toString() << endl; + } + delete node; +} + +void MagnetManager::saveMagnets(const QString &file) +{ + File fptr; + if (!fptr.open(file, QStringLiteral("wb"))) { + Out(SYS_GEN | LOG_NOTICE) << "Failed to open " << file << " : " << fptr.errorString() << endl; + return; + } + + BEncoder enc(&fptr); + enc.beginList(); + + for (MagnetDownloader *md : qAsConst(magnetQueue)) + writeEncoderInfo(enc, md); + + for (MagnetDownloader *md : qAsConst(stoppedList)) + writeEncoderInfo(enc, md); + + enc.end(); +} + +void MagnetManager::writeEncoderInfo(bt::BEncoder &enc, kt::MagnetDownloader *md) +{ + enc.beginDict(); + enc.write(QByteArrayLiteral("magnet"), md->magnetLink().toString().toUtf8()); + enc.write(QByteArrayLiteral("stopped"), stoppedHashes.contains(md->magnetLink().infoHash())); + enc.write(QByteArrayLiteral("silent"), md->options.silently); + enc.write(QByteArrayLiteral("group"), md->options.group.toUtf8()); + enc.write(QByteArrayLiteral("location"), md->options.location.toUtf8()); + enc.write(QByteArrayLiteral("move_on_completion"), md->options.move_on_completion.toUtf8()); + enc.end(); +} + +MagnetManager::MagnetState MagnetManager::status(bt::Uint32 idx) const +{ + Q_ASSERT(idx < (Uint32)magnetHashes.size()); + + const MagnetDownloader *md = getMagnetDownloader(idx); + + if (idx >= (Uint32)magnetQueue.size()) + return STOPPED; + else if (md->running()) + return DOWNLOADING; + else + return QUEUED; +} + +int MagnetManager::count() const +{ + return magnetHashes.size(); +} + +const MagnetDownloader *MagnetManager::getMagnetDownloader(bt::Uint32 idx) const +{ + Q_ASSERT(idx < (Uint32)magnetHashes.size()); + + MagnetDownloader *md = nullptr; + + Uint32 downloadQueueSize = magnetQueue.size(); + if (idx < downloadQueueSize) + md = magnetQueue.at(idx); + else + md = stoppedList.at(idx - downloadQueueSize); + + return md; +} + +void MagnetManager::onDownloadFinished(bt::MagnetDownloader *md, const QByteArray &data) +{ + MagnetDownloader *ktmd = (MagnetDownloader *)md; + Q_EMIT metadataDownloaded(md->magnetLink(), data, ktmd->options); + + int magnetIndex = getMagnetIndex((MagnetDownloader *)md); + if (magnetIndex >= 0) + removeMagnets(magnetIndex, 1); +} + +void MagnetManager::onSlotTimeout(int magnetIdx) +{ + if (magnetIdx >= usedDownloadingSlots.size()) + return; + + freeDownloadSlot(magnetIdx); + MagnetDownloader *md = magnetQueue.at(magnetIdx); + md->stop(); + magnetQueue.removeAt(magnetIdx); + magnetQueue.push_back(md); + + int updateIndex = startNextQueuedMagnets(); + if (updateIndex < 0) + updateIndex = magnetIdx; + + Q_EMIT updateQueue(updateIndex, magnetQueue.size() - updateIndex); +} + +int MagnetManager::startNextQueuedMagnets() +{ + if (magnetQueue.empty() || freeDownloadingSlots.isEmpty()) + return -1; + + int firstStartedIdx = usedDownloadingSlots.size(); + int queued = magnetQueue.size() - firstStartedIdx; + int magnetsToStart = std::min(freeDownloadingSlots.size(), queued); + + int nextIdx = firstStartedIdx; + while (magnetsToStart > 0) { + DownloadSlot *slot = freeDownloadingSlots.front(); + freeDownloadingSlots.pop_front(); + slot->setMagnetIndex(nextIdx); + usedDownloadingSlots.push_back(slot); + + magnetQueue.at(nextIdx)->start(); + if (useSlotTimer) + slot->startTimer(); + + --magnetsToStart; + ++nextIdx; + } + + return firstStartedIdx; +} + +void MagnetManager::freeDownloadSlot(bt::Uint32 magnetIdx) +{ + Uint32 usedDownloadingSlotsSize = usedDownloadingSlots.size(); + if (magnetIdx >= usedDownloadingSlotsSize) + return; + + // free the slot used by magnetIdx + DownloadSlot *slot = usedDownloadingSlots.at(magnetIdx); + usedDownloadingSlots.removeAt(magnetIdx); + slot->reset(); + freeDownloadingSlots.push_front(slot); + + // sync magnet indices of next slots + --usedDownloadingSlotsSize; + for (Uint32 i = magnetIdx; i < usedDownloadingSlotsSize; ++i) + usedDownloadingSlots.at(i)->setMagnetIndex(i); +} + +int MagnetManager::getMagnetIndex(kt::MagnetDownloader *md) +{ + if (stoppedHashes.contains(md->magnetLink().infoHash())) { + int magnetIndex = magnetQueue.size() + stoppedList.indexOf(md); + if (magnetIndex >= magnetQueue.size()) // md is inside of magnetStopList + return magnetIndex; + } else + return magnetQueue.indexOf(md); + + return -1; +} + +} diff --git a/libktcore/torrent/magnetmanager.h b/libktcore/torrent/magnetmanager.h new file mode 100644 index 0000000..e8df147 --- /dev/null +++ b/libktcore/torrent/magnetmanager.h @@ -0,0 +1,174 @@ +/* + SPDX-FileCopyrightText: 2014 Juan Palacios + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef MAGNETMANAGER_H +#define MAGNETMANAGER_H + +#include +#include +#include + +namespace kt +{ +/// Adds options struct to bt::MagnetDownloader +class MagnetDownloader : public bt::MagnetDownloader +{ + Q_OBJECT +public: + MagnetDownloader(const bt::MagnetLink &mlink, const MagnetLinkLoadOptions &options, QObject *parent) + : bt::MagnetDownloader(mlink, parent) + , options(options) + { + } + ~MagnetDownloader() override + { + } + + MagnetLinkLoadOptions options; +}; + +/// This class represent a downloading slot. +/// A downloading slot has the index of the magnet that occupy it and a timer that +/// controls the maximum time that one magnet can occupy the downloading slot. +class DownloadSlot : public QObject +{ + Q_OBJECT +public: + DownloadSlot(QObject *parent = nullptr); + ~DownloadSlot() override; + + void setTimerDuration(unsigned int duration); + void startTimer(); + void stopTimer(); + void reset(); + void setMagnetIndex(int index); + int getMagnetIndex() const; + bool isTimerActived() const; + bool isOccupied() const; + +Q_SIGNALS: + void timeout(int magnetIdx); + +private Q_SLOTS: + void onTimeout(); + +private: + int magnetIdx; + unsigned int timerDuration; + QTimer *timer; +}; + +/// This class manage the downloading of magnets. +/// For this task uses a queue with a determined number of concurrent downloads. +/// All downloading magnets uses a timer that determines the maximum time +/// that each magnet can be in the downloading state. If the magnet is not downloaded +/// within this time, that magnet will be pushed back at the end of the queued list, +/// just above the stopped magnets list. +/// The stopped magnet links always will occupy the latests positions of the queue. +class KTCORE_EXPORT MagnetManager : public QObject +{ + Q_OBJECT +public: + MagnetManager(QObject *parent = nullptr); + ~MagnetManager() override; + + /// Adds a magnet link to the queue + /// @param mlink magnet link to be added + /// @param options magnet link options + /// @param stopped whether this magnet should be added to the queue stopped + void addMagnet(const bt::MagnetLink &mlink, const MagnetLinkLoadOptions &options, bool stopped); + + /// Removes count successive magnets, starting on idx + void removeMagnets(bt::Uint32 idx, bt::Uint32 count); + + /// Starts count successive magnets, starting on idx + void start(bt::Uint32 idx, bt::Uint32 count); + + /// Stops count successive magnets, starting on idx + void stop(bt::Uint32 idx, bt::Uint32 count); + + /// Returns whether the magnet corresponding with idx is stopped + bool isStopped(bt::Uint32 idx) const; + + /// Set the number of concurrent downloading magnets + void setDownloadingSlots(bt::Uint32 count); + + /// Sets if the slot timer must be used + void setUseSlotTimer(bool value); + + /// Set the maximum time that a magnet link is in downloading state + /// @param duration time in minutes + void setTimerDuration(bt::Uint32 duration); + + /// Updates the downloading magnets + void update(); + + /// Load all magnets from a file + void loadMagnets(const QString &file); + + /// Save all magnets to a file + void saveMagnets(const QString &file); + + /// Defines the magnet state on the MagnetManager + enum MagnetState { + DOWNLOADING, ///< Started and downloading + QUEUED, ///< Started and waiting for download + STOPPED, ///< Stopped + }; + + /// Return the state of the magnet corresponding to idx + MagnetState status(bt::Uint32 idx) const; + + /// Return the number of managed magnets + int count() const; + + /// Get the magnet downloader at index idx in the list + /// @param idx index of the magnet + /// @return the magnet downloader or nullptr if idx is out of bounds + const kt::MagnetDownloader *getMagnetDownloader(bt::Uint32 idx) const; + +Q_SIGNALS: + /// Emitted when metadata has been downloaded for a MagnetLink. + void metadataDownloaded(const bt::MagnetLink &mlink, const QByteArray &data, const kt::MagnetLinkLoadOptions &options); + + /// Emitted when the queue has been altered in some form, so the + /// magnets order and/or number could be altered. + /// The range of the altered magnets is determined by idx and count. + /// @param idx determines the index of the first altered magnet + /// @param count determines the number of magnets that must be updated + void updateQueue(bt::Uint32 idx, bt::Uint32 count); + +private Q_SLOTS: + void onDownloadFinished(bt::MagnetDownloader *md, const QByteArray &data); + void onSlotTimeout(int magnetIdx); + +private: + /// Start the next queued magnets and return the index of the first started magnet + int startNextQueuedMagnets(); + + /// Free the download slot that is occupying the magnet with magnetIdx, updating + /// the indices of the magnets in all magnets slots to keep it in sync with the magnet + /// queue indices. + void freeDownloadSlot(bt::Uint32 magnetIdx); + + /// Return the magnet index in the queue + the stopped list + int getMagnetIndex(kt::MagnetDownloader *md); + + /// Writes the encoder info of one magnet + void writeEncoderInfo(bt::BEncoder &enc, kt::MagnetDownloader *md); + + bool useSlotTimer; + int timerDuration; + QList usedDownloadingSlots; + QList freeDownloadingSlots; + QList magnetQueue; + QList stoppedList; + QSet magnetHashes; + QSet stoppedHashes; +}; + +} + +#endif // MAGNETMANAGER_H diff --git a/libktcore/torrent/queuemanager.cpp b/libktcore/torrent/queuemanager.cpp new file mode 100644 index 0000000..1766ca3 --- /dev/null +++ b/libktcore/torrent/queuemanager.cpp @@ -0,0 +1,868 @@ +/* + SPDX-FileCopyrightText: 2005 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "queuemanager.h" + +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace bt; + +namespace kt +{ +QueueManager::QueueManager() + : QObject() +{ + max_downloads = 0; + max_seeds = 0; // for testing. Needs to be added to Settings:: + + keep_seeding = true; // test. Will be passed from Core + suspended_state = false; + exiting = false; + ordering = false; + + last_stats_sync_permitted = 0; + + QNetworkConfigurationManager *networkConfigurationManager = new QNetworkConfigurationManager(this); + connect(networkConfigurationManager, &QNetworkConfigurationManager::onlineStateChanged, this, &QueueManager::onOnlineStateChanged); +} + +QueueManager::~QueueManager() +{ + qDeleteAll(downloads); +} + +void QueueManager::append(bt::TorrentInterface *tc) +{ + downloads.append(tc); + connect(tc, &TorrentInterface::diskSpaceLow, this, &QueueManager::onLowDiskSpace); + connect(tc, &TorrentInterface::torrentStopped, this, &QueueManager::torrentStopped); + connect(tc, &TorrentInterface::updateQueue, this, &QueueManager::orderQueue); +} + +void QueueManager::remove(bt::TorrentInterface *tc) +{ + suspended_torrents.erase(tc); + int index = downloads.indexOf(tc); + if (index != -1) + downloads.takeAt(index)->deleteLater(); +} + +void QueueManager::clear() +{ + exiting = true; + suspended_torrents.clear(); + qDeleteAll(downloads); + downloads.clear(); +} + +TorrentStartResponse QueueManager::startInternal(bt::TorrentInterface *tc) +{ + const TorrentStats &s = tc->getStats(); + + if (!s.completed && !checkDiskSpace(tc, false)) + return bt::NOT_ENOUGH_DISKSPACE; + else if (s.completed && !checkLimits(tc, false)) + return bt::MAX_SHARE_RATIO_REACHED; + + Out(SYS_GEN | LOG_NOTICE) << "Starting download " << s.torrent_name << endl; + startSafely(tc); + return START_OK; +} + +bool QueueManager::checkLimits(TorrentInterface *tc, bool interactive) +{ + QString msg; + const TorrentStats &s = tc->getStats(); + bool max_ratio_reached = tc->overMaxRatio(); + bool max_seed_time_reached = tc->overMaxSeedTime(); + + if (max_ratio_reached && max_seed_time_reached) + msg = i18n("The torrent \"%1\" has reached its maximum share ratio and its maximum seed time. Ignore the limit and start seeding anyway?", + s.torrent_name); + else if (max_ratio_reached && !max_seed_time_reached) + msg = i18n("The torrent \"%1\" has reached its maximum share ratio. Ignore the limit and start seeding anyway?", s.torrent_name); + else if (max_seed_time_reached && !max_ratio_reached) + msg = i18n("The torrent \"%1\" has reached its maximum seed time. Ignore the limit and start seeding anyway?", s.torrent_name); + else + return true; + + if (interactive && KMessageBox::questionYesNo(nullptr, msg, i18n("Limits reached.")) == KMessageBox::Yes) { + if (max_ratio_reached) + tc->setMaxShareRatio(0.00f); + if (max_seed_time_reached) + tc->setMaxSeedTime(0.0f); + return true; + } + + return false; +} + +bool QueueManager::checkDiskSpace(TorrentInterface *tc, bool interactive) +{ + if (tc->checkDiskSpace(false)) + return true; + + // we're short! + switch (Settings::startDownloadsOnLowDiskSpace()) { + case 0: // don't start! + return false; + case 1: { // ask user + const TorrentStats &s = tc->getStats(); + QString msg = i18n( + "You don't have enough disk space to download this torrent. " + "Are you sure you want to continue?"); + + QString caption = i18n("Insufficient disk space for %1", s.torrent_name); + if (!interactive || KMessageBox::questionYesNo(nullptr, msg, caption) == KMessageBox::No) + return false; + else + break; + } + case 2: // force start + break; + } + + return true; +} + +TorrentStartResponse QueueManager::start(bt::TorrentInterface *tc) +{ + if (tc->getJobQueue()->runningJobs()) { + tc->setAllowedToStart(true); + return BUSY_WITH_JOB; + } + + const TorrentStats &s = tc->getStats(); + if (!s.completed && !checkDiskSpace(tc, true)) { + return bt::NOT_ENOUGH_DISKSPACE; + } else if (s.completed && !checkLimits(tc, true)) { + return bt::MAX_SHARE_RATIO_REACHED; + } + + if (!enabled()) { + return startInternal(tc); + } else { + tc->setAllowedToStart(true); + orderQueue(); + return START_OK; + } +} + +void QueueManager::stop(bt::TorrentInterface *tc) +{ + if (tc->getJobQueue()->runningJobs()) + return; + + const TorrentStats &s = tc->getStats(); + if (enabled()) + tc->setAllowedToStart(false); + + if (s.running) + stopSafely(tc); + else + tc->setQueued(false); +} + +void QueueManager::stop(QList &todo) +{ + ordering = true; + for (bt::TorrentInterface *tc : qAsConst(todo)) { + stop(tc); + } + ordering = false; + if (enabled()) + orderQueue(); +} + +void QueueManager::checkDiskSpace(QList &todo) +{ + // first see if we need to ask the user to start torrents when diskspace is low + if (Settings::startDownloadsOnLowDiskSpace() == 2) { + QStringList names; + QList tmp; + for (bt::TorrentInterface *tc : qAsConst(todo)) { + const TorrentStats &s = tc->getStats(); + if (!s.completed && !tc->checkDiskSpace(false)) { + names.append(s.torrent_name); + tmp.append(tc); + } + } + + if (tmp.count() > 0) { + if (KMessageBox::questionYesNoList(nullptr, i18n("Not enough disk space for the following torrents. Do you want to start them anyway?"), names) + == KMessageBox::No) { + for (bt::TorrentInterface *tc : qAsConst(tmp)) + todo.removeAll(tc); + } + } + } + // if the policy is to not start, remove torrents from todo list if diskspace is low + else if (Settings::startDownloadsOnLowDiskSpace() == 0) { + QList::iterator i = todo.begin(); + while (i != todo.end()) { + bt::TorrentInterface *tc = *i; + const TorrentStats &s = tc->getStats(); + if (!s.completed && !tc->checkDiskSpace(false)) + i = todo.erase(i); + else + i++; + } + } +} + +void QueueManager::checkMaxSeedTime(QList &todo) +{ + QStringList names; + QList tmp; + for (bt::TorrentInterface *tc : qAsConst(todo)) { + const TorrentStats &s = tc->getStats(); + if (s.completed && tc->overMaxSeedTime()) { + names.append(s.torrent_name); + tmp.append(tc); + } + } + + if (tmp.count() > 0) { + if (KMessageBox::questionYesNoList(nullptr, + i18n("The following torrents have reached their maximum seed time. Do you want to start them anyway?"), + names) + == KMessageBox::No) { + for (bt::TorrentInterface *tc : qAsConst(tmp)) + todo.removeAll(tc); + } else { + for (bt::TorrentInterface *tc : qAsConst(tmp)) + tc->setMaxSeedTime(0.0f); + } + } +} + +void QueueManager::checkMaxRatio(QList &todo) +{ + QStringList names; + QList tmp; + for (bt::TorrentInterface *tc : qAsConst(todo)) { + const TorrentStats &s = tc->getStats(); + if (s.completed && tc->overMaxRatio()) { + names.append(s.torrent_name); + tmp.append(tc); + } + } + + if (tmp.count() > 0) { + if (KMessageBox::questionYesNoList(nullptr, + i18n("The following torrents have reached their maximum share ratio. Do you want to start them anyway?"), + names) + == KMessageBox::No) { + for (bt::TorrentInterface *tc : qAsConst(tmp)) + todo.removeAll(tc); + } else { + for (bt::TorrentInterface *tc : qAsConst(tmp)) + tc->setMaxShareRatio(0.0f); + } + } +} + +void QueueManager::start(QList &todo) +{ + if (todo.count() == 0) + return; + + // check diskspace stuff + checkDiskSpace(todo); + if (todo.count() == 0) + return; + + checkMaxSeedTime(todo); + if (todo.count() == 0) + return; + + checkMaxRatio(todo); + if (todo.count() == 0) + return; + + // start what is left + for (bt::TorrentInterface *tc : qAsConst(todo)) { + const TorrentStats &s = tc->getStats(); + if (s.running) + continue; + + if (tc->getJobQueue()->runningJobs()) + continue; + + if (enabled()) + tc->setAllowedToStart(true); + else + startSafely(tc); + } + + if (enabled()) + orderQueue(); +} + +void QueueManager::startAll() +{ + if (enabled()) { + for (bt::TorrentInterface *tc : qAsConst(downloads)) + tc->setAllowedToStart(true); + + orderQueue(); + } else { + // first get the list of torrents which need to be started + QList todo; + for (bt::TorrentInterface *tc : qAsConst(downloads)) { + const TorrentStats &s = tc->getStats(); + if (s.running) + continue; + + if (tc->getJobQueue()->runningJobs()) + continue; + + todo.append(tc); + } + + start(todo); + } +} + +void QueueManager::stopAll() +{ + stop(downloads); +} + +void QueueManager::startAutoStartTorrents() +{ + if (enabled() || suspended_state) + return; + + // first get the list of torrents which need to be started + QList todo; + for (bt::TorrentInterface *tc : qAsConst(downloads)) { + const TorrentStats &s = tc->getStats(); + if (s.running || tc->getJobQueue()->runningJobs() || !s.autostart) + continue; + + todo.append(tc); + } + + start(todo); +} + +void QueueManager::onExit(WaitJob *wjob) +{ + exiting = true; + QList::iterator i = downloads.begin(); + while (i != downloads.end()) { + bt::TorrentInterface *tc = *i; + if (tc->getStats().running) { + stopSafely(tc, wjob); + } + i++; + } +} + +void QueueManager::startNext() +{ + orderQueue(); +} + +int QueueManager::countDownloads() +{ + return getNumRunning(DOWNLOADS); +} + +int QueueManager::countSeeds() +{ + return getNumRunning(SEEDS); +} + +int QueueManager::getNumRunning(Flags flags) +{ + int nr = 0; + QList::const_iterator i = downloads.constBegin(); + while (i != downloads.constEnd()) { + const TorrentInterface *tc = *i; + const TorrentStats &s = tc->getStats(); + + if (s.running) { + if (flags == ALL || (flags == DOWNLOADS && !s.completed) || (flags == SEEDS && s.completed)) + nr++; + } + i++; + } + return nr; +} + +const bt::TorrentInterface *QueueManager::getTorrent(Uint32 idx) const +{ + if (idx >= (Uint32)downloads.count()) + return nullptr; + else + return downloads[idx]; +} + +bt::TorrentInterface *QueueManager::getTorrent(bt::Uint32 idx) +{ + if (idx >= (Uint32)downloads.count()) + return nullptr; + else + return downloads[idx]; +} + +QList::iterator QueueManager::begin() +{ + return downloads.begin(); +} + +QList::iterator QueueManager::end() +{ + return downloads.end(); +} + +QList::const_iterator QueueManager::begin() const +{ + return downloads.cbegin(); +} + +QList::const_iterator QueueManager::end() const +{ + return downloads.cend(); +} + +void QueueManager::setMaxDownloads(int m) +{ + max_downloads = m; +} + +void QueueManager::onLowDiskSpace(bt::TorrentInterface *tc, bool toStop) +{ + if (toStop) { + stopSafely(tc); + if (enabled()) { + tc->setAllowedToStart(false); + orderQueue(); + } + } + + // then emit the signal to inform trayicon to show passive popup + Q_EMIT lowDiskSpace(tc, toStop); +} + +void QueueManager::setMaxSeeds(int m) +{ + max_seeds = m; +} + +void QueueManager::setKeepSeeding(bool ks) +{ + keep_seeding = ks; +} + +bool QueueManager::alreadyLoaded(const bt::SHA1Hash &ih) const +{ + for (const bt::TorrentInterface *tor : qAsConst(downloads)) { + if (tor->getInfoHash() == ih) + return true; + } + return false; +} + +void QueueManager::mergeAnnounceList(const bt::SHA1Hash &ih, const TrackerTier *trk) +{ + for (bt::TorrentInterface *tor : qAsConst(downloads)) { + if (tor->getInfoHash() == ih) { + TrackersList *ta = tor->getTrackersList(); + const int cnt = ta->getTrackers().count(); + ta->merge(trk); + if (cnt < ta->getTrackers().count()) { + // new trackers were added + // do "Manual Announce" for this torrent + if (tor->getStats().running) { + tor->updateTracker(); + } + } + return; + } + } +} + +bool QueueManager::permitStatsSync(TorrentControl *tc) +{ + // we want to assure that minimum time interval delay is happen + // before next TorrentControl dumps its State to the file + + // if you have more than 500 running torrents it's feasible to + // increase the period to avoid too small interval value + const TimeStamp max_period = (50 * 60 * 1000) * (downloads.size() / 500 + 1); + + if (tc->getStatsSyncElapsedTime() >= max_period) { + const bt::TimeStamp now = Now(); + const bt::TimeStamp interval = max_period / downloads.size(); + if (now - last_stats_sync_permitted > interval) { + last_stats_sync_permitted = now; + return true; + } + } + return false; +} + +void QueueManager::orderQueue() +{ + if (ordering || !downloads.count() || exiting) + return; + + Q_EMIT orderingQueue(); + + downloads.sort(); // sort downloads, even when suspended so that the QM widget is updated + if (Settings::manuallyControlTorrents() || suspended_state) { + Q_EMIT queueOrdered(); + return; + } + + RecursiveEntryGuard guard(&ordering); // make sure that recursive entering of this function is not possible + + QueuePtrList download_queue; + QueuePtrList seed_queue; + + for (TorrentInterface *tc : qAsConst(downloads)) { + const TorrentStats &s = tc->getStats(); + if (s.running || (tc->isAllowedToStart() && !s.stopped_by_error && !tc->getJobQueue()->runningJobs())) { + if (s.completed) { + if (s.running || (!tc->overMaxRatio() && !tc->overMaxSeedTime())) + seed_queue.append(tc); + } else + download_queue.append(tc); + } + } + + int num_running = 0; + for (bt::TorrentInterface *tc : qAsConst(download_queue)) { + const TorrentStats &s = tc->getStats(); + + if (num_running < max_downloads || max_downloads == 0) { + if (!s.running) { + Out(SYS_GEN | LOG_DEBUG) << "QM Starting: " << s.torrent_name << endl; + if (startInternal(tc) == bt::START_OK) + num_running++; + } else + num_running++; + } else { + if (s.running) { + Out(SYS_GEN | LOG_DEBUG) << "QM Stopping: " << s.torrent_name << endl; + stopSafely(tc); + } + tc->setQueued(true); + } + } + + num_running = 0; + for (bt::TorrentInterface *tc : qAsConst(seed_queue)) { + const TorrentStats &s = tc->getStats(); + if (num_running < max_seeds || max_seeds == 0) { + if (!s.running) { + Out(SYS_GEN | LOG_DEBUG) << "QM Starting: " << s.torrent_name << endl; + if (startInternal(tc) == bt::START_OK) + num_running++; + } else + num_running++; + } else { + if (s.running) { + Out(SYS_GEN | LOG_DEBUG) << "QM Stopping: " << s.torrent_name << endl; + stopSafely(tc); + } + tc->setQueued(true); + } + } + + Q_EMIT queueOrdered(); +} + +void QueueManager::torrentFinished(bt::TorrentInterface *tc) +{ + if (!keep_seeding) { + if (enabled()) + tc->setAllowedToStart(false); + + stopSafely(tc); + } + + orderQueue(); +} + +void QueueManager::torrentAdded(bt::TorrentInterface *tc, bool start_torrent) +{ + if (enabled()) { + // new torrents have the lowest priority + // so everybody else gets a higher priority + for (TorrentInterface *otc : qAsConst(downloads)) { + int p = otc->getPriority(); + otc->setPriority(p + 1); + } + tc->setAllowedToStart(start_torrent); + tc->setPriority(0); + rearrangeQueue(); + orderQueue(); + } else { + if (start_torrent) + start(tc); + } +} + +void QueueManager::torrentRemoved(bt::TorrentInterface *tc) +{ + remove(tc); + rearrangeQueue(); + orderQueue(); +} + +void QueueManager::torrentsRemoved(QList &tors) +{ + for (bt::TorrentInterface *tc : qAsConst(tors)) + remove(tc); + rearrangeQueue(); + orderQueue(); +} + +void QueueManager::setSuspendedState(bool suspend) +{ + if (suspended_state == suspend) + return; + + suspended_state = suspend; + if (!suspend) { + UpdateCurrentTime(); + std::set::iterator it = suspended_torrents.begin(); + while (it != suspended_torrents.end()) { + TorrentInterface *tc = *it; + startSafely(tc); + it++; + } + + suspended_torrents.clear(); + orderQueue(); + } else { + for (TorrentInterface *tc : qAsConst(downloads)) { + const TorrentStats &s = tc->getStats(); + if (s.running) { + suspended_torrents.insert(tc); + stopSafely(tc); + } + } + } + Q_EMIT suspendStateChanged(suspended_state); +} + +void QueueManager::rearrangeQueue() +{ + downloads.sort(); + reindexQueue(); +} + +void QueueManager::startSafely(bt::TorrentInterface *tc) +{ + try { + tc->start(); + } catch (bt::Error &err) { + const TorrentStats &s = tc->getStats(); + QString msg = i18n("Error starting torrent %1: %2", s.torrent_name, err.toString()); + KMessageBox::error(nullptr, msg, i18n("Error")); + } +} + +void QueueManager::stopSafely(bt::TorrentInterface *tc, WaitJob *wjob) +{ + try { + tc->stop(wjob); + } catch (bt::Error &err) { + const TorrentStats &s = tc->getStats(); + QString msg = i18n("Error stopping torrent %1: %2", s.torrent_name, err.toString()); + KMessageBox::error(nullptr, msg, i18n("Error")); + } +} + +void QueueManager::torrentStopped(bt::TorrentInterface *) +{ + orderQueue(); +} + +static bool IsStalled(bt::TorrentInterface *tc, bt::TimeStamp now, bt::Uint32 min_stall_time) +{ + bt::Int64 stalled_time = 0; + if (tc->getStats().completed) + stalled_time = (now - tc->getStats().last_upload_activity_time) / 1000; + else + stalled_time = (now - tc->getStats().last_download_activity_time) / 1000; + + return stalled_time > min_stall_time * 60 && tc->getStats().running; +} + +void QueueManager::checkStalledTorrents(bt::TimeStamp now, bt::Uint32 min_stall_time) +{ + if (!enabled()) + return; + + QueuePtrList newlist; + QueuePtrList stalled; + bool can_decrease = false; + + // find all stalled ones + for (bt::TorrentInterface *tc : qAsConst(downloads)) { + if (IsStalled(tc, now, min_stall_time)) { + stalled.append(tc); + } else { + // decreasing makes only sense if there are QM torrents after the stalled ones + can_decrease = stalled.count() > 0; + newlist.append(tc); + } + } + + if (stalled.count() == 0 || stalled.count() == downloads.count() || !can_decrease) + return; + + for (bt::TorrentInterface *tc : qAsConst(stalled)) + Out(SYS_GEN | LOG_NOTICE) << "The torrent " << tc->getStats().torrent_name << " has stalled longer than " << min_stall_time + << " minutes, decreasing its priority" << endl; + + downloads.clear(); + downloads += newlist; + downloads += stalled; + // redo priorities and then order the queue + int prio = downloads.count(); + for (bt::TorrentInterface *tc : qAsConst(downloads)) { + tc->setPriority(prio--); + } + orderQueue(); +} + +void QueueManager::onOnlineStateChanged(bool isOnline) +{ + if (isOnline) { + Out(SYS_GEN | LOG_IMPORTANT) << "Network is up" << endl; + // if the network has gone down, longer then 2 minutes + // all the connections are probably stale, so tell all + // running torrents, that they need to reannounce and kill stale peers + if (network_down_time.isValid() && network_down_time.secsTo(QDateTime::currentDateTime()) > 120) { + for (bt::TorrentInterface *tc : qAsConst(downloads)) { + if (tc->getStats().running) + tc->networkUp(); + } + } + + network_down_time = QDateTime(); + } else { + Out(SYS_GEN | LOG_IMPORTANT) << "Network is down" << endl; + network_down_time = QDateTime::currentDateTime(); + } +} + +void QueueManager::reindexQueue() +{ + int prio = downloads.count(); + // make sure everybody has an unique priority + for (bt::TorrentInterface *tc : qAsConst(downloads)) { + tc->setPriority(prio--); + } +} + +void QueueManager::loadState(KSharedConfigPtr cfg) +{ + KConfigGroup g = cfg->group("QueueManager"); + suspended_state = g.readEntry("suspended", false); + + if (suspended_state) { + QStringList info_hash_list = g.readEntry("suspended_torrents", QStringList()); + for (bt::TorrentInterface *t : qAsConst(downloads)) { + if (info_hash_list.contains(t->getInfoHash().toString())) + suspended_torrents.insert(t); + } + } +} + +void QueueManager::saveState(KSharedConfigPtr cfg) +{ + KConfigGroup g = cfg->group("QueueManager"); + g.writeEntry("suspended", suspended_state); + + if (suspended_state) { + QStringList info_hash_list; + for (bt::TorrentInterface *t : qAsConst(suspended_torrents)) { + info_hash_list << t->getInfoHash().toString(); + } + g.writeEntry("suspended_torrents", info_hash_list); + } +} + +bool QueueManager::checkFileConflicts(TorrentInterface *tc, QStringList &conflicting) const +{ + conflicting.clear(); + + // First get a set off all files of tc + QSet files; + if (tc->getStats().multi_file_torrent) { + for (bt::Uint32 i = 0; i < tc->getNumFiles(); i++) + files.insert(tc->getTorrentFile(i).getPathOnDisk()); + } else + files.insert(tc->getStats().output_path); + + for (bt::TorrentInterface *t : qAsConst(downloads)) { + if (t == tc) + continue; + + if (t->getStats().multi_file_torrent) { + for (bt::Uint32 i = 0; i < t->getNumFiles(); i++) { + if (files.contains(t->getTorrentFile(i).getPathOnDisk())) { + conflicting.append(t->getDisplayName()); + break; + } + } + } else { + if (files.contains(t->getStats().output_path)) + conflicting.append(t->getDisplayName()); + } + } + + return !conflicting.isEmpty(); +} + +///////////////////////////////////////////////////////////////////////////////////////////// + +QueuePtrList::QueuePtrList() + : QList() +{ +} + +QueuePtrList::~QueuePtrList() +{ +} + +void QueuePtrList::sort() +{ + std::sort(begin(), end(), QueuePtrList::biggerThan); +} + +bool QueuePtrList::biggerThan(bt::TorrentInterface *tc1, bt::TorrentInterface *tc2) +{ + return tc1->getPriority() > tc2->getPriority(); +} + +} diff --git a/libktcore/torrent/queuemanager.h b/libktcore/torrent/queuemanager.h new file mode 100644 index 0000000..8e72f65 --- /dev/null +++ b/libktcore/torrent/queuemanager.h @@ -0,0 +1,307 @@ +/* + SPDX-FileCopyrightText: 2005 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KTQUEUEMANAGER_H +#define KTQUEUEMANAGER_H + +#include + +#include +#include + +#include +#include +#include + +namespace bt +{ +class SHA1Hash; +struct TrackerTier; +class WaitJob; +} + +namespace kt +{ +class KTCORE_EXPORT QueuePtrList : public QList +{ +public: + QueuePtrList(); + ~QueuePtrList(); + + /** + * Sort based upon priority + */ + void sort(); + +protected: + static bool biggerThan(bt::TorrentInterface *tc1, bt::TorrentInterface *tc2); +}; + +/** + * @author Ivan Vasic + * @brief This class contains list of all TorrentControls and is responsible for starting/stopping them + */ +class KTCORE_EXPORT QueueManager : public QObject, public bt::QueueManagerInterface +{ + Q_OBJECT + +public: + QueueManager(); + ~QueueManager() override; + + void append(bt::TorrentInterface *tc); + void remove(bt::TorrentInterface *tc); + void clear(); + + /** + Save the state of the QueueManager + @param cfg The config + */ + void saveState(KSharedConfigPtr cfg); + + /** + Load the state of the QueueManager + @param cfg The config + */ + void loadState(KSharedConfigPtr cfg); + + /** + * Check if we need to decrease the priority of stalled torrents + * @param min_stall_time Stall time in minutes + * @param now The current time + */ + void checkStalledTorrents(bt::TimeStamp now, bt::Uint32 min_stall_time); + + /** + * Start a torrent + * @param tc The torrent + * @return What happened + */ + bt::TorrentStartResponse start(bt::TorrentInterface *tc); + + /** + * Stop a torrent + * @param tc The torrent + */ + void stop(bt::TorrentInterface *tc); + + /** + * Start a list of torrents. + * @param todo The list of torrents + */ + void start(QList &todo); + + /** + * Stop a list of torrents + * @param todo The list of torrents + */ + void stop(QList &todo); + + /// Stop all torrents + void stopAll(); + + /// Start all torrents + void startAll(); + + /** + * Stop all running torrents + * @param wjob WaitJob which waits for stopped events to reach the tracker + */ + void onExit(bt::WaitJob *wjob); + + /// Get the number of torrents + int count() + { + return downloads.count(); + } + + /// Get the number of downloads + int countDownloads(); + + /// Get the number of seeds + int countSeeds(); + + enum Flags { + SEEDS = 1, + DOWNLOADS = 2, + ALL = 3, + }; + + /** + * Get the number of running torrents + * @param flags Which torrents to choose + */ + int getNumRunning(Flags flags = ALL); + + /** + * Start the next torrent. + */ + void startNext(); + + /** + If the QM is disabled this function needs to be called to start + all the torrents which were running at the time of the previous exit. + */ + void startAutoStartTorrents(); + + typedef QList::iterator iterator; + typedef QList::const_iterator const_iterator; + + iterator begin(); + iterator end(); + + const_iterator begin() const; + const_iterator end() const; + + /** + * Get the torrent at index idx in the list. + * @param idx Index of the torrent + * @return The torrent or 0 if the index is out of bounds + */ + const bt::TorrentInterface *getTorrent(bt::Uint32 idx) const; + + /** + * Get the torrent at index idx in the list. + * @param idx Index of the torrent + * @return The torrent or 0 if the index is out of bounds + */ + bt::TorrentInterface *getTorrent(bt::Uint32 idx); + + /** + * See if we already loaded a torrent. + * @param ih The info hash of a torrent + * @return true if we do, false if we don't + */ + bool alreadyLoaded(const bt::SHA1Hash &ih) const override; + + /** + * Merge announce lists to a torrent + * @param ih The info_hash of the torrent to merge to + * @param trk First tier of trackers + */ + void mergeAnnounceList(const bt::SHA1Hash &ih, const bt::TrackerTier *trk) override; + + /** + * Requested by each TorrentControl during its update to + * get permission on saving Stats file to disk. May be + * overriden to balance I/O operations. + * @param tc Pointer to TorrentControl instance + * @return true if file save is permitted, false otherwise + */ + + bool permitStatsSync(bt::TorrentControl *tc) override; + + /** + * Set the maximum number of downloads + * @param m Max downloads + */ + void setMaxDownloads(int m); + + /** + * Set the maximum number of seeds + * @param m Max seeds + */ + void setMaxSeeds(int m); + + /** + * Enable or disable keep seeding (after a torrent has finished) + * @param ks Keep seeding + */ + void setKeepSeeding(bool ks); + + /** + * Sets global suspended state for QueueManager and stopps all running torrents. + * No torrents will be automatically started/stopped with QM. + */ + void setSuspendedState(bool suspend); + + /// Get the suspended state + bool getSuspendedState() const + { + return suspended_state; + } + + /** + * Reindex the queue priorities. + */ + void reindexQueue(); + + /** + * Check if a torrent has file conflicts with other torrents. + * If conflicting are found, a list of names of the conflicting torrents is filled in. + * @param tc The torrent + * @param conflicting List of conflicting torrents + */ + bool checkFileConflicts(bt::TorrentInterface *tc, QStringList &conflicting) const; + + /** + * Places all torrents from downloads in the right order in queue. + * Use this when torrent priorities get changed + */ + void orderQueue(); + +Q_SIGNALS: + /** + * User tried to enqueue a torrent that has reached max share ratio. It's not possible. + * Signal should be connected to SysTray slot which shows appropriate KPassivePopup info. + * @param tc The torrent in question. + */ + void queuingNotPossible(bt::TorrentInterface *tc); + + /** + * Diskspace is running low. + * Signal should be connected to SysTray slot which shows appropriate KPassivePopup info. + * @param tc The torrent in question. + */ + void lowDiskSpace(bt::TorrentInterface *tc, bool stopped); + + /// Emitted before the queue is reordered + void orderingQueue(); + + /** + * Emitted when the QM has reordered it's queue + */ + void queueOrdered(); + + /** + * Emitted when the suspended state changes. + * @param suspended The suspended state + */ + void suspendStateChanged(bool suspended); + +public Q_SLOTS: + void torrentFinished(bt::TorrentInterface *tc); + void torrentAdded(bt::TorrentInterface *tc, bool start_torrent); + void torrentRemoved(bt::TorrentInterface *tc); + void torrentsRemoved(QList &tors); + void torrentStopped(bt::TorrentInterface *tc); + void onLowDiskSpace(bt::TorrentInterface *tc, bool toStop); + void onOnlineStateChanged(bool); + +private: + void startSafely(bt::TorrentInterface *tc); + void stopSafely(bt::TorrentInterface *tc, bt::WaitJob *wjob = nullptr); + void checkDiskSpace(QList &todo); + void checkMaxSeedTime(QList &todo); + void checkMaxRatio(QList &todo); + void rearrangeQueue(); + bt::TorrentStartResponse startInternal(bt::TorrentInterface *tc); + bool checkLimits(bt::TorrentInterface *tc, bool interactive); + bool checkDiskSpace(bt::TorrentInterface *tc, bool interactive); + +private: + QueuePtrList downloads; + std::set suspended_torrents; + int max_downloads; + int max_seeds; + bool suspended_state; + bool keep_seeding; + bool exiting; + bool ordering; + QDateTime network_down_time; + bt::TimeStamp last_stats_sync_permitted; +}; +} +#endif diff --git a/libktcore/torrent/torrentfilelistmodel.cpp b/libktcore/torrent/torrentfilelistmodel.cpp new file mode 100644 index 0000000..0c3f221 --- /dev/null +++ b/libktcore/torrent/torrentfilelistmodel.cpp @@ -0,0 +1,276 @@ +/* + SPDX-FileCopyrightText: 2007 Joris Guisson + SPDX-FileCopyrightText: 2007 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "torrentfilelistmodel.h" + +#include + +#include +#include +#include +#include + +#include +#include +#include + +using namespace bt; + +namespace kt +{ +TorrentFileListModel::TorrentFileListModel(bt::TorrentInterface *tc, DeselectMode mode, QObject *parent) + : TorrentFileModel(tc, mode, parent) +{ +} + +TorrentFileListModel::~TorrentFileListModel() +{ +} + +void TorrentFileListModel::changeTorrent(bt::TorrentInterface *tc) +{ + beginResetModel(); + this->tc = tc; + endResetModel(); +} + +int TorrentFileListModel::rowCount(const QModelIndex &parent) const +{ + if (tc && !parent.isValid()) + return tc->getStats().multi_file_torrent ? tc->getNumFiles() : 1; + else + return 0; +} + +int TorrentFileListModel::columnCount(const QModelIndex &parent) const +{ + if (!parent.isValid()) + return 2; + else + return 0; +} + +QVariant TorrentFileListModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if (role != Qt::DisplayRole || orientation != Qt::Horizontal) + return QVariant(); + + switch (section) { + case 0: + return i18n("File"); + case 1: + return i18n("Size"); + default: + return QVariant(); + } +} + +QVariant TorrentFileListModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid() || !tc) + return QVariant(); + + int r = index.row(); + int nfiles = rowCount(QModelIndex()); + bool multi = tc->getStats().multi_file_torrent; + if (r >= nfiles) + return QVariant(); + + const TorrentStats &s = tc->getStats(); + if (role == Qt::DisplayRole || role == Qt::EditRole) { + switch (index.column()) { + case 0: + if (multi) + return tc->getTorrentFile(r).getUserModifiedPath(); + else + return tc->getUserModifiedFileName(); + case 1: + if (multi) + return BytesToString(tc->getTorrentFile(r).getSize()); + else + return BytesToString(s.total_bytes); + default: + return QVariant(); + } + } else if (role == Qt::UserRole) { // sorting + switch (index.column()) { + case 0: + if (multi) + return tc->getTorrentFile(r).getUserModifiedPath(); + else + return tc->getUserModifiedFileName(); + case 1: + if (multi) + return tc->getTorrentFile(r).getSize(); + else + return s.total_bytes; + default: + return QVariant(); + } + } else if (role == Qt::DecorationRole && index.column() == 0) { + // if this is an empty folder then we are in the single file case + return QIcon::fromTheme(QMimeDatabase().mimeTypeForFile(multi ? tc->getTorrentFile(r).getPath() : s.torrent_name).iconName()); + } else if (role == Qt::CheckStateRole && index.column() == 0 && multi) { + const TorrentFileInterface &file = tc->getTorrentFile(r); + return file.doNotDownload() || file.getPriority() == ONLY_SEED_PRIORITY ? Qt::Unchecked : Qt::Checked; + } + + return QVariant(); +} + +QModelIndex TorrentFileListModel::parent(const QModelIndex &index) const +{ + Q_UNUSED(index); + return QModelIndex(); +} + +QModelIndex TorrentFileListModel::index(int row, int column, const QModelIndex &parent) const +{ + if (!tc || !hasIndex(row, column, parent)) + return QModelIndex(); + else { + bt::TorrentFileInterface *f = &tc->getTorrentFile(row); + return createIndex(row, column, f); + } +} + +bool TorrentFileListModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + if (!tc || !index.isValid()) + return false; + + if (role == Qt::CheckStateRole) { + Qt::CheckState newState = static_cast(value.toInt()); + bt::TorrentFileInterface &file = tc->getTorrentFile(index.row()); + if (newState == Qt::Checked) { + if (file.getPriority() == ONLY_SEED_PRIORITY) + file.setPriority(NORMAL_PRIORITY); + else + file.setDoNotDownload(false); + } else { + if (mode == KEEP_FILES) + file.setPriority(ONLY_SEED_PRIORITY); + else + file.setDoNotDownload(true); + } + dataChanged(createIndex(index.row(), 0), createIndex(index.row(), columnCount(index) - 1)); + checkStateChanged(); + return true; + } else if (role == Qt::EditRole) { + QString path = value.toString(); + if (path.isEmpty()) + return false; + + if (tc->getStats().multi_file_torrent) { + bt::TorrentFileInterface &file = tc->getTorrentFile(index.row()); + + // Check if we are not changing into somebody elses path + bt::Uint32 num_files = tc->getNumFiles(); + for (bt::Uint32 i = 0; i < num_files; i++) { + if ((int)i == index.row()) + continue; + + if (path == tc->getTorrentFile(i).getUserModifiedPath()) + return false; + } + + // keep track of modified paths + file.setUserModifiedPath(path); + } else { + // change the name of the file or toplevel directory + tc->setUserModifiedFileName(path); + } + dataChanged(createIndex(index.row(), 0), createIndex(index.row(), columnCount(index) - 1)); + return true; + } + + return false; +} + +void TorrentFileListModel::checkAll() +{ + if (tc && tc->getStats().multi_file_torrent) { + for (Uint32 i = 0; i < tc->getNumFiles(); i++) + setData(index(i, 0, QModelIndex()), Qt::Checked, Qt::CheckStateRole); + } +} + +void TorrentFileListModel::uncheckAll() +{ + if (tc && tc->getStats().multi_file_torrent) { + for (Uint32 i = 0; i < tc->getNumFiles(); i++) + setData(index(i, 0, QModelIndex()), Qt::Unchecked, Qt::CheckStateRole); + } +} + +void TorrentFileListModel::invertCheck() +{ + if (!tc || !tc->getStats().multi_file_torrent) + return; + + for (Uint32 i = 0; i < tc->getNumFiles(); i++) + invertCheck(index(i, 0, QModelIndex())); +} + +void TorrentFileListModel::invertCheck(const QModelIndex &idx) +{ + if (!tc) + return; + + if (tc->getTorrentFile(idx.row()).doNotDownload()) + setData(idx, Qt::Checked, Qt::CheckStateRole); + else + setData(idx, Qt::Unchecked, Qt::CheckStateRole); +} + +bt::Uint64 TorrentFileListModel::bytesToDownload() +{ + if (!tc) + return 0; + + if (tc->getStats().multi_file_torrent) { + bt::Uint64 ret = 0; + for (Uint32 i = 0; i < tc->getNumFiles(); i++) { + const bt::TorrentFileInterface &file = tc->getTorrentFile(i); + if (!file.doNotDownload()) + ret += file.getSize(); + } + return ret; + } else + return tc->getStats().total_bytes; +} + +bt::TorrentFileInterface *TorrentFileListModel::indexToFile(const QModelIndex &idx) +{ + if (!tc || !idx.isValid()) + return nullptr; + + int r = idx.row(); + if (r >= rowCount(QModelIndex())) + return nullptr; + else + return &tc->getTorrentFile(r); +} + +QString TorrentFileListModel::dirPath(const QModelIndex &idx) +{ + if (!tc || !idx.isValid()) + return QString(); + + int r = idx.row(); + if (r >= rowCount(QModelIndex())) + return QString(); + else + return tc->getTorrentFile(r).getPath(); +} + +void TorrentFileListModel::changePriority(const QModelIndexList &indexes, bt::Priority newpriority) +{ + for (const QModelIndex &idx : indexes) { + setData(idx, newpriority, Qt::UserRole); + } +} +} diff --git a/libktcore/torrent/torrentfilelistmodel.h b/libktcore/torrent/torrentfilelistmodel.h new file mode 100644 index 0000000..443a64a --- /dev/null +++ b/libktcore/torrent/torrentfilelistmodel.h @@ -0,0 +1,47 @@ +/* + SPDX-FileCopyrightText: 2007 Joris Guisson + SPDX-FileCopyrightText: 2007 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KTTORRENTFILELISTMODEL_H +#define KTTORRENTFILELISTMODEL_H + +#include "torrentfilemodel.h" + +namespace kt +{ +/** + * Model for displaying file trees of a torrent + * @author Joris Guisson + */ +class KTCORE_EXPORT TorrentFileListModel : public TorrentFileModel +{ + Q_OBJECT +public: + TorrentFileListModel(bt::TorrentInterface *tc, DeselectMode mode, QObject *parent); + ~TorrentFileListModel() override; + + void changeTorrent(bt::TorrentInterface *tc) override; + int rowCount(const QModelIndex &parent) const override; + int columnCount(const QModelIndex &parent) const override; + QVariant headerData(int section, Qt::Orientation orientation, int role) const override; + QVariant data(const QModelIndex &index, int role) const override; + QModelIndex parent(const QModelIndex &index) const override; + QModelIndex index(int row, int column, const QModelIndex &parent) const override; + bool setData(const QModelIndex &index, const QVariant &value, int role) override; + void checkAll() override; + void uncheckAll() override; + void invertCheck() override; + bt::Uint64 bytesToDownload() override; + bt::TorrentFileInterface *indexToFile(const QModelIndex &idx) override; + QString dirPath(const QModelIndex &idx) override; + void changePriority(const QModelIndexList &indexes, bt::Priority newpriority) override; + +private: + void invertCheck(const QModelIndex &idx); +}; + +} + +#endif diff --git a/libktcore/torrent/torrentfilemodel.cpp b/libktcore/torrent/torrentfilemodel.cpp new file mode 100644 index 0000000..7fa01d5 --- /dev/null +++ b/libktcore/torrent/torrentfilemodel.cpp @@ -0,0 +1,77 @@ +/* + SPDX-FileCopyrightText: 2007 Joris Guisson + SPDX-FileCopyrightText: 2007 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "torrentfilemodel.h" + +#include +#include + +namespace kt +{ +TorrentFileModel::TorrentFileModel(bt::TorrentInterface *tc, DeselectMode mode, QObject *parent) + : QAbstractItemModel(parent) + , tc(tc) + , mode(mode) + , file_names_editable(false) +{ +} + +TorrentFileModel::~TorrentFileModel() +{ +} + +QByteArray TorrentFileModel::saveExpandedState(QSortFilterProxyModel *, QTreeView *) +{ + return QByteArray(); +} + +void TorrentFileModel::loadExpandedState(QSortFilterProxyModel *, QTreeView *, const QByteArray &) +{ +} + +void TorrentFileModel::missingFilesMarkedDND() +{ + beginResetModel(); + endResetModel(); +} + +void TorrentFileModel::update() +{ +} + +void TorrentFileModel::onCodecChange() +{ + beginResetModel(); + endResetModel(); +} + +Qt::ItemFlags TorrentFileModel::flags(const QModelIndex &index) const +{ + if (!index.isValid()) + return {}; + + Qt::ItemFlags flags = Qt::ItemIsSelectable | Qt::ItemIsEnabled; + if (tc->getStats().multi_file_torrent) + flags |= Qt::ItemIsUserCheckable; + + if (fileNamesEditable() && index.column() == 0) + flags |= Qt::ItemIsEditable; + + return flags; +} + +void TorrentFileModel::filePercentageChanged(bt::TorrentFileInterface *file, float percentage) +{ + Q_UNUSED(file); + Q_UNUSED(percentage); +} + +void TorrentFileModel::filePreviewChanged(bt::TorrentFileInterface *file, bool preview) +{ + Q_UNUSED(file); + Q_UNUSED(preview); +} +} diff --git a/libktcore/torrent/torrentfilemodel.h b/libktcore/torrent/torrentfilemodel.h new file mode 100644 index 0000000..ebf3351 --- /dev/null +++ b/libktcore/torrent/torrentfilemodel.h @@ -0,0 +1,145 @@ +/* + SPDX-FileCopyrightText: 2007 Joris Guisson + SPDX-FileCopyrightText: 2007 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#ifndef KTTORRENTFILEMODEL_HH + +#define KTTORRENTFILEMODEL_HH + +#include +#include + +#include +#include + +class QTreeView; +class QSortFilterProxyModel; + +namespace bt +{ +class TorrentInterface; +class TorrentFileInterface; +} + +namespace kt +{ +class KTCORE_EXPORT TorrentFileModel : public QAbstractItemModel +{ + Q_OBJECT +public: + enum DeselectMode { + KEEP_FILES, + DELETE_FILES, + }; + TorrentFileModel(bt::TorrentInterface *tc, DeselectMode mode, QObject *parent); + ~TorrentFileModel() override; + + /** + * Change the current torrent + */ + virtual void changeTorrent(bt::TorrentInterface *tc) = 0; + + /** + * Check all the files in the torrent. + */ + virtual void checkAll() = 0; + + /** + * Uncheck all files in the torrent. + */ + virtual void uncheckAll() = 0; + + /** + * Invert the check of each file of the torrent + */ + virtual void invertCheck() = 0; + + /** + * Calculate the number of bytes to download + * @return Bytes to download + */ + virtual bt::Uint64 bytesToDownload() = 0; + + /** + * Save which items are expanded. + * @param pm Proxy model of the view + * @param tv The QTreeView + * @return The expanded state encoded in a byte array + */ + virtual QByteArray saveExpandedState(QSortFilterProxyModel *pm, QTreeView *tv); + + /** + * Retore the expanded state of the tree.in a QTreeView + * @param pm Proxy model of the view + * @param tv The QTreeView + * @param state The encoded expanded state + */ + virtual void loadExpandedState(QSortFilterProxyModel *pm, QTreeView *tv, const QByteArray &state); + + /** + * Convert a model index to a file. + * @param idx The model index + * @return The file index or 0 for a directory + **/ + virtual bt::TorrentFileInterface *indexToFile(const QModelIndex &idx) = 0; + + /** + * Get the path of a directory (root directory not included) + * @param idx The model index + * @return The path + */ + virtual QString dirPath(const QModelIndex &idx) = 0; + + /** + * Change the priority of a bunch of items. + * @param indexes The list of items + * @param newpriority The new priority + */ + virtual void changePriority(const QModelIndexList &indexes, bt::Priority newpriority) = 0; + + /** + * Missing files have been marked DND, update the preview and selection information. + */ + virtual void missingFilesMarkedDND(); + + /** + * Update gui if necessary + */ + virtual void update(); + + /** + * Codec has changed, so update the model. + */ + virtual void onCodecChange(); + + /// Set the file names editable + void setFileNamesEditable(bool on) + { + file_names_editable = on; + } + + /// Are the file names editable + bool fileNamesEditable() const + { + return file_names_editable; + } + + Qt::ItemFlags flags(const QModelIndex &index) const override; + + virtual void filePercentageChanged(bt::TorrentFileInterface *file, float percentage); + virtual void filePreviewChanged(bt::TorrentFileInterface *file, bool preview); +Q_SIGNALS: + /** + * Emitted whenever one or more items changes check state + */ + void checkStateChanged(); + +protected: + bt::TorrentInterface *tc; + DeselectMode mode; + bool file_names_editable; +}; +} + +#endif diff --git a/libktcore/torrent/torrentfiletreemodel.cpp b/libktcore/torrent/torrentfiletreemodel.cpp new file mode 100644 index 0000000..1a442af --- /dev/null +++ b/libktcore/torrent/torrentfiletreemodel.cpp @@ -0,0 +1,705 @@ +/* + SPDX-FileCopyrightText: 2007 Joris Guisson + SPDX-FileCopyrightText: 2007 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "torrentfiletreemodel.h" + +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace bt; + +namespace kt +{ +TorrentFileTreeModel::Node::Node(Node *parent, bt::TorrentFileInterface *file, const QString &name, const bt::Uint32 total_chunks) + : parent(parent) + , file(file) + , name(name) + , size(0) + , chunks(total_chunks) + , chunks_set(false) + , percentage(0.0f) +{ + chunks.setAll(false); +} + +TorrentFileTreeModel::Node::Node(Node *parent, const QString &name, const bt::Uint32 total_chunks) + : parent(parent) + , file(nullptr) + , name(name) + , size(0) + , chunks(total_chunks) + , chunks_set(false) + , percentage(0.0f) +{ + chunks.setAll(false); +} + +TorrentFileTreeModel::Node::~Node() +{ + qDeleteAll(children); +} + +void TorrentFileTreeModel::Node::insert(const QString &path, bt::TorrentFileInterface *file, bt::Uint32 num_chunks) +{ + int p = path.indexOf(bt::DirSeparator()); + if (p == -1) { + // the file is part of this directory + children.append(new Node(this, file, path, num_chunks)); + } else { + QString subdir = path.left(p); + for (Node *n : qAsConst(children)) { + if (n->name == subdir) { + n->insert(path.mid(p + 1), file, num_chunks); + return; + } + } + + Node *n = new Node(this, subdir, num_chunks); + children.append(n); + n->insert(path.mid(p + 1), file, num_chunks); + } +} + +int TorrentFileTreeModel::Node::row() +{ + if (parent) + return parent->children.indexOf(this); + else + return 0; +} + +bt::Uint64 TorrentFileTreeModel::Node::fileSize(const bt::TorrentInterface *tc) +{ + if (size > 0) + return size; + + if (!file) { + // directory + for (Node *n : qAsConst(children)) + size += n->fileSize(tc); + } else { + size = file->getSize(); + } + return size; +} + +void TorrentFileTreeModel::Node::fillChunks() +{ + if (chunks_set) + return; + + if (!file) { + for (Node *n : qAsConst(children)) { + n->fillChunks(); + chunks.orBitSet(n->chunks); + } + } else { + for (Uint32 i = file->getFirstChunk(); i <= file->getLastChunk(); i++) + chunks.set(i, true); + } + chunks_set = true; +} + +void TorrentFileTreeModel::Node::updatePercentage(const BitSet &havechunks) +{ + if (!chunks_set) + fillChunks(); // make sure we know the chunks which are part of this node + + if (file) { + percentage = file->getDownloadPercentage(); + } else { + if (havechunks.numOnBits() == 0 || chunks.numOnBits() == 0) { + percentage = 0.0f; + } else if (havechunks.allOn()) { + percentage = 100.0f; + } else { + // take the chunks of the node and + // logical and them with the chunks we have + BitSet tmp(chunks); + tmp.andBitSet(havechunks); + + percentage = 100.0f * ((float)tmp.numOnBits() / (float)chunks.numOnBits()); + } + } + + if (parent) + parent->updatePercentage(havechunks); // update the percentage of the parent +} + +void TorrentFileTreeModel::Node::initPercentage(const bt::TorrentInterface *tc, const bt::BitSet &havechunks) +{ + if (!chunks_set) + fillChunks(); + + if (!tc->getStats().multi_file_torrent) { + percentage = bt::Percentage(tc->getStats()); + return; + } + + if (file) { + percentage = file->getDownloadPercentage(); + } else { + if (havechunks.numOnBits() == 0 || chunks.numOnBits() == 0) { + percentage = 0.0f; + } else if (havechunks.allOn()) { + percentage = 100.0f; + } else { + // take the chunks of the node and + // logical and them with the chunks we have + BitSet tmp(chunks); + tmp.andBitSet(havechunks); + + percentage = 100.0f * ((float)tmp.numOnBits() / (float)chunks.numOnBits()); + } + + for (Node *n : qAsConst(children)) + n->initPercentage(tc, havechunks); // update the percentage of the children + } +} + +bt::Uint64 TorrentFileTreeModel::Node::bytesToDownload(const bt::TorrentInterface *tc) +{ + bt::Uint64 s = 0; + + if (!file) { + // directory + for (Node *n : qAsConst(children)) + s += n->bytesToDownload(tc); + } else { + if (!file->doNotDownload()) + s = file->getSize(); + } + return s; +} + +Qt::CheckState TorrentFileTreeModel::Node::checkState(const bt::TorrentInterface *tc) const +{ + if (!file) { + bool found_checked = false; + bool found_unchecked = false; + // directory + for (Node *n : qAsConst(children)) { + Qt::CheckState s = n->checkState(tc); + if (s == Qt::PartiallyChecked) + return s; + else if (s == Qt::Checked) + found_checked = true; + else + found_unchecked = true; + + if (found_checked && found_unchecked) + return Qt::PartiallyChecked; + } + + return found_checked ? Qt::Checked : Qt::Unchecked; + } else { + return file->doNotDownload() || file->getPriority() == ONLY_SEED_PRIORITY ? Qt::Unchecked : Qt::Checked; + } +} + +void TorrentFileTreeModel::Node::saveExpandedState(const QModelIndex &index, QSortFilterProxyModel *pm, QTreeView *tv, BEncoder *enc) +{ + if (file) + return; + + enc->write(QByteArrayLiteral("expanded")); + enc->write((Uint32)(tv->isExpanded(pm->mapFromSource(index)) ? 1 : 0)); + + int idx = 0; + for (Node *n : qAsConst(children)) { + if (!n->file) { + enc->write(n->name.toUtf8()); + enc->beginDict(); + n->saveExpandedState(pm->index(idx, 0, index), pm, tv, enc); + enc->end(); + } + idx++; + } +} + +void TorrentFileTreeModel::Node::loadExpandedState(const QModelIndex &index, QSortFilterProxyModel *pm, QTreeView *tv, BNode *n) +{ + if (file) + return; + + BDictNode *dict = dynamic_cast(n); + if (!dict) + return; + + BValueNode *v = dict->getValue("expanded"); + if (v) + tv->setExpanded(pm->mapFromSource(index), v->data().toInt() == 1); + + int idx = 0; + for (Node *n : qAsConst(children)) { + if (!n->file) { + if (BDictNode *d = dict->getDict(n->name.toUtf8())) + n->loadExpandedState(pm->index(idx, 0, index), pm, tv, d); + } + idx++; + } +} + +QString TorrentFileTreeModel::Node::path() +{ + if (!parent) + return QString(); // the root node must not be included in the path + + if (file) + return parent->path() + name; + else + return parent->path() + name + bt::DirSeparator(); +} + +TorrentFileTreeModel::TorrentFileTreeModel(bt::TorrentInterface *tc, DeselectMode mode, QObject *parent) + : TorrentFileModel(tc, mode, parent) + , root(nullptr) + , emit_check_state_change(true) +{ + if (tc) { + if (tc->getStats().multi_file_torrent) + constructTree(); + else + root = new Node(nullptr, tc->getUserModifiedFileName(), tc->getStats().total_chunks); + } +} + +TorrentFileTreeModel::~TorrentFileTreeModel() +{ + delete root; +} + +void TorrentFileTreeModel::changeTorrent(bt::TorrentInterface *tc) +{ + beginResetModel(); + this->tc = tc; + delete root; + root = nullptr; + if (tc) { + if (tc->getStats().multi_file_torrent) + constructTree(); + else + root = new Node(nullptr, tc->getUserModifiedFileName(), tc->getStats().total_chunks); + } + endResetModel(); +} + +void TorrentFileTreeModel::constructTree() +{ + bt::Uint32 num_chunks = tc->getStats().total_chunks; + if (!root) + root = new Node(nullptr, tc->getUserModifiedFileName(), num_chunks); + + for (Uint32 i = 0; i < tc->getNumFiles(); i++) { + bt::TorrentFileInterface &tf = tc->getTorrentFile(i); + root->insert(tf.getUserModifiedPath(), &tf, num_chunks); + } +} + +void TorrentFileTreeModel::onCodecChange() +{ + beginResetModel(); + delete root; + root = nullptr; + constructTree(); + endResetModel(); +} + +int TorrentFileTreeModel::rowCount(const QModelIndex &parent) const +{ + if (!tc) + return 0; + + if (!parent.isValid()) + return 1; + + Node *n = (Node *)parent.internalPointer(); + return n->children.count(); +} + +int TorrentFileTreeModel::columnCount(const QModelIndex &) const +{ + return 2; +} + +QVariant TorrentFileTreeModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if (role != Qt::DisplayRole || orientation != Qt::Horizontal) + return QVariant(); + + switch (section) { + case 0: + return i18n("File"); + case 1: + return i18n("Size"); + default: + return QVariant(); + } +} + +QVariant TorrentFileTreeModel::data(const QModelIndex &index, int role) const +{ + if (!tc || !index.isValid()) + return QVariant(); + + Node *n = (Node *)index.internalPointer(); + if (!n) + return QVariant(); + + if (role == Qt::DisplayRole || role == Qt::EditRole) { + switch (index.column()) { + case 0: + return n->name; + case 1: + if (tc->getStats().multi_file_torrent) + return BytesToString(n->fileSize(tc)); + else + return BytesToString(tc->getStats().total_bytes); + default: + return QVariant(); + } + } else if (role == Qt::UserRole) { // sorting + switch (index.column()) { + case 0: + return n->name; + case 1: + if (tc->getStats().multi_file_torrent) + return n->fileSize(tc); + else + return tc->getStats().total_bytes; + default: + return QVariant(); + } + } else if (role == Qt::DecorationRole && index.column() == 0) { +#if 0 + if (!n->file && n->children.count() <= 0) { + qWarning() << tc; + qWarning() << tc->getStats().torrent_name; + } + + if (n->file) { + qWarning() << n; + qWarning() << n->file; + qWarning() << n->file->getPath(); + } +#endif + + // if this is an empty folder then we are in the single file case + if (!n->file) + return n->children.count() > 0 ? QIcon::fromTheme(QStringLiteral("folder")) + : QIcon::fromTheme(QMimeDatabase().mimeTypeForFile(tc->getStats().torrent_name).iconName()); + else + return QIcon::fromTheme(QMimeDatabase().mimeTypeForFile(n->file->getPath()).iconName()); + } else if (role == Qt::CheckStateRole && index.column() == 0) { + if (tc->getStats().multi_file_torrent) + return n->checkState(tc); + } + + return QVariant(); +} + +QModelIndex TorrentFileTreeModel::parent(const QModelIndex &index) const +{ + if (!tc || !index.isValid()) + return QModelIndex(); + + Node *child = static_cast(index.internalPointer()); + if (!child) + return QModelIndex(); + + Node *parent = child->parent; + if (!parent) + return QModelIndex(); + else + return createIndex(parent->row(), 0, parent); +} + +QModelIndex TorrentFileTreeModel::index(int row, int column, const QModelIndex &parent) const +{ + if (!tc || row < 0 || row >= rowCount(parent) || column < 0 || column >= columnCount(parent)) + return QModelIndex(); + + Node *p = nullptr; + + if (!parent.isValid()) + return createIndex(row, column, root); + else { + p = static_cast(parent.internalPointer()); + + if (row >= 0 && row < p->children.count()) + return createIndex(row, column, p->children.at(row)); + else + return QModelIndex(); + } +} + +bool TorrentFileTreeModel::setCheckState(const QModelIndex &index, Qt::CheckState state) +{ + if (!tc) + return false; + + Node *n = static_cast(index.internalPointer()); + if (!n) + return false; + + if (!n->file) { + bool reenable = false; + if (emit_check_state_change) { + reenable = true; + emit_check_state_change = false; + } + + for (int i = 0; i < n->children.count(); i++) { + // recurse down the tree + setCheckState(this->index(i, 0, index), state); + } + + if (reenable) + emit_check_state_change = true; + } else { + bt::TorrentFileInterface *file = n->file; + if (state == Qt::Checked) { + if (file->getPriority() == ONLY_SEED_PRIORITY) + file->setPriority(NORMAL_PRIORITY); + else + file->setDoNotDownload(false); + } else { + if (mode == KEEP_FILES) + file->setPriority(ONLY_SEED_PRIORITY); + else + file->setDoNotDownload(true); + } + dataChanged(createIndex(index.row(), 0), createIndex(index.row(), columnCount(index) - 1)); + + QModelIndex parent = index.parent(); + if (parent.isValid()) + dataChanged(parent, parent); // parent needs to be updated to + } + + if (emit_check_state_change) + checkStateChanged(); + return true; +} + +void TorrentFileTreeModel::modifyPathOfFiles(Node *n, const QString &path) +{ + if (!tc) + return; + + for (int i = 0; i < n->children.count(); i++) { + Node *c = n->children.at(i); + if (!c->file) // another directory, continue recursively + modifyPathOfFiles(c, path + c->name + bt::DirSeparator()); + else + c->file->setUserModifiedPath(path + c->name); + } +} + +bool TorrentFileTreeModel::setName(const QModelIndex &index, const QString &name) +{ + if (!tc) + return false; + + Node *n = static_cast(index.internalPointer()); + if (!n || name.isEmpty() || name.contains(bt::DirSeparator())) + return false; + + if (!tc->getStats().multi_file_torrent) { + // single file case so we only need to change the user modified name + tc->setUserModifiedFileName(name); + n->name = name; + dataChanged(index, index); + return true; + } + + if (!n->file) { + // we are in a directory + if (!n->parent) { + // toplevel directory name has changed + tc->setUserModifiedFileName(name); + } else { + // Check if there is a sibling with the same name + for (const Node *sibling : qAsConst(n->parent->children)) { + if (sibling != n && sibling->name == name) + return false; + } + } + + n->name = name; + dataChanged(index, index); + // modify the path of all files + modifyPathOfFiles(n, n->path()); + return true; + } else { + // Check if there is a sibling with the same name + for (const Node *sibling : qAsConst(n->parent->children)) { + if (sibling != n && sibling->name == name) + return false; + } + + n->name = name; + n->file->setUserModifiedPath(n->path()); + dataChanged(index, index); + return true; + } +} + +bool TorrentFileTreeModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + if (!tc || !index.isValid()) + return false; + + if (role == Qt::CheckStateRole) + return setCheckState(index, static_cast(value.toInt())); + else if (role == Qt::EditRole) + return setName(index, value.toString()); + + return false; +} + +void TorrentFileTreeModel::checkAll() +{ + if (tc && tc->getStats().multi_file_torrent) + setData(index(0, 0, QModelIndex()), Qt::Checked, Qt::CheckStateRole); +} + +void TorrentFileTreeModel::uncheckAll() +{ + if (tc && tc->getStats().multi_file_torrent) + setData(index(0, 0, QModelIndex()), Qt::Unchecked, Qt::CheckStateRole); +} + +void TorrentFileTreeModel::invertCheck() +{ + if (!tc || !tc->getStats().multi_file_torrent) + return; + + invertCheck(index(0, 0, QModelIndex())); +} + +void TorrentFileTreeModel::invertCheck(const QModelIndex &idx) +{ + if (!tc) + return; + + Node *n = static_cast(idx.internalPointer()); + if (!n) + return; + + if (!n->file) { + for (int i = 0; i < n->children.count(); i++) { + // recurse down the tree + invertCheck(this->index(i, 0, idx)); + } + } else { + if (n->file->doNotDownload()) + setData(idx, Qt::Checked, Qt::CheckStateRole); + else + setData(idx, Qt::Unchecked, Qt::CheckStateRole); + } +} + +bt::Uint64 TorrentFileTreeModel::bytesToDownload() +{ + if (!tc) + return 0; + + if (tc->getStats().multi_file_torrent) + return root->bytesToDownload(tc); + else + return tc->getStats().total_bytes; +} + +QByteArray TorrentFileTreeModel::saveExpandedState(QSortFilterProxyModel *pm, QTreeView *tv) +{ + if (!tc || !tc->getStats().multi_file_torrent) + return QByteArray(); + + QByteArray data; + BEncoder enc(new BEncoderBufferOutput(data)); + enc.beginDict(); + root->saveExpandedState(index(0, 0, QModelIndex()), pm, tv, &enc); + enc.end(); + return data; +} + +void TorrentFileTreeModel::loadExpandedState(QSortFilterProxyModel *pm, QTreeView *tv, const QByteArray &state) +{ + if (!tc || !tc->getStats().multi_file_torrent) + return; + + BDecoder dec(state, false, 0); + BNode *n = nullptr; + try { + n = dec.decode(); + if (n && n->getType() == BNode::DICT) { + root->loadExpandedState(index(0, 0, QModelIndex()), pm, tv, n); + } + } catch (bt::Error &err) { + Out(SYS_GEN | LOG_DEBUG) << "Failed to load expanded state" << endl; + } + delete n; +} + +bt::TorrentFileInterface *TorrentFileTreeModel::indexToFile(const QModelIndex &idx) +{ + if (!tc || !idx.isValid()) + return nullptr; + + Node *n = (Node *)idx.internalPointer(); + if (!n) + return nullptr; + + return n->file; +} + +QString TorrentFileTreeModel::dirPath(const QModelIndex &idx) +{ + if (!tc || !idx.isValid()) + return QString(); + + Node *n = (Node *)idx.internalPointer(); + if (!n || n == root) + return QString(); + + QString ret = n->name; + do { + n = n->parent; + if (n && n->parent) + ret = n->name + bt::DirSeparator() + ret; + } while (n); + + return ret; +} + +void TorrentFileTreeModel::changePriority(const QModelIndexList &indexes, bt::Priority newpriority) +{ + if (!tc) + return; + + for (const QModelIndex &idx : indexes) { + Node *n = (Node *)idx.internalPointer(); + if (!n) + continue; + + setData(idx, newpriority, Qt::UserRole); + } +} +} diff --git a/libktcore/torrent/torrentfiletreemodel.h b/libktcore/torrent/torrentfiletreemodel.h new file mode 100644 index 0000000..7fa3829 --- /dev/null +++ b/libktcore/torrent/torrentfiletreemodel.h @@ -0,0 +1,96 @@ +/* + SPDX-FileCopyrightText: 2007 Joris Guisson + SPDX-FileCopyrightText: 2007 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KTTORRENTFILETREEMODEL_H +#define KTTORRENTFILETREEMODEL_H + +#include "torrentfilemodel.h" +#include + +class QSortFilterProxyModel; + +namespace bt +{ +class BEncoder; +class BNode; +} + +namespace kt +{ +/** + * Model for displaying file trees of a torrent + * @author Joris Guisson + */ +class KTCORE_EXPORT TorrentFileTreeModel : public TorrentFileModel +{ + Q_OBJECT +protected: + struct KTCORE_EXPORT Node { + Node *parent; + bt::TorrentFileInterface *file; // file (0 if this is a directory) + QString name; // name or directory + QList children; // child dirs + bt::Uint64 size; + bt::BitSet chunks; + bool chunks_set; + float percentage; + + Node(Node *parent, bt::TorrentFileInterface *file, const QString &name, bt::Uint32 total_chunks); + Node(Node *parent, const QString &name, bt::Uint32 total_chunks); + ~Node(); + + void insert(const QString &path, bt::TorrentFileInterface *file, bt::Uint32 num_chunks); + int row(); + bt::Uint64 fileSize(const bt::TorrentInterface *tc); + bt::Uint64 bytesToDownload(const bt::TorrentInterface *tc); + Qt::CheckState checkState(const bt::TorrentInterface *tc) const; + QString path(); + void fillChunks(); + void updatePercentage(const bt::BitSet &havechunks); + void initPercentage(const bt::TorrentInterface *tc, const bt::BitSet &havechunks); + + void saveExpandedState(const QModelIndex &index, QSortFilterProxyModel *pm, QTreeView *tv, bt::BEncoder *enc); + void loadExpandedState(const QModelIndex &index, QSortFilterProxyModel *pm, QTreeView *tv, bt::BNode *node); + }; + +public: + TorrentFileTreeModel(bt::TorrentInterface *tc, DeselectMode mode, QObject *parent); + ~TorrentFileTreeModel() override; + + void changeTorrent(bt::TorrentInterface *tc) override; + int rowCount(const QModelIndex &parent) const override; + int columnCount(const QModelIndex &parent) const override; + QVariant headerData(int section, Qt::Orientation orientation, int role) const override; + QVariant data(const QModelIndex &index, int role) const override; + QModelIndex parent(const QModelIndex &index) const override; + QModelIndex index(int row, int column, const QModelIndex &parent) const override; + bool setData(const QModelIndex &index, const QVariant &value, int role) override; + void checkAll() override; + void uncheckAll() override; + void invertCheck() override; + bt::Uint64 bytesToDownload() override; + QByteArray saveExpandedState(QSortFilterProxyModel *pm, QTreeView *tv) override; + void loadExpandedState(QSortFilterProxyModel *pm, QTreeView *tv, const QByteArray &state) override; + bt::TorrentFileInterface *indexToFile(const QModelIndex &idx) override; + QString dirPath(const QModelIndex &idx) override; + void changePriority(const QModelIndexList &indexes, bt::Priority newpriority) override; + void onCodecChange() override; + +private: + void constructTree(); + void invertCheck(const QModelIndex &idx); + bool setCheckState(const QModelIndex &index, Qt::CheckState state); + bool setName(const QModelIndex &index, const QString &name); + void modifyPathOfFiles(Node *n, const QString &path); + +protected: + Node *root; + bool emit_check_state_change; +}; + +} + +#endif diff --git a/libktcore/util/indexofcompare.h b/libktcore/util/indexofcompare.h new file mode 100644 index 0000000..1f88030 --- /dev/null +++ b/libktcore/util/indexofcompare.h @@ -0,0 +1,26 @@ +/* + SPDX-FileCopyrightText: 2005 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KT_INDEXOFCOMPARE_HH +#define KT_INDEXOFCOMPARE_HH + +namespace kt +{ +template struct IndexOfCompare { + IndexOfCompare(Container *container) + : container(container) + { + } + + bool operator()(Item *a, Item *b) + { + return container->indexOf(a) < container->indexOf(b); + } + + Container *container; +}; +} + +#endif diff --git a/libktcore/util/itemselectionmodel.cpp b/libktcore/util/itemselectionmodel.cpp new file mode 100644 index 0000000..3abe201 --- /dev/null +++ b/libktcore/util/itemselectionmodel.cpp @@ -0,0 +1,87 @@ +/* + SPDX-FileCopyrightText: 2009 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "itemselectionmodel.h" +#include + +namespace kt +{ +ItemSelectionModel::ItemSelectionModel(QAbstractItemModel *model, QObject *parent) + : QItemSelectionModel(model, parent) +{ +} + +ItemSelectionModel::~ItemSelectionModel() +{ +} + +void ItemSelectionModel::select(const QModelIndex &index, QItemSelectionModel::SelectionFlags command) +{ + QItemSelection sel(index, index); + select(sel, command); +} + +void ItemSelectionModel::select(const QItemSelection &sel, QItemSelectionModel::SelectionFlags command) +{ + if (command == NoUpdate) + return; + + if (command & QItemSelectionModel::Clear) + selection.clear(); + + for (const QItemSelectionRange &r : sel) + doRange(r, command); + + QItemSelectionModel::select(sel, command); +} + +void ItemSelectionModel::doRange(const QItemSelectionRange r, QItemSelectionModel::SelectionFlags command) +{ + for (int i = r.topLeft().row(); i <= r.bottomRight().row(); i++) { + void *item = model()->index(i, 0).internalPointer(); + if (!item) + continue; + + if (command & QItemSelectionModel::Select) { + selection.insert(item); + } else if (command & QItemSelectionModel::Deselect) { + selection.remove(item); + } else if (command & QItemSelectionModel::Toggle) { + if (selection.contains(item)) + selection.remove(item); + else + selection.insert(item); + } + } +} + +void ItemSelectionModel::reset() +{ + selection.clear(); + QItemSelectionModel::reset(); +} + +void ItemSelectionModel::clear() +{ + selection.clear(); + QItemSelectionModel::clear(); +} + +void ItemSelectionModel::sorted() +{ + QItemSelection ns; + int rows = model()->rowCount(QModelIndex()); + int cols = model()->columnCount(QModelIndex()); + for (int i = 0; i < rows; i++) { + QModelIndex idx = model()->index(i, 0, QModelIndex()); + void *item = idx.internalPointer(); + if (item && selection.contains(item)) { + ns.select(idx, model()->index(i, cols - 1, QModelIndex())); + } + } + + select(ns, QItemSelectionModel::ClearAndSelect); +} +} diff --git a/libktcore/util/itemselectionmodel.h b/libktcore/util/itemselectionmodel.h new file mode 100644 index 0000000..1f26207 --- /dev/null +++ b/libktcore/util/itemselectionmodel.h @@ -0,0 +1,45 @@ +/* + SPDX-FileCopyrightText: 2009 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef ITEMSELECTIONMODEL_H +#define ITEMSELECTIONMODEL_H + +#include +#include + +#include + +namespace kt +{ +/** + * Selection model which works on internal pointers instead of indexes. + */ +class KTCORE_EXPORT ItemSelectionModel : public QItemSelectionModel +{ + Q_OBJECT +public: + ItemSelectionModel(QAbstractItemModel *model, QObject *parent); + ~ItemSelectionModel() override; + + void select(const QModelIndex &index, QItemSelectionModel::SelectionFlags command) override; + void select(const QItemSelection &sel, QItemSelectionModel::SelectionFlags command) override; + void clear() override; + void reset() override; + +public Q_SLOTS: + /** + * Updates the selection after a sort. + */ + void sorted(); + +private: + void doRange(const QItemSelectionRange r, QItemSelectionModel::SelectionFlags command); + +private: + QSet selection; +}; +} + +#endif // ITEMSELECTIONMODEL_H diff --git a/libktcore/util/mmapfile.cpp b/libktcore/util/mmapfile.cpp new file mode 100644 index 0000000..ecf59a8 --- /dev/null +++ b/libktcore/util/mmapfile.cpp @@ -0,0 +1,266 @@ +/* + SPDX-FileCopyrightText: 2005 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include + +#include "mmapfile.h" +#include +#include +#include + +namespace bt +{ +MMapFile::MMapFile() + : fptr(nullptr) + , data(nullptr) + , size(0) + , file_size(0) + , ptr(0) + , mode(QIODevice::ReadOnly) +{ +} + +MMapFile::~MMapFile() +{ + if (fptr) + close(); +} + +bool MMapFile::open(const QString &file, QIODevice::OpenModeFlag mode) +{ + // close already open file + if (fptr && fptr->isOpen()) { + close(); + } + + // setup flags + int mmap_flag = 0; + switch (mode) { + case QIODevice::ReadOnly: + mmap_flag = PROT_READ; + break; + case QIODevice::WriteOnly: + mmap_flag = PROT_WRITE; + break; + default: + case QIODevice::ReadWrite: + mmap_flag = PROT_READ | PROT_WRITE; + break; + } + + fptr = new QFile(file); + // open the file + if (!(fptr->open(mode))) { + delete fptr; + fptr = nullptr; + return false; + } + + // read the file size + this->size = fptr->size(); + this->mode = mode; + + file_size = fptr->size(); + filename = file; + + // mmap the file +#ifndef Q_WS_WIN + int fd = fptr->handle(); +#ifdef HAVE_MMAP64 + data = (Uint8 *)mmap64(0, size, mmap_flag, MAP_SHARED, fd, 0); +#else + data = (Uint8 *)mmap(nullptr, size, mmap_flag, MAP_SHARED, fd, 0); +#endif + if (data == MAP_FAILED) { + ::close(fd); + data = nullptr; + fd = -1; + ptr = 0; + return false; + } + ptr = 0; + return true; +#else // Q_WS_WIN + data = (Uint8 *)fptr->map(0, size); + + if (!data) { + fptr->close(); + delete fptr; + fptr = nullptr; + return false; + } + ptr = 0; + return true; +#endif +} + +void MMapFile::close() +{ + if (fptr) { +#ifndef Q_WS_WIN +#ifdef HAVE_MUNMAP64 + munmap64(data, size); +#else + munmap(data, size); +#endif +#else + fptr->unmap(data); +#endif + fptr->close(); + delete fptr; + fptr = nullptr; + ptr = size = 0; + data = nullptr; + filename = QString(); + } +} + +void MMapFile::flush() +{ + if (fptr) +#ifndef Q_WS_WIN + msync(data, size, MS_SYNC); +#else + FlushViewOfFile(data, size); +#endif +} + +Uint32 MMapFile::write(const void *buf, Uint32 buf_size) +{ + if (!fptr || mode == QIODevice::ReadOnly) + return 0; + + // check if data fits in memory mapping + if (ptr + buf_size > size) + throw Error(i18n("Cannot write beyond end of the mmap buffer.")); + + Out(SYS_GEN | LOG_DEBUG) << "MMapFile::write : " << (ptr + buf_size) << " " << file_size << endl; + // enlarge the file if necessary + if (ptr + buf_size > file_size) { + growFile(ptr + buf_size); + } + + // memcpy data + memcpy(&data[ptr], buf, buf_size); + // update ptr + ptr += buf_size; + // update file size if necessary + if (ptr >= size) + size = ptr; + + return buf_size; +} + +void MMapFile::growFile(Uint64 new_size) +{ + Out(SYS_GEN | LOG_DEBUG) << "Growing file to " << new_size << " bytes " << endl; + Uint64 to_write = new_size - file_size; + // jump to the end of the file + fptr->seek(fptr->size()); + + Uint8 buf[1024]; + memset(buf, 0, 1024); + // write data until to_write is 0 + while (to_write > 0) { + ssize_t w = fptr->write((const char *)buf, to_write > 1024 ? 1024 : to_write); + if (w > 0) + to_write -= w; + else if (w < 0) + break; + } + file_size = new_size; +} + +Uint32 MMapFile::read(void *buf, Uint32 buf_size) +{ + if (!fptr || mode == QIODevice::WriteOnly) + return 0; + + // check if we aren't going to read past the end of the file + Uint32 to_read = ptr + buf_size >= size ? size - ptr : buf_size; + // read data + memcpy(buf, data + ptr, to_read); + ptr += to_read; + return to_read; +} + +Uint64 MMapFile::seek(SeekPos from, Int64 num) +{ + switch (from) { + case BEGIN: + if (num > 0) + ptr = num; + if (ptr >= size) + ptr = size - 1; + break; + case END: { + Int64 np = (size - 1) + num; + if (np < 0) { + ptr = 0; + break; + } + if (np >= (Int64)size) { + ptr = size - 1; + break; + } + ptr = np; + } break; + case CURRENT: { + Int64 np = ptr + num; + if (np < 0) { + ptr = 0; + break; + } + if (np >= (Int64)size) { + ptr = size - 1; + break; + } + ptr = np; + } break; + } + return ptr; +} + +bool MMapFile::eof() const +{ + return ptr >= size; +} + +Uint64 MMapFile::tell() const +{ + return ptr; +} + +QString MMapFile::errorString() const +{ + return QString::fromUtf8(strerror(errno)); +} + +Uint64 MMapFile::getSize() const +{ + return size; +} + +Uint8 *MMapFile::getData(Uint64 off) +{ + if (off >= size) + return nullptr; + return &data[off]; +} +} diff --git a/libktcore/util/mmapfile.h b/libktcore/util/mmapfile.h new file mode 100644 index 0000000..430994d --- /dev/null +++ b/libktcore/util/mmapfile.h @@ -0,0 +1,119 @@ +/* + SPDX-FileCopyrightText: 2005 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef BTMMAPFILE_H +#define BTMMAPFILE_H + +#include +#include + +#include +#include + +namespace bt +{ +/** + * @author Joris Guisson + * @brief Memory mapped file + * + * This class allows to access memory mapped files. It's pretty similar to + * File. + */ +class KTCORE_EXPORT MMapFile +{ +public: + MMapFile(); + ~MMapFile(); + + /** + * Open the file. If mode is write and the file doesn't exist, it will + * be created. + * @param file Filename + * @param mode Mode (READ, WRITE or RW) + * @return true upon succes + */ + bool open(const QString &file, QIODevice::OpenModeFlag mode); + + /** + * Close the file. Undoes the memory mapping. + */ + void close(); + + /** + * Flush the file. + */ + void flush(); + + /** + * Write a bunch of data. + * @param buf The data + * @param size Size of the data + * @return The number of bytes written + */ + Uint32 write(const void *buf, Uint32 size); + + /** + * Read a bunch of data + * @param buf The buffer to store the data + * @param size Size of the buffer + * @return The number of bytes read + */ + Uint32 read(void *buf, Uint32 size); + + enum SeekPos { + BEGIN, + END, + CURRENT, + }; + + /** + * Seek in the file. + * @param from Position to seek from + * @param num Number of bytes to move + * @return New position + */ + Uint64 seek(SeekPos from, Int64 num); + + /// Check to see if we are at the end of the file. + bool eof() const; + + /// Get the current position in the file. + Uint64 tell() const; + + /// Get the error string. + QString errorString() const; + + /// Get the file size + Uint64 getSize() const; + + /** + * Get a pointer to the mmapped region of data. + * @param off Offset into buffer, if invalid 0 will be returned + * @return Pointer to a location in the mmapped region + */ + Uint8 *getData(Uint64 off); + + /// Gets the data pointer + void *getDataPointer() + { + return data; + } + +private: + void growFile(Uint64 new_size); + +private: + QFile *fptr; + Uint8 *data; + Uint64 size; // size of mmapping + Uint64 file_size; // size of file + Uint64 ptr; + QString filename; + QIODevice::OpenModeFlag mode; +}; + +} + +#endif diff --git a/libktcore/util/stringcompletionmodel.cpp b/libktcore/util/stringcompletionmodel.cpp new file mode 100644 index 0000000..f9fc3b1 --- /dev/null +++ b/libktcore/util/stringcompletionmodel.cpp @@ -0,0 +1,69 @@ +/* + SPDX-FileCopyrightText: 2009 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "stringcompletionmodel.h" + +#include +#include +#include + +#include + +using namespace bt; + +namespace kt +{ +StringCompletionModel::StringCompletionModel(const QString &file, QObject *parent) + : QStringListModel(parent) + , file(file) +{ +} + +StringCompletionModel::~StringCompletionModel() +{ +} + +void StringCompletionModel::load() +{ + QFile fptr(file); + if (!fptr.open(QIODevice::ReadOnly)) { + Out(SYS_GEN | LOG_NOTICE) << "Failed to open " << file << " : " << fptr.errorString() << endl; + return; + } + + QSet strings; + while (!fptr.atEnd()) { + QString line = QString::fromUtf8(fptr.readLine().trimmed()); + if (line.length() > 0) + strings.insert(line); + } + + setStringList(QList(strings.begin(), strings.end())); +} + +void StringCompletionModel::save() +{ + QFile fptr(file); + if (!fptr.open(QIODevice::WriteOnly)) { + Out(SYS_GEN | LOG_NOTICE) << "Failed to open " << file << " : " << fptr.errorString() << endl; + return; + } + + QTextStream out(&fptr); + const QStringList sl = stringList(); + for (const QString &s : sl) + out << s << Qt::endl; +} + +void StringCompletionModel::addString(const QString &s) +{ + QStringList curr = stringList(); + if (!curr.contains(s)) { + curr.append(s); + setStringList(curr); + } +} + +} diff --git a/libktcore/util/stringcompletionmodel.h b/libktcore/util/stringcompletionmodel.h new file mode 100644 index 0000000..338b27c --- /dev/null +++ b/libktcore/util/stringcompletionmodel.h @@ -0,0 +1,45 @@ +/* + SPDX-FileCopyrightText: 2009 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KT_STRINGCOMPLETIONMODEL_H +#define KT_STRINGCOMPLETIONMODEL_H + +#include +#include + +namespace kt +{ +/** + Model for a QCompleter which works with a list of unique strings loaded from a file. +*/ +class KTCORE_EXPORT StringCompletionModel : public QStringListModel +{ + Q_OBJECT +public: + StringCompletionModel(const QString &file, QObject *parent); + ~StringCompletionModel() override; + + /** + Load the list of strings. + */ + void load(); + + /** + Save the list of strings to the file + */ + void save(); + + /** + Add a string to the list, automatically saves it. + */ + void addString(const QString &s); + +private: + QString file; +}; + +} + +#endif // KT_STRINGCOMPLETIONMODEL_H diff --git a/libktcore/util/treefiltermodel.cpp b/libktcore/util/treefiltermodel.cpp new file mode 100644 index 0000000..2dfcb46 --- /dev/null +++ b/libktcore/util/treefiltermodel.cpp @@ -0,0 +1,41 @@ +/* + SPDX-FileCopyrightText: 2009 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "treefiltermodel.h" + +namespace kt +{ +TreeFilterModel::TreeFilterModel(QObject *parent) + : QSortFilterProxyModel(parent) +{ + setFilterCaseSensitivity(Qt::CaseInsensitive); +} + +TreeFilterModel::~TreeFilterModel() +{ +} + +bool TreeFilterModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const +{ + QModelIndex idx = sourceModel()->index(source_row, 0, source_parent); + if (!idx.isValid()) + return false; + + // if we are in a leaf return filterAcceptsRow + if (!sourceModel()->hasIndex(0, 0, idx)) + return QSortFilterProxyModel::filterAcceptsRow(source_row, source_parent); + + // now walk over each child recursively and check if one matches, if so we need to accept this + int child = 0; + while (sourceModel()->hasIndex(child, 0, idx)) { + if (filterAcceptsRow(child, idx)) + return true; + child++; + } + + return false; +} + +} diff --git a/libktcore/util/treefiltermodel.h b/libktcore/util/treefiltermodel.h new file mode 100644 index 0000000..243590f --- /dev/null +++ b/libktcore/util/treefiltermodel.h @@ -0,0 +1,29 @@ +/* + SPDX-FileCopyrightText: 2009 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KT_TREEFILTERMODEL_H +#define KT_TREEFILTERMODEL_H + +#include +#include + +namespace kt +{ +/** + SortFilterProxyModel for trees, which doesn't +*/ +class KTCORE_EXPORT TreeFilterModel : public QSortFilterProxyModel +{ +public: + TreeFilterModel(QObject *parent = nullptr); + ~TreeFilterModel() override; + +protected: + bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override; +}; + +} + +#endif // KT_TREEFILTERMODEL_H diff --git a/logo.png b/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..50e8f4c94132363ea369705e4adace58187c0fa2 GIT binary patch literal 2949 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 literal 0 HcmV?d00001 diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt new file mode 100644 index 0000000..b918a3c --- /dev/null +++ b/plugins/CMakeLists.txt @@ -0,0 +1,47 @@ +MACRO (MACRO_KT_PLUGIN _option _name _dir) +option(${_option} "Whether to build the ${_name} plugin or not" true) +if (${_option}) + message(STATUS "Building ${_name} plugin (-D${_option}=false to disable)") + add_subdirectory(${_dir}) +else (${_option}) + message(STATUS "Not building ${_name} plugin (-D${_option}=true to enable)") +endif (${_option}) +ENDMACRO (MACRO_KT_PLUGIN) + +macro_kt_plugin(ENABLE_LOGVIEWER_PLUGIN logviewer logviewer) +macro_kt_plugin(ENABLE_INFOWIDGET_PLUGIN infowidget infowidget) +if (HAVE_KF5Completion) + macro_kt_plugin(ENABLE_UPNP_PLUGIN upnp upnp) +endif() +if (HAVE_Qt5WebEngineWidgets) + macro_kt_plugin(ENABLE_SEARCH_PLUGIN search search) +endif() +macro_kt_plugin(ENABLE_SCANFOLDER_PLUGIN scanfolder scanfolder) +macro_kt_plugin(ENABLE_SCANFOLDER_PLUGIN scanforlostfiles scanforlostfiles) +if(HAVE_QT5_Test) + set(HAVE_QT5_Test2 1) +endif() +if (HAVE_KF5TextWidgets) + macro_kt_plugin(ENABLE_IPFILTER_PLUGIN ipfilter ipfilter) +endif() +if (HAVE_KF5Plotting) + macro_kt_plugin(ENABLE_STATS_PLUGIN stats stats) +endif() +macro_kt_plugin(ENABLE_BWSCHEDULER_PLUGIN bwscheduler bwscheduler) +if (HAVE_Taglib AND PHONON_FOUND_EXPERIMENTAL) #AND Qt5Multimedia_FOUND) + macro_kt_plugin(ENABLE_MEDIAPLAYER_PLUGIN mediaplayer mediaplayer) +endif() +if (HAVE_KF5Archive AND HAVE_KF5ItemViews AND HAVE_KF5Kross) + macro_kt_plugin(ENABLE_SCRIPTING_PLUGIN scripting scripting) +endif() +if (HAVE_KF5Syndication AND HAVE_Qt5WebEngineWidgets) + macro_kt_plugin(ENABLE_SYNDICATION_PLUGIN syndication syndication) +endif() +macro_kt_plugin(ENABLE_DOWNLOADORDER_PLUGIN downloadorder downloadorder) +if (HAVE_LibKWorkspace) + macro_kt_plugin(ENABLE_SHUTDOWN_PLUGIN shutdown shutdown) +endif() +if (HAVE_KF5DNSSD) + macro_kt_plugin(ENABLE_ZEROCONF_PLUGIN zeroconf zeroconf) +endif() +macro_kt_plugin(ENABLE_MAGNETGENERATOR_PLUGIN magnetgenerator magnetgenerator) diff --git a/plugins/bwscheduler/CMakeLists.txt b/plugins/bwscheduler/CMakeLists.txt new file mode 100644 index 0000000..49b95db --- /dev/null +++ b/plugins/bwscheduler/CMakeLists.txt @@ -0,0 +1,38 @@ +add_library(ktorrent_bwscheduler MODULE) + +set(ktbwschedulerplugin_dbus_SRC) +set(screensaver_xml ${KTORRENT_DBUS_XML_DIR}/org.freedesktop.ScreenSaver.xml) +qt5_add_dbus_interface(ktbwschedulerplugin_dbus_SRC ${screensaver_xml} screensaver_interface) + +target_sources(ktorrent_bwscheduler PRIVATE + ${ktbwschedulerplugin_dbus_SRC} + bwschedulerplugin.cpp + weekview.cpp + weekscene.cpp + schedule.cpp + scheduleeditor.cpp + schedulegraphicsitem.cpp + bwprefpage.cpp + guidanceline.cpp + edititemdlg.cpp + weekdaymodel.cpp +) + +ki18n_wrap_ui(ktorrent_bwscheduler edititemdlg.ui bwprefpage.ui) +kconfig_add_kcfg_files(ktorrent_bwscheduler bwschedulerpluginsettings.kcfgc) + +kcoreaddons_desktop_to_json(ktorrent_bwscheduler ktorrent_bwscheduler.desktop) + +target_link_libraries( + ktorrent_bwscheduler + ktcore + Boost::boost + KF5::Torrent + KF5::CoreAddons + KF5::I18n + KF5::XmlGui + KF5::WidgetsAddons +) +install(TARGETS ktorrent_bwscheduler DESTINATION ${KTORRENT_PLUGIN_INSTALL_DIR} ) +install(FILES ktorrent_bwschedulerui.rc DESTINATION ${KXMLGUI_INSTALL_DIR}/ktorrent ) + diff --git a/plugins/bwscheduler/bwprefpage.cpp b/plugins/bwscheduler/bwprefpage.cpp new file mode 100644 index 0000000..9f6f139 --- /dev/null +++ b/plugins/bwscheduler/bwprefpage.cpp @@ -0,0 +1,38 @@ +/* + SPDX-FileCopyrightText: 2008 Joris Guisson + SPDX-FileCopyrightText: 2008 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "bwprefpage.h" +#include "bwschedulerpluginsettings.h" + +namespace kt +{ +BWPrefPage::BWPrefPage(QWidget *parent) + : PrefPageInterface(SchedulerPluginSettings::self(), i18n("Scheduler"), QStringLiteral("kt-bandwidth-scheduler"), parent) +{ + setupUi(this); +} + +BWPrefPage::~BWPrefPage() +{ +} + +void BWPrefPage::loadDefaults() +{ + kcfg_screensaverDownloadLimit->setEnabled(SchedulerPluginSettings::screensaverLimits()); + kcfg_screensaverUploadLimit->setEnabled(SchedulerPluginSettings::screensaverLimits()); +} + +void BWPrefPage::loadSettings() +{ + kcfg_screensaverDownloadLimit->setEnabled(SchedulerPluginSettings::screensaverLimits()); + kcfg_screensaverUploadLimit->setEnabled(SchedulerPluginSettings::screensaverLimits()); +} + +void BWPrefPage::updateSettings() +{ + colorsChanged(); +} + +} diff --git a/plugins/bwscheduler/bwprefpage.h b/plugins/bwscheduler/bwprefpage.h new file mode 100644 index 0000000..fa0efc5 --- /dev/null +++ b/plugins/bwscheduler/bwprefpage.h @@ -0,0 +1,35 @@ +/* + SPDX-FileCopyrightText: 2008 Joris Guisson + SPDX-FileCopyrightText: 2008 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KTBWPREFPAGE_H +#define KTBWPREFPAGE_H + +#include "ui_bwprefpage.h" +#include + +namespace kt +{ +/** + @author +*/ +class BWPrefPage : public PrefPageInterface, public Ui_BWPrefPage +{ + Q_OBJECT +public: + BWPrefPage(QWidget *parent); + ~BWPrefPage() override; + + void loadDefaults() override; + void loadSettings() override; + void updateSettings() override; + +Q_SIGNALS: + void colorsChanged(); +}; + +} + +#endif diff --git a/plugins/bwscheduler/bwprefpage.ui b/plugins/bwscheduler/bwprefpage.ui new file mode 100644 index 0000000..6039f78 --- /dev/null +++ b/plugins/bwscheduler/bwprefpage.ui @@ -0,0 +1,233 @@ + + BWPrefPage + + + + 0 + 0 + 473 + 385 + + + + + + + Special Limits + + + + + + Use these global limits when the screensaver is activated, instead of the ones configured in the network settings. + + + Use different speed limits when the screensaver is activated + + + + + + + + + + + Maximum upload speed: + + + + + + + + 0 + 0 + + + + Global upload limit when the screensaver is activated. + + + No limit + + + KiB/s + + + 100000000 + + + + + + + Maximum download speed: + + + + + + + + 0 + 0 + + + + Global download limit when the screensaver is activated. + + + No limit + + + KiB/s + + + 100000000 + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + Colors + + + + + + Schedule background color: + + + + + + + Color of the schedule background. + + + + + + + Schedule line color: + + + + + + + Color of all lines on the schedule. + + + + + + + Schedule Item color: + + + + + + + Color of each normal item on the schedule. + + + + + + + Suspended schedule item color: + + + + + + + Color of each suspended item on the schedule. + + + + + + + + + + Qt::Vertical + + + + 20 + 46 + + + + + + + + + KColorButton + QPushButton +
kcolorbutton.h
+
+
+ + + + kcfg_screensaverLimits + toggled(bool) + kcfg_screensaverUploadLimit + setEnabled(bool) + + + 101 + 44 + + + 159 + 78 + + + + + kcfg_screensaverLimits + toggled(bool) + kcfg_screensaverDownloadLimit + setEnabled(bool) + + + 70 + 51 + + + 131 + 114 + + + + +
diff --git a/plugins/bwscheduler/bwschedulerplugin.cpp b/plugins/bwscheduler/bwschedulerplugin.cpp new file mode 100644 index 0000000..23b8216 --- /dev/null +++ b/plugins/bwscheduler/bwschedulerplugin.cpp @@ -0,0 +1,229 @@ +/* + SPDX-FileCopyrightText: 2006 Ivan Vasić + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "bwschedulerplugin.h" + +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "bwprefpage.h" +#include "schedule.h" +#include "scheduleeditor.h" + +#include +#include +#include + +using namespace bt; + +K_PLUGIN_FACTORY_WITH_JSON(ktorrent_bwscheduler, "ktorrent_bwscheduler.json", registerPlugin();) + +namespace kt +{ +BWSchedulerPlugin::BWSchedulerPlugin(QObject *parent, const QVariantList &args) + : Plugin(parent) + , m_editor(nullptr) + , m_pref(nullptr) +{ + Q_UNUSED(args); + connect(&m_timer, &QTimer::timeout, this, &BWSchedulerPlugin::timerTriggered); + screensaver = + new org::freedesktop::ScreenSaver(QStringLiteral("org.freedesktop.ScreenSaver"), QStringLiteral("/ScreenSaver"), QDBusConnection::sessionBus(), this); + connect(screensaver, &org::freedesktop::ScreenSaver::ActiveChanged, this, &BWSchedulerPlugin::screensaverActivated); + screensaver_on = screensaver->GetActive(); + + QNetworkConfigurationManager *networkConfigurationManager = new QNetworkConfigurationManager(this); + connect(networkConfigurationManager, &QNetworkConfigurationManager::onlineStateChanged, this, &BWSchedulerPlugin::networkStatusChanged); +} + +BWSchedulerPlugin::~BWSchedulerPlugin() +{ +} + +void BWSchedulerPlugin::load() +{ + LogSystemManager::instance().registerSystem(i18n("Scheduler"), SYS_SCD); + m_schedule = new Schedule(); + m_pref = new BWPrefPage(nullptr); + connect(m_pref, &BWPrefPage::colorsChanged, this, &BWSchedulerPlugin::colorsChanged); + getGUI()->addPrefPage(m_pref); + + connect(getCore(), &CoreInterface::settingsChanged, this, &BWSchedulerPlugin::colorsChanged); + + try { + m_schedule->load(kt::DataDir() + QLatin1String("current.sched")); + } catch (bt::Error &err) { + Out(SYS_SCD | LOG_NOTICE) << "Failed to load current.sched : " << err.toString() << endl; + m_schedule->clear(); + } + + m_editor = new ScheduleEditor(nullptr); + connect(m_editor, &ScheduleEditor::loaded, this, &BWSchedulerPlugin::onLoaded); + connect(m_editor, &ScheduleEditor::scheduleChanged, this, &BWSchedulerPlugin::timerTriggered); + getGUI()->addActivity(m_editor); + m_editor->setSchedule(m_schedule); + + // make sure that schedule gets applied again if the settings change + connect(getCore(), &CoreInterface::settingsChanged, this, &BWSchedulerPlugin::timerTriggered); + timerTriggered(); +} + +void BWSchedulerPlugin::unload() +{ + setNormalLimits(); + LogSystemManager::instance().unregisterSystem(i18n("Bandwidth Scheduler")); + disconnect(getCore(), &CoreInterface::settingsChanged, this, &BWSchedulerPlugin::colorsChanged); + disconnect(getCore(), &CoreInterface::settingsChanged, this, &BWSchedulerPlugin::timerTriggered); + m_timer.stop(); + + getGUI()->removeActivity(m_editor); + delete m_editor; + m_editor = nullptr; + + getGUI()->removePrefPage(m_pref); + delete m_pref; + m_pref = nullptr; + + try { + m_schedule->save(kt::DataDir() + QLatin1String("current.sched")); + } catch (bt::Error &err) { + Out(SYS_SCD | LOG_NOTICE) << "Failed to save current.sched : " << err.toString() << endl; + } + + delete m_schedule; + m_schedule = nullptr; +} + +void BWSchedulerPlugin::setNormalLimits() +{ + int ulim = Settings::maxUploadRate(); + int dlim = Settings::maxDownloadRate(); + if (screensaver_on && SchedulerPluginSettings::screensaverLimits()) { + ulim = SchedulerPluginSettings::screensaverUploadLimit(); + dlim = SchedulerPluginSettings::screensaverDownloadLimit(); + } + + Out(SYS_SCD | LOG_NOTICE) << QStringLiteral("Changing schedule to normal values : %1 down, %2 up").arg(dlim).arg(ulim) << endl; + // set normal limits + getCore()->setSuspendedState(false); + net::SocketMonitor::setDownloadCap(1024 * dlim); + net::SocketMonitor::setUploadCap(1024 * ulim); + if (m_editor) + m_editor->updateStatusText(ulim, dlim, false, m_schedule->isEnabled()); + + PeerManager::connectionLimits().setLimits(Settings::maxTotalConnections(), Settings::maxConnections()); +} + +void BWSchedulerPlugin::timerTriggered() +{ + QDateTime now = QDateTime::currentDateTime(); + ScheduleItem *item = m_schedule->getCurrentItem(now); + if (!item || !m_schedule->isEnabled()) { + setNormalLimits(); + restartTimer(); + return; + } + + if (item->suspended) { + Out(SYS_SCD | LOG_NOTICE) << QStringLiteral("Changing schedule to : PAUSED") << endl; + if (!getCore()->getSuspendedState()) { + getCore()->setSuspendedState(true); + net::SocketMonitor::setDownloadCap(1024 * Settings::maxDownloadRate()); + net::SocketMonitor::setUploadCap(1024 * Settings::maxUploadRate()); + if (m_editor) + m_editor->updateStatusText(Settings::maxUploadRate(), Settings::maxDownloadRate(), true, m_schedule->isEnabled()); + } + } else { + int ulim = item->upload_limit; + int dlim = item->download_limit; + if (screensaver_on && SchedulerPluginSettings::screensaverLimits()) { + ulim = item->ss_upload_limit; + dlim = item->ss_download_limit; + } + + Out(SYS_SCD | LOG_NOTICE) << QStringLiteral("Changing schedule to : %1 down, %2 up").arg(dlim).arg(ulim) << endl; + getCore()->setSuspendedState(false); + + net::SocketMonitor::setDownloadCap(1024 * dlim); + net::SocketMonitor::setUploadCap(1024 * ulim); + if (m_editor) + m_editor->updateStatusText(ulim, dlim, false, m_schedule->isEnabled()); + } + + if (item->set_conn_limits) { + Out(SYS_SCD | LOG_NOTICE) + << QStringLiteral("Setting connection limits to : %1 per torrent, %2 global").arg(item->torrent_conn_limit).arg(item->global_conn_limit) << endl; + + PeerManager::connectionLimits().setLimits(item->global_conn_limit, item->torrent_conn_limit); + } else { + PeerManager::connectionLimits().setLimits(Settings::maxTotalConnections(), Settings::maxConnections()); + } + + restartTimer(); +} + +void BWSchedulerPlugin::restartTimer() +{ + QDateTime now = QDateTime::currentDateTime(); + // now calculate the new interval + int wait_time = m_schedule->getTimeToNextScheduleEvent(now) * 1000; + Out(SYS_SCD | LOG_NOTICE) << "Timer will fire in " << wait_time << " ms" << endl; + if (wait_time < 1000) + wait_time = 1000; + m_timer.stop(); + m_timer.start(wait_time); +} + +void BWSchedulerPlugin::onLoaded(Schedule *ns) +{ + delete m_schedule; + m_schedule = ns; + m_editor->setSchedule(ns); + timerTriggered(); +} + +bool BWSchedulerPlugin::versionCheck(const QString &version) const +{ + return version == QStringLiteral(VERSION); +} + +void BWSchedulerPlugin::colorsChanged() +{ + if (m_editor) { + m_editor->setSchedule(m_schedule); + m_editor->colorsChanged(); + } +} + +void BWSchedulerPlugin::screensaverActivated(bool on) +{ + screensaver_on = on; + timerTriggered(); +} + +void BWSchedulerPlugin::networkStatusChanged(bool online) +{ + if (online) { + Out(SYS_SCD | LOG_NOTICE) << "Network is up, setting schedule" << endl; + timerTriggered(); + } +} + +} + +#include diff --git a/plugins/bwscheduler/bwschedulerplugin.h b/plugins/bwscheduler/bwschedulerplugin.h new file mode 100644 index 0000000..5fef791 --- /dev/null +++ b/plugins/bwscheduler/bwschedulerplugin.h @@ -0,0 +1,61 @@ +/* + SPDX-FileCopyrightText: 2006 Ivan Vasić + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KTschedulerPLUGIN_H +#define KTschedulerPLUGIN_H + +#include "screensaver_interface.h" +#include +#include +#include +#include + +class QString; + +namespace kt +{ +class ScheduleEditor; +class Schedule; +class BWPrefPage; + +/** + * @author Ivan Vasic + * @brief KTorrent scheduler plugin. + * + */ +class BWSchedulerPlugin : public Plugin +{ + Q_OBJECT +public: + BWSchedulerPlugin(QObject *parent, const QVariantList &args); + ~BWSchedulerPlugin() override; + + void load() override; + void unload() override; + bool versionCheck(const QString &version) const override; + +public Q_SLOTS: + void timerTriggered(); + void onLoaded(Schedule *ns); + void colorsChanged(); + void screensaverActivated(bool on); + void networkStatusChanged(bool online); + +private: + void setNormalLimits(); + void restartTimer(); + +private: + QTimer m_timer; + ScheduleEditor *m_editor; + Schedule *m_schedule; + BWPrefPage *m_pref; + org::freedesktop::ScreenSaver *screensaver; + bool screensaver_on; +}; + +} + +#endif diff --git a/plugins/bwscheduler/bwschedulerpluginsettings.kcfgc b/plugins/bwscheduler/bwschedulerpluginsettings.kcfgc new file mode 100644 index 0000000..5f2f408 --- /dev/null +++ b/plugins/bwscheduler/bwschedulerpluginsettings.kcfgc @@ -0,0 +1,7 @@ +# Code generation options for kconfig_compiler +File=ktbwschedulerplugin.kcfg +ClassName=SchedulerPluginSettings +Namespace=kt +Singleton=true +Mutators=true +# will create the necessary code for setting those variables diff --git a/plugins/bwscheduler/edititemdlg.cpp b/plugins/bwscheduler/edititemdlg.cpp new file mode 100644 index 0000000..4d7a763 --- /dev/null +++ b/plugins/bwscheduler/edititemdlg.cpp @@ -0,0 +1,153 @@ +/* + SPDX-FileCopyrightText: 2007 Joris Guisson + SPDX-FileCopyrightText: 2007 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include +#include + +#include + +#include "edititemdlg.h" +#include "schedule.h" + +namespace kt +{ +EditItemDlg::EditItemDlg(kt::Schedule *schedule, ScheduleItem *item, bool new_item, QWidget *parent) + : QDialog(parent) + , schedule(schedule) + , item(item) +{ + setupUi(this); + connect(m_buttonBox, &QDialogButtonBox::accepted, this, &EditItemDlg::accept); + connect(m_buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); + + connect(m_suspended, &QCheckBox::toggled, this, &EditItemDlg::suspendedChanged); + connect(m_screensaver_limits, &QCheckBox::toggled, this, &EditItemDlg::screensaverLimitsToggled); + + QLocale locale(QLocale::system()); + for (int i = 1; i <= 7; i++) { + m_start_day->addItem(locale.dayName(i)); + m_end_day->addItem(locale.dayName(i)); + } + + m_from->setMaximumTime(QTime(23, 58, 0)); + m_to->setMinimumTime(QTime(0, 1, 0)); + + m_start_day->setCurrentIndex(0); + m_end_day->setCurrentIndex(6); + + m_from->setTime(item->start); + m_to->setTime(item->end); + m_start_day->setCurrentIndex(item->start_day - 1); + m_end_day->setCurrentIndex(item->end_day - 1); + m_suspended->setChecked(item->suspended); + m_upload_limit->setValue(item->upload_limit); + m_download_limit->setValue(item->download_limit); + m_set_connection_limits->setChecked(item->set_conn_limits); + m_max_conn_per_torrent->setEnabled(item->set_conn_limits); + m_max_conn_per_torrent->setValue(item->torrent_conn_limit); + m_max_conn_global->setValue(item->global_conn_limit); + m_max_conn_global->setEnabled(item->set_conn_limits); + m_screensaver_limits->setChecked(item->screensaver_limits); + m_screensaver_limits->setEnabled(!item->suspended); + m_ss_download_limit->setValue(item->ss_download_limit); + m_ss_upload_limit->setValue(item->ss_upload_limit); + m_ss_download_limit->setEnabled(!item->suspended && item->screensaver_limits); + m_ss_upload_limit->setEnabled(!item->suspended && item->screensaver_limits); + + m_buttonBox->button(QDialogButtonBox::Ok)->setEnabled(!schedule->conflicts(item)); + + connect(m_from, &QTimeEdit::timeChanged, this, &EditItemDlg::fromChanged); + connect(m_to, &QTimeEdit::timeChanged, this, &EditItemDlg::toChanged); + connect(m_start_day, qOverload(&QComboBox::activated), this, &EditItemDlg::startDayChanged); + connect(m_end_day, qOverload(&QComboBox::activated), this, &EditItemDlg::endDayChanged); + + setWindowTitle(new_item ? i18n("Add an item") : i18n("Edit an item")); +} + +EditItemDlg::~EditItemDlg() +{ +} + +void EditItemDlg::fromChanged(const QTime &time) +{ + // ensure that from is always smaller then to + if (time >= m_to->time()) + m_to->setTime(time.addSecs(60)); + + fillItem(); + m_buttonBox->button(QDialogButtonBox::Ok)->setEnabled(!schedule->conflicts(item)); +} + +void EditItemDlg::toChanged(const QTime &time) +{ + // ensure that from is always smaller then to + if (time <= m_from->time()) + m_from->setTime(time.addSecs(-60)); + + fillItem(); + m_buttonBox->button(QDialogButtonBox::Ok)->setEnabled(!schedule->conflicts(item)); +} + +void EditItemDlg::startDayChanged(int idx) +{ + // Make sure end day is >= start day + if (idx > m_end_day->currentIndex()) + m_end_day->setCurrentIndex(idx); + + fillItem(); + m_buttonBox->button(QDialogButtonBox::Ok)->setEnabled(!schedule->conflicts(item)); +} + +void EditItemDlg::endDayChanged(int idx) +{ + // Make sure end day is >= start day + if (idx < m_start_day->currentIndex()) + m_start_day->setCurrentIndex(idx); + + fillItem(); + m_buttonBox->button(QDialogButtonBox::Ok)->setEnabled(!schedule->conflicts(item)); +} + +void EditItemDlg::suspendedChanged(bool on) +{ + m_upload_limit->setDisabled(on); + m_download_limit->setDisabled(on); + m_screensaver_limits->setDisabled(on); + screensaverLimitsToggled(m_screensaver_limits->isChecked()); +} + +void EditItemDlg::screensaverLimitsToggled(bool on) +{ + m_ss_download_limit->setEnabled(!m_suspended->isChecked() && on); + m_ss_upload_limit->setEnabled(!m_suspended->isChecked() && on); +} + +void EditItemDlg::accept() +{ + fillItem(); + if (!schedule->conflicts(item)) + QDialog::accept(); +} + +void EditItemDlg::fillItem() +{ + item->start = m_from->time(); + item->end = m_to->time(); + item->start_day = m_start_day->currentIndex() + 1; + item->end_day = m_end_day->currentIndex() + 1; + item->upload_limit = m_upload_limit->value(); + item->download_limit = m_download_limit->value(); + item->suspended = m_suspended->isChecked(); + item->global_conn_limit = m_max_conn_global->value(); + item->torrent_conn_limit = m_max_conn_per_torrent->value(); + item->set_conn_limits = m_set_connection_limits->isChecked(); + item->screensaver_limits = m_screensaver_limits->isChecked(); + item->ss_download_limit = m_ss_download_limit->value(); + item->ss_upload_limit = m_ss_upload_limit->value(); + item->checkTimes(); +} + +} diff --git a/plugins/bwscheduler/edititemdlg.h b/plugins/bwscheduler/edititemdlg.h new file mode 100644 index 0000000..0eb4651 --- /dev/null +++ b/plugins/bwscheduler/edititemdlg.h @@ -0,0 +1,51 @@ +/* + SPDX-FileCopyrightText: 2007 Joris Guisson + SPDX-FileCopyrightText: 2007 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KTEDITITEMDLG_H +#define KTEDITITEMDLG_H + +#include "ui_edititemdlg.h" +#include + +namespace kt +{ +struct ScheduleItem; +class Schedule; + +/** + @author +*/ +class EditItemDlg : public QDialog, public Ui_EditItemDlg +{ + Q_OBJECT +public: + EditItemDlg(Schedule *schedule, ScheduleItem *item, bool new_item, QWidget *parent); + ~EditItemDlg() override; + + /** + * accept will only work if the item does not conflict + **/ + void accept() override; + +private Q_SLOTS: + void fromChanged(const QTime &time); + void toChanged(const QTime &time); + void startDayChanged(int idx); + void endDayChanged(int idx); + void suspendedChanged(bool on); + void screensaverLimitsToggled(bool on); + +private: + void fillItem(); + +private: + Schedule *schedule; + ScheduleItem *item; +}; + +} + +#endif diff --git a/plugins/bwscheduler/edititemdlg.ui b/plugins/bwscheduler/edititemdlg.ui new file mode 100644 index 0000000..7a316e7 --- /dev/null +++ b/plugins/bwscheduler/edititemdlg.ui @@ -0,0 +1,331 @@ + + + EditItemDlg + + + + 0 + 0 + 852 + 419 + + + + Edit an item + + + + + + Duration + + + + + + + + From: + + + + + + + hh:mm + + + + + + + To: + + + + + + + hh:mm + + + + + + + + + + + From: + + + + + + + + + + To: + + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + Limits + + + + + + Suspend all running torrents + + + + + + + + + Download limit: + + + + + + + No limit + + + KiB/s + + + 10000000 + + + + + + + Upload limit: + + + + + + + No limit + + + KiB/s + + + 10000000 + + + + + + + + + When screensaver is activated: + + + + + + + + + Download limit: + + + + + + + No limit + + + KiB/s + + + 10000000 + + + + + + + Upload limit: + + + + + + + No limit + + + KiB/s + + + 10000000 + + + + + + + + + + + + Connection Limits + + + + + + Set connection limits + + + + + + + + + Maximum connections per torrent: + + + + + + + No limit + + + 99999 + + + + + + + Global connection limit: + + + + + + + No limit + + + 99999 + + + 0 + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + + Qt::Horizontal + + + + + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + m_set_connection_limits + toggled(bool) + m_max_conn_per_torrent + setEnabled(bool) + + + 381 + 59 + + + 547 + 93 + + + + + m_set_connection_limits + toggled(bool) + m_max_conn_global + setEnabled(bool) + + + 327 + 59 + + + 547 + 126 + + + + + diff --git a/plugins/bwscheduler/guidanceline.cpp b/plugins/bwscheduler/guidanceline.cpp new file mode 100644 index 0000000..1e7ab8a --- /dev/null +++ b/plugins/bwscheduler/guidanceline.cpp @@ -0,0 +1,49 @@ +/* + SPDX-FileCopyrightText: 2008 Joris Guisson + SPDX-FileCopyrightText: 2008 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "guidanceline.h" + +#include "bwschedulerpluginsettings.h" +#include +#include + +namespace kt +{ +GuidanceLine::GuidanceLine(qreal x, qreal y, qreal text_offset) + : QGraphicsLineItem() + , x(x) + , y(y) + , text_offset(text_offset) +{ + QPen pen(SchedulerPluginSettings::scheduleLineColor()); + pen.setStyle(Qt::DashLine); + setPen(pen); + setZValue(5); + + const QString ZERO = QStringLiteral("00:00"); + text = new QGraphicsTextItem(ZERO, this); + text->setPos(text_offset, y); + + QFontMetricsF fm(text->font()); + qreal xe = text_offset + fm.width(ZERO); + setLine(x, y, xe, y); +} + +GuidanceLine::~GuidanceLine() +{ +} + +void GuidanceLine::update(qreal nx, qreal ny, const QString &txt) +{ + x = nx; + y = ny; + text->setPlainText(txt); + text->setPos(text_offset, y); + QFontMetricsF fm(text->font()); + qreal xe = text_offset + fm.width(txt); + setLine(x, y, xe, y); +} + +} diff --git a/plugins/bwscheduler/guidanceline.h b/plugins/bwscheduler/guidanceline.h new file mode 100644 index 0000000..f5e2ada --- /dev/null +++ b/plugins/bwscheduler/guidanceline.h @@ -0,0 +1,42 @@ +/* + SPDX-FileCopyrightText: 2008 Joris Guisson + SPDX-FileCopyrightText: 2008 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KTGUIDANCELINE_H +#define KTGUIDANCELINE_H + +#include +#include + +namespace kt +{ +/** + Line displayed when the user is resizing or moving items + The line has a text item below it to the side +*/ +class GuidanceLine : public QGraphicsLineItem +{ +public: + GuidanceLine(qreal x, qreal y, qreal text_offset); + ~GuidanceLine() override; + + /** + * Update the guidance line + * @param nx The nex x start + * @param ny The new y start + * @param text The text to display + */ + void update(qreal nx, qreal ny, const QString &text); + +private: + qreal x; + qreal y; + qreal text_offset; + QGraphicsTextItem *text; +}; + +} + +#endif diff --git a/plugins/bwscheduler/ktbwschedulerplugin.kcfg b/plugins/bwscheduler/ktbwschedulerplugin.kcfg new file mode 100644 index 0000000..56239fe --- /dev/null +++ b/plugins/bwscheduler/ktbwschedulerplugin.kcfg @@ -0,0 +1,33 @@ + + + + + + + QColor(0,255,0,255) + + + QColor(255,0,0,255) + + + QColor(Qt::yellow) + + + QColor(Qt::blue) + + + false + + + 0 + 0 + + + 0 + 0 + + + diff --git a/plugins/bwscheduler/ktorrent_bwscheduler.desktop b/plugins/bwscheduler/ktorrent_bwscheduler.desktop new file mode 100644 index 0000000..ef58bff --- /dev/null +++ b/plugins/bwscheduler/ktorrent_bwscheduler.desktop @@ -0,0 +1,106 @@ +[Desktop Entry] +Name=Scheduler +Name[bg]=Разписание +Name[bs]=Raspoređivač +Name[ca]=Planificador +Name[ca@valencia]=Planificador +Name[cs]=Plánovač +Name[da]=Skemalægning +Name[de]=Bandbreitenplaner +Name[el]=Χρονοπρογραμματιστής +Name[en_GB]=Scheduler +Name[es]=Planificador +Name[et]=Ajastaja +Name[fi]=Ajastin +Name[fr]=Planificateur +Name[ga]=Sceidealóir +Name[gl]=Planificador +Name[hu]=Ütemező +Name[ia]=Planificator +Name[it]=Pianificatore +Name[kk]=Жоспарлағыш +Name[km]=កម្មវិធី​រៀបចំ​ពេលវេលា +Name[ko]=스케줄러 +Name[lt]=Planuotojas +Name[mr]=नियोजक +Name[nb]=Tidsplanlegger +Name[nds]=Planer +Name[nl]=Planner +Name[nn]=Planleggjar +Name[pl]=Planista +Name[pt]=Escalonador +Name[pt_BR]=Agendador +Name[ro]=Programare +Name[ru]=Расписание загрузок +Name[si]=කාලසටහන්කරු +Name[sk]=Plánovač +Name[sl]=Razporejevalnik +Name[sr]=Распоређивач +Name[sr@ijekavian]=Распоређивач +Name[sr@ijekavianlatin]=Raspoređivač +Name[sr@latin]=Raspoređivač +Name[sv]=Schemaläggning +Name[tr]=Zamanlayıcı +Name[ug]=پىلانچى +Name[uk]=Планування +Name[x-test]=xxSchedulerxx +Name[zh_CN]=调度器 +Name[zh_TW]=排程器 +Comment=Schedule upload and download limits over a period of a week +Comment[bg]=Приставка за разчет на качването и свалянето през седмицата +Comment[bs]=Rasporedite ograničenja slanja i preuzimanja tokom sedmice +Comment[ca]=Planifica els límits de pujada i baixada durant un període setmanal +Comment[ca@valencia]=Planifica els límits de pujada i baixada durant un període setmanal +Comment[cs]=Naplánování odesílacích a stahovacích limitů během týdne +Comment[da]=Skemalæg up- og downloadgrænser over en periode pÃ¥ en uge +Comment[de]=Upload- und Downloadbegrenzungen über den Zeitraum von einer Woche planen +Comment[el]=Προγραμματισμός των ορίων αποστολής και λήψης για περίοδο μιας εβδομάδας +Comment[en_GB]=Schedule upload and download limits over a period of a week +Comment[es]=Planifique los límites de envío y descarga durante un periodo de una semana +Comment[et]=Üles- ja allalaadimise piirangute ajastamine terve nädala peale +Comment[fi]=Ajasta viikoittaiset lähetys- ja latausrajoitukset +Comment[fr]=Planifie les limites des envois et des téléchargement sur une période d'une semaine +Comment[ga]=Sceideal teorainneacha uasluchtaithe/íosluchtaithe le linn tréimhse seachtaine +Comment[gl]=Planificar os límites de envío e descarga dun período semanal. +Comment[hu]=Feltöltési és letöltési korlátok ütemezése egyhetes időszakra +Comment[ia]=Planifica limites de incargr e discargar sur un periodo de un septimana +Comment[is]=Gerðu áætlun um niðurhal og sendingamörk yfir vikutíma +Comment[it]=Pianifica i limiti di invio e scaricamento a livello settimanale +Comment[ja]=1 週間のアップロードとダウンロード帯域幅の制限を設定します +Comment[kk]=Бір апта мерзімге жүктеп алу мен беру шектерін жоспарлау +Comment[km]=តារាង​ពេលវេលា​ផ្ទុក​ឡើង និង​ទាញ​យក កំណត់​​អំឡុង​ពេល​ក្នុង​មួយ​សប្ដាហ៍ +Comment[ko]=매 주의 특정 시간마다 업로드와 다운로드 제한 예약 +Comment[lt]=Planuoti iÅ¡siuntimo ir atsiuntimo ribojimus per savaitės laikotarpį +Comment[lv]=Plāno augÅ¡upielādes un lejupielādes ātruma ierobežojumus pa nedēļas dienām +Comment[nb]=Gjør det mulig Ã¥ sette fartsgrenser for opplasting og nedlasting for ulike tider av uka +Comment[nds]=Grenzen för't Hooch- un Daalladen över en Week planen +Comment[nl]=Plannen van upload- en download-limieten over een periode van een week +Comment[nn]=Planlegg opplastings- og nedlastingsgrenser i periodar pÃ¥ ei veke +Comment[pl]=Tygodniowe planowanie szybkości wysyłania i pobierania plików +Comment[pt]=Um 'plugin' para agendar os limites de envio e recepção durante o período de uma semana +Comment[pt_BR]=Agenda os limites de download e envio, no período de uma semana +Comment[ro]=Planifică limitele de încărcare și descărcare pe perioada unei săptămîni +Comment[ru]=Позволяет устанавливать разные скоростные ограничения в разное время дня/недели +Comment[si]=සතියක කාලයක් තුල බාගැනීම් හා අපගත සීමා කාලසටහන්ගත කරන්න +Comment[sk]=NaplánovaÅ¥ limity odosielania a sÅ¥ahovania počas obdobia týždňa +Comment[sl]=Razporejanje omejitev hitrosti prenosov tekom tedna +Comment[sr]=Распоредите ограничења отпремања и преузимања током седмице +Comment[sr@ijekavian]=Распоредите ограничења отпремања и преузимања током седмице +Comment[sr@ijekavianlatin]=Rasporedite ograničenja otpremanja i preuzimanja tokom sedmice +Comment[sr@latin]=Rasporedite ograničenja otpremanja i preuzimanja tokom sedmice +Comment[sv]=Schemalägg uppladdnings- och nerladdningsgränser under en veckoperiod +Comment[tr]=Bir hataflık periyodlarla gönderme ve indirme sınırlarını zamanla +Comment[uk]=Додаток для складання розкладу обмежень на отримання на тиждень +Comment[x-test]=xxSchedule upload and download limits over a period of a weekxx +Comment[zh_CN]=调度一周内的上传和下载限制 +Comment[zh_TW]=排程一週以上的上傳與下載限制 +Type=Service +X-KDE-Library=ktbwschedulerplugin +X-KDE-PluginInfo-Author=Joris Guisson, Ivan Vasic +X-KDE-PluginInfo-Email=joris.guisson@gmail.com, ivasic@gmail.com +X-KDE-PluginInfo-Name=BandwidthSchedulerPlugin +X-KDE-PluginInfo-Version=0.1 +X-KDE-PluginInfo-Website=http://kde.org/applications/internet/ktorrent/ +X-KDE-PluginInfo-License=GPL +X-KDE-PluginInfo-EnabledByDefault=false +Icon=kt-bandwidth-scheduler diff --git a/plugins/bwscheduler/ktorrent_bwschedulerui.rc b/plugins/bwscheduler/ktorrent_bwschedulerui.rc new file mode 100644 index 0000000..465c1c3 --- /dev/null +++ b/plugins/bwscheduler/ktorrent_bwschedulerui.rc @@ -0,0 +1,16 @@ + + + + + Bandwidth Schedule + + + + + + + + + + + diff --git a/plugins/bwscheduler/schedule.cpp b/plugins/bwscheduler/schedule.cpp new file mode 100644 index 0000000..4b5ab31 --- /dev/null +++ b/plugins/bwscheduler/schedule.cpp @@ -0,0 +1,393 @@ +/* + SPDX-FileCopyrightText: 2007 Joris Guisson + SPDX-FileCopyrightText: 2007 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include +#include + +#include "schedule.h" +#include +#include +#include +#include +#include +#include + +using namespace bt; + +namespace kt +{ +ScheduleItem::ScheduleItem() + : start_day(0) + , end_day(0) + , upload_limit(0) + , download_limit(0) + , suspended(false) + , screensaver_limits(false) + , ss_upload_limit(0) + , ss_download_limit(0) + , set_conn_limits(false) + , global_conn_limit(0) + , torrent_conn_limit(0) +{ +} + +ScheduleItem::ScheduleItem(const ScheduleItem &item) +{ + operator=(item); +} + +bool ScheduleItem::conflicts(const ScheduleItem &other) const +{ + bool on_same_day = between(other.start_day, start_day, end_day) || between(other.end_day, start_day, end_day) + || (other.start_day <= start_day && other.end_day >= end_day); + + return on_same_day && (between(other.start, start, end) || between(other.end, start, end) || (other.start <= start && other.end >= end)); +} + +bool ScheduleItem::contains(const QDateTime &dt) const +{ + return between(dt.date().dayOfWeek(), start_day, end_day) && between(dt.time(), start, end); +} + +ScheduleItem &ScheduleItem::operator=(const ScheduleItem &item) +{ + start_day = item.start_day; + end_day = item.end_day; + start = item.start; + end = item.end; + upload_limit = item.upload_limit; + download_limit = item.download_limit; + suspended = item.suspended; + screensaver_limits = item.screensaver_limits; + ss_download_limit = item.ss_download_limit; + ss_upload_limit = item.ss_upload_limit; + set_conn_limits = item.set_conn_limits; + global_conn_limit = item.global_conn_limit; + torrent_conn_limit = item.torrent_conn_limit; + return *this; +} + +bool ScheduleItem::operator==(const ScheduleItem &item) const +{ + // clang-format off + return start_day == item.start_day && + end_day == item.end_day && + start == item.start && + end == item.end && + upload_limit == item.upload_limit && + download_limit == item.download_limit && + suspended == item.suspended && + set_conn_limits == item.set_conn_limits && + global_conn_limit == item.global_conn_limit && + torrent_conn_limit == item.torrent_conn_limit && + screensaver_limits == item.screensaver_limits && + ss_download_limit == item.ss_download_limit && + ss_upload_limit == item.ss_upload_limit; + // clang-format on +} + +void ScheduleItem::checkTimes() +{ + start.setHMS(start.hour(), start.minute(), 0); + end.setHMS(end.hour(), end.minute(), 59); +} + +///////////////////////////////////////// + +Schedule::Schedule() + : enabled(true) +{ +} + +Schedule::~Schedule() +{ + qDeleteAll(items); +} + +void Schedule::load(const QString &file) +{ + QFile fptr(file); + if (!fptr.open(QIODevice::ReadOnly)) { + QString msg = i18n("Cannot open file %1: %2", file, fptr.errorString()); + Out(SYS_SCD | LOG_NOTICE) << msg << endl; + throw bt::Error(msg); + } + + QByteArray data = fptr.readAll(); + BDecoder dec(data, false, 0); + BNode *node = nullptr; + try { + node = dec.decode(); + } catch (bt::Error &err) { + delete node; + Out(SYS_SCD | LOG_NOTICE) << "Decoding " << file << " failed : " << err.toString() << endl; + throw bt::Error(i18n("The file %1 is corrupted or not a proper KTorrent schedule file.", file)); + } + + if (!node) { + Out(SYS_SCD | LOG_NOTICE) << "Decoding " << file << " failed !" << endl; + throw bt::Error(i18n("The file %1 is corrupted or not a proper KTorrent schedule file.", file)); + } + + if (node->getType() == BNode::LIST) { + // Old format + parseItems((BListNode *)node); + } else if (node->getType() == BNode::DICT) { + BDictNode *dict = (BDictNode *)node; + BListNode *items = dict->getList(QByteArrayLiteral("items")); + if (items) + parseItems(items); + + try { + enabled = dict->getInt(QByteArrayLiteral("enabled")) == 1; + } catch (...) { + enabled = true; + } + } + + delete node; +} + +void Schedule::parseItems(BListNode *items) +{ + for (Uint32 i = 0; i < items->getNumChildren(); i++) { + BDictNode *dict = items->getDict(i); + if (!dict) + continue; + + ScheduleItem *item(new ScheduleItem()); + if (parseItem(item, dict)) + addItem(item); + else + delete item; + } +} + +bool Schedule::parseItem(ScheduleItem *item, bt::BDictNode *dict) +{ + // Must have at least a day or days entry + BValueNode *day = dict->getValue(QByteArrayLiteral("day")); + BValueNode *start_day = dict->getValue(QByteArrayLiteral("start_day")); + BValueNode *end_day = dict->getValue(QByteArrayLiteral("end_day")); + if (!day && !start_day && !end_day) + return false; + + BValueNode *start = dict->getValue(QByteArrayLiteral("start")); + BValueNode *end = dict->getValue(QByteArrayLiteral("end")); + BValueNode *upload_limit = dict->getValue(QByteArrayLiteral("upload_limit")); + BValueNode *download_limit = dict->getValue(QByteArrayLiteral("download_limit")); + BValueNode *suspended = dict->getValue(QByteArrayLiteral("suspended")); + + if (!start || !end || !upload_limit || !download_limit || !suspended) + return false; + + if (day) + item->start_day = item->end_day = day->data().toInt(); + else { + item->start_day = start_day->data().toInt(); + item->end_day = end_day->data().toInt(); + } + + item->start = QTime::fromString(start->data().toString()); + item->end = QTime::fromString(end->data().toString()); + item->upload_limit = upload_limit->data().toInt(); + item->download_limit = download_limit->data().toInt(); + item->suspended = suspended->data().toInt() == 1; + item->set_conn_limits = false; + + BDictNode *conn_limits = dict->getDict(QByteArrayLiteral("conn_limits")); + if (conn_limits) { + BValueNode *glob = conn_limits->getValue(QByteArrayLiteral("global")); + BValueNode *per_torrent = conn_limits->getValue(QByteArrayLiteral("per_torrent")); + if (glob && per_torrent) { + item->global_conn_limit = glob->data().toInt(); + item->torrent_conn_limit = per_torrent->data().toInt(); + item->set_conn_limits = true; + } + } + + BValueNode *ss_limits = dict->getValue(QByteArrayLiteral("screensaver_limits")); + if (ss_limits) { + item->screensaver_limits = ss_limits->data().toInt() == 1; + item->ss_download_limit = dict->getInt(QByteArrayLiteral("ss_download_limit")); + item->ss_upload_limit = dict->getInt(QByteArrayLiteral("ss_upload_limit")); + } else { + item->screensaver_limits = false; + item->ss_download_limit = item->ss_upload_limit = 0; + } + + item->checkTimes(); + return true; +} + +void Schedule::save(const QString &file) +{ + File fptr; + if (!fptr.open(file, QStringLiteral("wb"))) { + QString msg = i18n("Cannot open file %1: %2", file, fptr.errorString()); + Out(SYS_SCD | LOG_NOTICE) << msg << endl; + throw bt::Error(msg); + } + + BEncoder enc(&fptr); + enc.beginDict(); + enc.write(QByteArrayLiteral("enabled"), enabled); + enc.write(QByteArrayLiteral("items")); + enc.beginList(); + for (ScheduleItem *i : qAsConst(items)) { + enc.beginDict(); + enc.write(QByteArrayLiteral("start_day")); + enc.write((Uint32)i->start_day); + enc.write(QByteArrayLiteral("end_day")); + enc.write((Uint32)i->end_day); + enc.write(QByteArrayLiteral("start")); + enc.write(i->start.toString().toLatin1()); + enc.write(QByteArrayLiteral("end")); + enc.write(i->end.toString().toLatin1()); + enc.write(QByteArrayLiteral("upload_limit")); + enc.write(i->upload_limit); + enc.write(QByteArrayLiteral("download_limit")); + enc.write(i->download_limit); + enc.write(QByteArrayLiteral("suspended")); + enc.write((Uint32)(i->suspended ? 1 : 0)); + if (i->set_conn_limits) { + enc.write(QByteArrayLiteral("conn_limits")); + enc.beginDict(); + enc.write(QByteArrayLiteral("global")); + enc.write((Uint32)i->global_conn_limit); + enc.write(QByteArrayLiteral("per_torrent")); + enc.write((Uint32)i->torrent_conn_limit); + enc.end(); + } + enc.write(QByteArrayLiteral("screensaver_limits"), (Uint32)i->screensaver_limits); + enc.write(QByteArrayLiteral("ss_upload_limit"), i->ss_upload_limit); + enc.write(QByteArrayLiteral("ss_download_limit"), i->ss_download_limit); + enc.end(); + } + enc.end(); + enc.end(); +} + +void Schedule::clear() +{ + qDeleteAll(items); + items.clear(); +} + +bool Schedule::addItem(ScheduleItem *item) +{ + if (!item->isValid() || item->end <= item->start) + return false; + + for (ScheduleItem *i : qAsConst(items)) { + if (item->conflicts(*i)) + return false; + } + + items.append(item); + return true; +} + +void Schedule::removeItem(ScheduleItem *item) +{ + if (items.removeAll(item) > 0) + delete item; +} + +ScheduleItem *Schedule::getCurrentItem(const QDateTime &now) +{ + for (ScheduleItem *i : qAsConst(items)) { + if (i->contains(now)) { + return i; + } + } + return nullptr; +} + +int Schedule::getTimeToNextScheduleEvent(const QDateTime &now) +{ + ScheduleItem *item = getCurrentItem(now); + // when we are in the middle of a ScheduleItem, we need to trigger again at the end of it + if (item) + return now.time().secsTo(item->end) + 5; // change the schedule 5 seconds after it expires + + // lets look at all schedule items on the same day + // and find the next one + for (ScheduleItem *i : qAsConst(items)) { + if (between(now.date().dayOfWeek(), i->start_day, i->end_day) && i->start > now.time()) { + if (!item || i->start < item->start) + item = i; + } + } + + if (item) + return now.time().secsTo(item->start) + 5; + + QTime end_of_day(23, 59, 59); + return now.time().secsTo(end_of_day) + 5; +} + +bool Schedule::modify(kt::ScheduleItem *item, const QTime &start, const QTime &end, int start_day, int end_day) +{ + QTime old_start = item->start; + QTime old_end = item->end; + int old_start_day = item->start_day; + int old_end_day = item->end_day; + + item->start = start; + item->end = end; + item->start_day = start_day; + item->end_day = end_day; + item->checkTimes(); + if (!item->isValid() || conflicts(item)) { + // restore old start and end time + item->start = old_start; + item->end = old_end; + item->start_day = old_start_day; + item->end_day = old_end_day; + return false; + } + + return true; +} + +bool Schedule::validModify(ScheduleItem *item, const QTime &start, const QTime &end, int start_day, int end_day) +{ + QTime old_start = item->start; + QTime old_end = item->end; + int old_start_day = item->start_day; + int old_end_day = item->end_day; + + item->start = start; + item->end = end; + item->start_day = start_day; + item->end_day = end_day; + item->checkTimes(); + bool invalid = !item->isValid() || conflicts(item); + + // restore old start and end time + item->start = old_start; + item->end = old_end; + item->start_day = old_start_day; + item->end_day = old_end_day; + return !invalid; +} + +bool Schedule::conflicts(ScheduleItem *item) +{ + for (ScheduleItem *i : qAsConst(items)) { + if (i != item && (i->conflicts(*item) || item->conflicts(*i))) + return true; + } + return false; +} + +void Schedule::setEnabled(bool on) +{ + enabled = on; +} + +} diff --git a/plugins/bwscheduler/schedule.h b/plugins/bwscheduler/schedule.h new file mode 100644 index 0000000..d9a8131 --- /dev/null +++ b/plugins/bwscheduler/schedule.h @@ -0,0 +1,189 @@ +/* + SPDX-FileCopyrightText: 2007 Joris Guisson + SPDX-FileCopyrightText: 2007 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KTSCHEDULE_H +#define KTSCHEDULE_H + +#include +#include +#include + +namespace bt +{ +class BDictNode; +class BListNode; +} + +namespace kt +{ +template bool between(T v, T min_val, T max_val) +{ + return v >= min_val && v <= max_val; +} + +struct ScheduleItem { + int start_day; + int end_day; + QTime start; + QTime end; + bt::Uint32 upload_limit; + bt::Uint32 download_limit; + bool suspended; + bool screensaver_limits; + bt::Uint32 ss_upload_limit; + bt::Uint32 ss_download_limit; + bool set_conn_limits; + bt::Uint32 global_conn_limit; + bt::Uint32 torrent_conn_limit; + + ScheduleItem(); + ScheduleItem(const ScheduleItem &item); + + bool isValid() const + { + return between(start_day, 1, 7) && between(end_day, 1, 7) && start_day <= end_day; + } + + /** + * Check if this item conflicts with another + * @param other The other + * @return true If there is a conflict, false otherwise + */ + bool conflicts(const ScheduleItem &other) const; + + /** + * Assignment operator + * @param item The item to copy + * @return this + */ + ScheduleItem &operator=(const ScheduleItem &item); + + /** + * Comparison operator. + * @param item Item to compare + * @return true if the items are the same + */ + bool operator==(const ScheduleItem &item) const; + + /// Whether or not a QDateTime is falls within this item + bool contains(const QDateTime &dt) const; + + /// Check if start and end time are OK + void checkTimes(); +}; + +/** + * Class which holds the schedule of one week. + */ +class Schedule +{ +public: + Schedule(); + ~Schedule(); + + /** + * Load a schedule from a file. + * This will clear the current schedule. + * @param file The file to load from + * @throw Error When this fails + */ + void load(const QString &file); + + /** + * Save a schedule to a file. + * @param file The file to write to + * @throw Error When this fails + */ + void save(const QString &file); + + /** + * Add a ScheduleItem to the schedule + * @param item The ScheduleItem + * @return true upon succes, false otherwise (probably conflicts with other items) + */ + bool addItem(ScheduleItem *item); + + /** + * Get the current schedule item we should be setting. + * @return 0 If the current time doesn't fall into any item, the item otherwise + */ + ScheduleItem *getCurrentItem(const QDateTime &now); + + /** + * Get the time in seconds to the next time we need to update the schedule. + */ + int getTimeToNextScheduleEvent(const QDateTime &now); + + /** + * Try to modify start, stop time and day of an item. + * @param item The item + * @param start The start time + * @param end The stop time + * @param start_day The start day + * @param end_day The end day + * @return true If this succeeds (i.e. no conflicts) + */ + bool modify(ScheduleItem *item, const QTime &start, const QTime &end, int start_day, int end_day); + + /** + * Would a modify succeed ? + * @param item The item + * @param start The start time + * @param end The stop time + * @param start_day The start day + * @param end_day The end day + * @return true If this succeeds (i.e. no conflicts) + */ + bool validModify(ScheduleItem *item, const QTime &start, const QTime &end, int start_day, int end_day); + + /** + * Check for conflicts with other schedule items. + * @param item The item + */ + bool conflicts(ScheduleItem *item); + + /** + * Disable or enabled the schedule + */ + void setEnabled(bool on); + + /// Is the schedule enabled + bool isEnabled() const + { + return enabled; + } + + /// Clear the schedule + void clear(); + + /// Apply an operation on each ScheduleItem + template void apply(Operation op) + { + for (ScheduleItem *i : qAsConst(items)) + op(i); + } + + /// Remove a ScheduleItem, item will be deleted + void removeItem(ScheduleItem *item); + + /// Get the number of items in the schedule + int count() const + { + return items.count(); + } + +private: + bool parseItem(ScheduleItem *item, bt::BDictNode *dict); + void parseItems(bt::BListNode *items); + +private: + bool enabled; + QList items; +}; + +} + +#endif diff --git a/plugins/bwscheduler/scheduleeditor.cpp b/plugins/bwscheduler/scheduleeditor.cpp new file mode 100644 index 0000000..6bd684c --- /dev/null +++ b/plugins/bwscheduler/scheduleeditor.cpp @@ -0,0 +1,210 @@ +/* + SPDX-FileCopyrightText: 2007 Joris Guisson + SPDX-FileCopyrightText: 2007 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "scheduleeditor.h" + +#include +#include +#include +#include +#include + +#include +#include + +#include "edititemdlg.h" +#include "schedule.h" +#include "weekview.h" +#include + +namespace kt +{ +ScheduleEditor::ScheduleEditor(QWidget *parent) + : Activity(i18n("Bandwidth Schedule"), QStringLiteral("kt-bandwidth-scheduler"), 20, parent) + , schedule(nullptr) +{ + setXMLGUIFile(QStringLiteral("ktorrent_bwschedulerui.rc")); + setToolTip(i18n("Edit the bandwidth schedule")); + QVBoxLayout *layout = new QVBoxLayout(this); + view = new WeekView(this); + layout->addWidget(view); + layout->setMargin(0); + layout->setSpacing(0); + + setupActions(); + + clear_action->setEnabled(false); + edit_item_action->setEnabled(false); + remove_item_action->setEnabled(false); + + QMenu *menu = view->rightClickMenu(); + menu->addAction(new_item_action); + menu->addAction(edit_item_action); + menu->addAction(remove_item_action); + menu->addSeparator(); + menu->addAction(clear_action); + + connect(view, &WeekView::selectionChanged, this, &ScheduleEditor::onSelectionChanged); + connect(view, &WeekView::editItem, this, qOverload(&ScheduleEditor::editItem)); + connect(view, &WeekView::itemMoved, this, &ScheduleEditor::itemMoved); +} + +ScheduleEditor::~ScheduleEditor() +{ +} + +QAction *ScheduleEditor::addAction(const QString &icon, const QString &text, const QString &name, Func slot) +{ + KActionCollection *ac = part()->actionCollection(); + QAction *a = new QAction(QIcon::fromTheme(icon), text, this); + connect(a, &QAction::triggered, [this, slot](bool) { + (this->*slot)(); + }); + ac->addAction(name, a); + return a; +} + +void ScheduleEditor::setupActions() +{ + load_action = addAction(QStringLiteral("document-open"), i18n("Load Schedule"), QStringLiteral("schedule_load"), &ScheduleEditor::load); + save_action = addAction(QStringLiteral("document-save"), i18n("Save Schedule"), QStringLiteral("schedule_save"), &ScheduleEditor::save); + new_item_action = addAction(QStringLiteral("list-add"), i18n("New Item"), QStringLiteral("new_schedule_item"), &ScheduleEditor::addItem); + remove_item_action = addAction(QStringLiteral("list-remove"), i18n("Remove Item"), QStringLiteral("remove_schedule_item"), &ScheduleEditor::removeItem); + edit_item_action = addAction(QStringLiteral("edit-select-all"), i18n("Edit Item"), QStringLiteral("edit_schedule_item"), &ScheduleEditor::editItem); + clear_action = addAction(QStringLiteral("edit-clear"), i18n("Clear Schedule"), QStringLiteral("schedule_clear"), &ScheduleEditor::clear); + + QWidgetAction *act = new QWidgetAction(this); + enable_schedule = new QCheckBox(i18n("Scheduler Active"), this); + enable_schedule->setToolTip(i18n("Activate or deactivate the scheduler")); + act->setDefaultWidget(enable_schedule); + part()->actionCollection()->addAction(QStringLiteral("schedule_active"), act); + connect(enable_schedule, &QCheckBox::toggled, this, &ScheduleEditor::enableChecked); +} + +void ScheduleEditor::setSchedule(Schedule *s) +{ + schedule = s; + view->setSchedule(s); + onSelectionChanged(); + enable_schedule->setChecked(s->isEnabled()); + clear_action->setEnabled(s->count() > 0); +} + +void ScheduleEditor::clear() +{ + view->clear(); + schedule->clear(); + view->setSchedule(schedule); + clear_action->setEnabled(false); + edit_item_action->setEnabled(false); + remove_item_action->setEnabled(false); + scheduleChanged(); +} + +void ScheduleEditor::save() +{ + QString fn = QFileDialog::getSaveFileName(this, QString(), i18n("KTorrent scheduler files") + QLatin1String(" (*.sched)")); + if (!fn.isEmpty()) { + try { + schedule->save(fn); + } catch (bt::Error &err) { + QMessageBox::critical(this, QString(), err.toString()); + } + } +} + +void ScheduleEditor::load() +{ + QString fn = QFileDialog::getOpenFileName(this, QString(), i18n("KTorrent scheduler files") + QLatin1String(" (*.sched)")); + if (!fn.isEmpty()) { + Schedule *s = new Schedule(); + try { + s->load(fn); + loaded(s); + } catch (bt::Error &err) { + QMessageBox::critical(this, QString(), err.toString()); + delete s; + } + } +} + +void ScheduleEditor::addItem() +{ + ScheduleItem *item = new ScheduleItem(); + item->start_day = 1; + item->end_day = 7; + item->start = QTime(10, 0); + item->end = QTime(12, 0); + item->checkTimes(); + EditItemDlg dlg(schedule, item, true, this); + if (dlg.exec() == QDialog::Accepted && schedule->addItem(item)) { + clear_action->setEnabled(true); + view->addScheduleItem(item); + scheduleChanged(); + } else + delete item; +} + +void ScheduleEditor::removeItem() +{ + view->removeSelectedItems(); + clear_action->setEnabled(schedule->count() > 0); + scheduleChanged(); +} + +void ScheduleEditor::editItem(ScheduleItem *item) +{ + ScheduleItem tmp = *item; + + EditItemDlg dlg(schedule, item, false, this); + if (dlg.exec() == QDialog::Accepted) { + if (schedule->conflicts(item)) { + *item = tmp; // restore old values + QMessageBox::critical(this, QString(), i18n("This item conflicts with another item in the schedule, we cannot change it.")); + } else { + view->itemChanged(item); + } + clear_action->setEnabled(schedule->count() > 0); + scheduleChanged(); + } +} + +void ScheduleEditor::editItem() +{ + editItem(view->selectedItems().front()); +} + +void ScheduleEditor::onSelectionChanged() +{ + bool on = view->selectedItems().count() > 0; + edit_item_action->setEnabled(on); + remove_item_action->setEnabled(on); +} + +void ScheduleEditor::updateStatusText(int up, int down, bool suspended, bool enabled) +{ + view->updateStatusText(up, down, suspended, enabled); +} + +void ScheduleEditor::itemMoved(kt::ScheduleItem *item, const QTime &start, const QTime &end, int start_day, int end_day) +{ + schedule->modify(item, start, end, start_day, end_day); + view->itemChanged(item); + scheduleChanged(); +} + +void ScheduleEditor::colorsChanged() +{ + view->colorsChanged(); +} + +void ScheduleEditor::enableChecked(bool on) +{ + schedule->setEnabled(on); + scheduleChanged(); +} + +} diff --git a/plugins/bwscheduler/scheduleeditor.h b/plugins/bwscheduler/scheduleeditor.h new file mode 100644 index 0000000..ba1e3f9 --- /dev/null +++ b/plugins/bwscheduler/scheduleeditor.h @@ -0,0 +1,96 @@ +/* + SPDX-FileCopyrightText: 2007 Joris Guisson + SPDX-FileCopyrightText: 2007 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KTSCHEDULEEDITOR_H +#define KTSCHEDULEEDITOR_H + +#include + +class QCheckBox; + +namespace kt +{ +class WeekView; +class Schedule; +struct ScheduleItem; + +/** + @author +*/ +class ScheduleEditor : public Activity +{ + Q_OBJECT + + typedef void (ScheduleEditor::*Func)(); + +public: + ScheduleEditor(QWidget *parent); + ~ScheduleEditor() override; + + /** + * Set the current Schedule + * @param s The current schedule + */ + void setSchedule(Schedule *s); + + /** + * Update the text of the status line + * @param up Up speed + * @param down Down speed + * @param suspended Suspended or not + * @param enabled Enabled or not + */ + void updateStatusText(int up, int down, bool suspended, bool enabled); + + /** + * The color settings have changed + */ + void colorsChanged(); + +private Q_SLOTS: + void clear(); + void save(); + void load(); + void addItem(); + void removeItem(); + void editItem(); + void onSelectionChanged(); + void editItem(ScheduleItem *item); + void itemMoved(ScheduleItem *item, const QTime &start, const QTime &end, int start_day, int end_day); + void enableChecked(bool on); + +Q_SIGNALS: + /** + * Emitted when the user loads a new schedule. + * @param ns The new schedule + */ + void loaded(Schedule *ns); + + /** + * Emitted when something changes in the schedule. + */ + void scheduleChanged(); + +private: + void setupActions(); + QAction *addAction(const QString &icon, const QString &text, const QString &name, Func slot); + +private: + WeekView *view; + Schedule *schedule; + + QAction *load_action; + QAction *save_action; + QAction *new_item_action; + QAction *remove_item_action; + QAction *edit_item_action; + QAction *clear_action; + QCheckBox *enable_schedule; +}; + +} + +#endif diff --git a/plugins/bwscheduler/schedulegraphicsitem.cpp b/plugins/bwscheduler/schedulegraphicsitem.cpp new file mode 100644 index 0000000..ca50a7e --- /dev/null +++ b/plugins/bwscheduler/schedulegraphicsitem.cpp @@ -0,0 +1,317 @@ +/* + SPDX-FileCopyrightText: 2008 Joris Guisson + SPDX-FileCopyrightText: 2008 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "schedulegraphicsitem.h" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "bwschedulerpluginsettings.h" +#include "schedule.h" +#include "weekscene.h" +#include +#include + +using namespace bt; + +namespace kt +{ +const Uint32 Top = 1; +const Uint32 Bottom = 2; +const Uint32 Left = 4; +const Uint32 Right = 8; + +const Uint32 TopRight = Top | Right; +const Uint32 TopLeft = Top | Left; +const Uint32 BottomRight = Bottom | Right; +const Uint32 BottomLeft = Bottom | Left; + +ScheduleGraphicsItem::ScheduleGraphicsItem(ScheduleItem *item, const QRectF &r, const QRectF &constraints, WeekScene *ws) + : QGraphicsRectItem(r) + , item(item) + , constraints(constraints) + , ws(ws) + , text_item(nullptr) + , resize_edge(0) + , ready_to_resize(false) + , resizing(false) +{ + setAcceptHoverEvents(true); + setPen(QPen(Qt::black)); + setZValue(3); + setHandlesChildEvents(true); + + setBrush(QBrush(item->suspended ? SchedulerPluginSettings::suspendedColor() : SchedulerPluginSettings::itemColor())); + setFlag(QGraphicsItem::ItemIsSelectable, true); + setFlag(QGraphicsItem::ItemIsMovable, true); +} + +ScheduleGraphicsItem::~ScheduleGraphicsItem() +{ +} + +void ScheduleGraphicsItem::update(const QRectF &r) +{ + setRect(r); + setPos(QPointF(0, 0)); + QString text; + if (item->suspended) { + setBrush(QBrush(SchedulerPluginSettings::suspendedColor())); + text = i18n("Suspended"); + } else { + setBrush(QBrush(SchedulerPluginSettings::itemColor())); + QString ds = item->download_limit == 0 ? i18n("Unlimited") : BytesPerSecToString(item->download_limit * 1024); + QString us = item->upload_limit == 0 ? i18n("Unlimited") : BytesPerSecToString(item->upload_limit * 1024); + text = i18n("%1 Down\n%2 Up", ds, us); + } + + if (text_item == nullptr) + text_item = scene()->addText(text); + else + text_item->setPlainText(text); + + QFontMetricsF fm(text_item->font()); + text_item->setPos(QPointF(r.x(), r.y())); + text_item->setZValue(4); + text_item->setTextWidth(r.width()); + text_item->setParentItem(this); + setToolTip(text); + + if (text_item->boundingRect().height() > r.height()) { + // Text is to big for rect + delete text_item; + text_item = nullptr; + } +} + +QVariant ScheduleGraphicsItem::itemChange(GraphicsItemChange change, const QVariant &value) +{ + if (change == ItemPositionChange && scene()) { + QPointF new_pos = value.toPointF(); + if (!constraints.contains(new_pos)) { + qreal x = constraints.x() - boundingRect().x(); + if (new_pos.x() < x) + new_pos.setX(x); + else if (new_pos.x() + rect().width() > x + constraints.width()) + new_pos.setX(x + constraints.width() - rect().width()); + + qreal y = constraints.y() - boundingRect().y(); + if (new_pos.y() < y) + new_pos.setY(y); + else if (new_pos.y() + rect().height() > y + constraints.height()) + new_pos.setY(y + constraints.height() - rect().height()); + + return new_pos; + } + } + + return QGraphicsItem::itemChange(change, value); +} + +QRectF ScheduleGraphicsItem::resize(QPointF scene_pos) +{ + qreal x = scene_pos.x(); + qreal y = scene_pos.y(); + + QRectF cur = rect(); + if (resize_edge & Top) { + if (y >= cur.y() + cur.height()) { // rect becomes flipped + qreal yn = cur.y() + cur.height(); + if (yn < constraints.y()) + yn = constraints.y(); + + qreal h = y - yn; + cur.setY(yn); + cur.setHeight(h); + resize_edge |= kt::Bottom; + resize_edge &= ~kt::Top; + } else { + qreal yn = y < constraints.y() ? constraints.y() : y; + qreal h = cur.height() + (cur.y() - yn); + cur.setY(yn); + cur.setHeight(h); + } + } else if (resize_edge & Bottom) { + if (y < cur.y()) { // rect becomes flipped + qreal yn = y; + if (yn < constraints.y()) + yn = constraints.y(); + + qreal h = cur.y() - yn; + cur.setY(yn); + cur.setHeight(h); + resize_edge |= kt::Top; + resize_edge &= ~kt::Bottom; + } else { + cur.setHeight(y - cur.y()); + if (cur.y() + cur.height() >= constraints.y() + constraints.height()) + cur.setHeight(constraints.y() + constraints.height() - cur.y()); + } + } + + if (resize_edge & Left) { + if (x >= cur.x() + cur.width()) { // rect becomes flipped + qreal xn = cur.x() + cur.x(); + if (xn < constraints.x()) + xn = constraints.x(); + + qreal w = x - xn; + cur.setX(xn); + cur.setWidth(w); + resize_edge |= kt::Right; + resize_edge &= ~kt::Left; + } else { + qreal xn = x < constraints.x() ? constraints.x() : x; + qreal w = cur.width() + (cur.x() - xn); + cur.setX(xn); + cur.setWidth(w); + } + } else if (resize_edge & Right) { + if (x < cur.x()) { // rect becomes flipped + qreal xn = x; + if (xn < constraints.x()) + xn = constraints.x(); + + qreal w = cur.x() - xn; + cur.setX(xn); + cur.setWidth(w); + resize_edge |= kt::Left; + resize_edge &= ~kt::Right; + } else { + cur.setWidth(x - cur.x()); + if (cur.x() + cur.width() >= constraints.x() + constraints.width()) + cur.setWidth(constraints.x() + constraints.width() - cur.x()); + } + } + + return cur; +} + +void ScheduleGraphicsItem::mouseMoveEvent(QGraphicsSceneMouseEvent *event) +{ + if (!resizing) { + QGraphicsItem::mouseMoveEvent(event); + ws->setShowGuidanceLines(true); + QPointF sp = pos() + rect().topLeft(); + ws->updateGuidanceLines(sp.y(), sp.y() + rect().height()); + + setCursor(ws->validMove(item, sp) ? Qt::DragMoveCursor : Qt::ForbiddenCursor); + } else { + QRectF cur = resize(event->scenePos()); + setRect(cur); + if (text_item) + text_item->setPos(cur.x(), cur.y()); + + ws->updateGuidanceLines(cur.y(), cur.y() + cur.height()); + } +} + +void ScheduleGraphicsItem::mousePressEvent(QGraphicsSceneMouseEvent *event) +{ + if (!ready_to_resize || !(event->button() & Qt::LeftButton)) { + QGraphicsRectItem::mousePressEvent(event); + // keep track of original position before the item is dragged + original_pos = pos(); + } else { + resizing = true; + ws->setShowGuidanceLines(true); + ws->updateGuidanceLines(rect().y(), rect().y() + rect().height()); + } + + setZValue(4); +} + +void ScheduleGraphicsItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) +{ + if (resizing) { + resizing = false; + ws->setShowGuidanceLines(false); + ws->itemResized(item, rect()); + } else { + QGraphicsRectItem::mouseReleaseEvent(event); + + if (event->button() & Qt::LeftButton) { + if (original_pos != pos()) { + QPointF sp = pos() + rect().topLeft(); + ws->itemMoved(item, sp); + } + } + ws->setShowGuidanceLines(false); + } + + setZValue(3); + setCursor(Qt::ArrowCursor); +} + +void ScheduleGraphicsItem::hoverEnterEvent(QGraphicsSceneHoverEvent *event) +{ + ready_to_resize = true; + resize_edge = nearEdge(event->scenePos()); + updateCursor(); +} + +void ScheduleGraphicsItem::hoverLeaveEvent(QGraphicsSceneHoverEvent *event) +{ + Q_UNUSED(event); + setCursor(Qt::ArrowCursor); + ready_to_resize = false; +} + +void ScheduleGraphicsItem::hoverMoveEvent(QGraphicsSceneHoverEvent *event) +{ + resize_edge = nearEdge(event->scenePos()); + ready_to_resize = resize_edge != 0; + updateCursor(); +} + +void ScheduleGraphicsItem::updateCursor() +{ + Qt::CursorShape shape = Qt::ArrowCursor; + if (resize_edge != 0) { + if (resize_edge == kt::TopRight || resize_edge == kt::BottomLeft) + shape = Qt::SizeBDiagCursor; + else if (resize_edge == kt::BottomRight || resize_edge == kt::TopLeft) + shape = Qt::SizeFDiagCursor; + else if (resize_edge == kt::Top || resize_edge == kt::Bottom) + shape = Qt::SizeVerCursor; + else + shape = Qt::SizeHorCursor; + } + setCursor(shape); +} + +Uint32 ScheduleGraphicsItem::nearEdge(QPointF p) +{ + qreal y = rect().y(); + qreal ye = y + rect().height(); + qreal x = rect().x(); + qreal xe = x + rect().width(); + Uint32 ret = 0; + if (std::fabs(p.y() - y) < 4) + ret |= Top; + else if (std::fabs(p.y() - ye) < 4) + ret |= Bottom; + + if (std::fabs(p.x() - x) < 4) + ret |= Left; + else if (std::fabs(p.x() - xe) < 4) + ret |= Right; + + return ret; +} + +} diff --git a/plugins/bwscheduler/schedulegraphicsitem.h b/plugins/bwscheduler/schedulegraphicsitem.h new file mode 100644 index 0000000..9e78f2c --- /dev/null +++ b/plugins/bwscheduler/schedulegraphicsitem.h @@ -0,0 +1,58 @@ +/* + SPDX-FileCopyrightText: 2008 Joris Guisson + SPDX-FileCopyrightText: 2008 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KTSCHEDULEGRAPHICSITEM_H +#define KTSCHEDULEGRAPHICSITEM_H + +#include "schedule.h" +#include + +namespace kt +{ +class WeekScene; + +/** + QGraphicsItem to display a ScheduleItem +*/ +class ScheduleGraphicsItem : public QGraphicsRectItem +{ +public: + ScheduleGraphicsItem(ScheduleItem *item, const QRectF &r, const QRectF &constraints, WeekScene *ws); + ~ScheduleGraphicsItem() override; + + QVariant itemChange(GraphicsItemChange change, const QVariant &value) override; + + /** + * Update the item. + * @param r The new rect + */ + void update(const QRectF &r); + +private: + void mouseMoveEvent(QGraphicsSceneMouseEvent *event) override; + void mousePressEvent(QGraphicsSceneMouseEvent *event) override; + void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override; + void hoverEnterEvent(QGraphicsSceneHoverEvent *event) override; + void hoverLeaveEvent(QGraphicsSceneHoverEvent *event) override; + void hoverMoveEvent(QGraphicsSceneHoverEvent *event) override; + bt::Uint32 nearEdge(QPointF p); + QRectF resize(QPointF scene_pos); + void updateCursor(); + +private: + ScheduleItem *item; + QRectF constraints; + WeekScene *ws; + QGraphicsTextItem *text_item; + QPointF original_pos; + bt::Uint32 resize_edge; + bool ready_to_resize; + bool resizing; +}; + +} + +#endif diff --git a/plugins/bwscheduler/weekdaymodel.cpp b/plugins/bwscheduler/weekdaymodel.cpp new file mode 100644 index 0000000..679a70e --- /dev/null +++ b/plugins/bwscheduler/weekdaymodel.cpp @@ -0,0 +1,75 @@ +/* + SPDX-FileCopyrightText: 2008 Joris Guisson + SPDX-FileCopyrightText: 2008 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "weekdaymodel.h" + +#include + +namespace kt +{ +WeekDayModel::WeekDayModel(QObject *parent) + : QAbstractListModel(parent) +{ + for (int i = 0; i < 7; i++) + checked[i] = false; +} + +WeekDayModel::~WeekDayModel() +{ +} + +int WeekDayModel::rowCount(const QModelIndex &parent) const +{ + if (!parent.isValid()) + return 7; + else + return 0; +} + +QVariant WeekDayModel::data(const QModelIndex &index, int role) const +{ + if (index.row() < 0 || index.row() >= 7) + return QVariant(); + + if (role == Qt::DisplayRole) { + return QLocale::system().dayName(index.row() + 1); + } else if (role == Qt::CheckStateRole) { + return checked[index.row()] ? Qt::Checked : Qt::Unchecked; + } + + return QVariant(); +} + +bool WeekDayModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + if (!index.isValid() || index.row() < 0 || index.row() >= 7) + return false; + + if (role == Qt::CheckStateRole) { + checked[index.row()] = (Qt::CheckState)value.toUInt() == Qt::Checked; + dataChanged(index, index); + return true; + } + return false; +} + +Qt::ItemFlags WeekDayModel::flags(const QModelIndex &index) const +{ + if (!index.isValid() || index.row() >= 7 || index.row() < 0) + return QAbstractItemModel::flags(index); + else + return QAbstractItemModel::flags(index) | Qt::ItemIsUserCheckable; +} + +QList WeekDayModel::checkedDays() const +{ + QList ret; + for (int i = 0; i < 7; i++) + if (checked[i]) + ret << (i + 1); + return ret; +} + +} diff --git a/plugins/bwscheduler/weekdaymodel.h b/plugins/bwscheduler/weekdaymodel.h new file mode 100644 index 0000000..60fe75d --- /dev/null +++ b/plugins/bwscheduler/weekdaymodel.h @@ -0,0 +1,39 @@ +/* + SPDX-FileCopyrightText: 2008 Joris Guisson + SPDX-FileCopyrightText: 2008 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KTWEEKDAYMODEL_H +#define KTWEEKDAYMODEL_H + +#include + +namespace kt +{ +/** + Model to display the days of a week in a list view. The weekdays are checkable. + @author +*/ +class WeekDayModel : public QAbstractListModel +{ + Q_OBJECT +public: + WeekDayModel(QObject *parent); + ~WeekDayModel() override; + + int rowCount(const QModelIndex &parent) const override; + QVariant data(const QModelIndex &index, int role) const override; + bool setData(const QModelIndex &index, const QVariant &value, int role) override; + Qt::ItemFlags flags(const QModelIndex &index) const override; + + /// Get all the days which have been checked + QList checkedDays() const; + +private: + bool checked[7]; +}; + +} + +#endif diff --git a/plugins/bwscheduler/weekscene.cpp b/plugins/bwscheduler/weekscene.cpp new file mode 100644 index 0000000..2f2bbab --- /dev/null +++ b/plugins/bwscheduler/weekscene.cpp @@ -0,0 +1,297 @@ +/* + SPDX-FileCopyrightText: 2007 Joris Guisson + SPDX-FileCopyrightText: 2007 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "weekscene.h" + +#include + +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "bwschedulerpluginsettings.h" +#include "guidanceline.h" +#include "schedule.h" +#include "schedulegraphicsitem.h" +#include +#include + +using namespace bt; + +namespace kt +{ +WeekScene::WeekScene(QObject *parent) + : QGraphicsScene(parent) + , schedule(nullptr) +{ + addCalendar(); +} + +WeekScene::~WeekScene() +{ +} + +qreal LongestDayWidth(const QFontMetricsF &fm) +{ + qreal wd = 0; + for (int i = 1; i <= 7; i++) { + qreal w = fm.width(QLocale::system().dayName(i)); + if (w > wd) + wd = w; + } + return wd; +} + +void WeekScene::updateStatusText(int up, int down, bool suspended, bool enabled) +{ + static KFormat format; + QString msg; + if (suspended) + msg = i18n("Current schedule: suspended"); + else if (up > 0 && down > 0) + msg = i18n("Current schedule: %1/s download, %2/s upload", format.formatByteSize(down * 1024), format.formatByteSize(up * 1024)); + else if (up > 0) + msg = i18n("Current schedule: unlimited download, %1/s upload", format.formatByteSize(up * 1024)); + else if (down > 0) + msg = i18n("Current schedule: %1/s download, unlimited upload", format.formatByteSize(down * 1024)); + else + msg = i18n("Current schedule: unlimited upload and download"); + + if (!enabled) + msg += i18n(" (scheduler disabled)"); + + status->setPlainText(msg); +} + +void WeekScene::addCalendar() +{ + QGraphicsTextItem *tmp = addText(QStringLiteral("Dinges")); + QFontMetricsF fm(tmp->font()); + removeItem(tmp); + delete tmp; + + // first add 7 rectangles for each day of the week + xoff = fm.width(QStringLiteral("00:00")) + 10; + yoff = 2 * fm.height() + 10; + day_width = LongestDayWidth(fm) * 1.5; + hour_height = fm.height() * 1.5; + + status = addText(i18n("Current schedule:")); + status->setPos(QPointF(0, 0)); + status->setZValue(2); + + QPen pen(SchedulerPluginSettings::scheduleLineColor()); + QBrush brush(SchedulerPluginSettings::scheduleBackgroundColor()); + + for (int i = 0; i < 7; i++) { + QGraphicsRectItem *item = addRect(xoff + day_width * i, yoff, day_width, 24 * hour_height, pen, brush); + item->setZValue(1); + + QString day = QLocale::system().dayName(i + 1); + + // make sure day is centered in the middle of the column + qreal dlen = fm.width(day); + qreal mid = xoff + day_width * (i + 0.5); + qreal start = mid - dlen * 0.5; + + QGraphicsTextItem *t = addText(day); + t->setPos(QPointF(start, fm.height() + 5)); + t->setZValue(2); + + rects.append(item); + } + + // draw hour lines + for (int i = 0; i <= 24; i++) { + QGraphicsLineItem *item = addLine(0, yoff + i * hour_height, xoff + 7 * day_width, yoff + i * hour_height, pen); + item->setZValue(2); + + if (i < 24) { + QGraphicsTextItem *t = addText(QStringLiteral("%1:00").arg(i)); + t->setPos(QPointF(0, yoff + i * hour_height)); + t->setZValue(2); + } + lines.append(item); + } + + ; + gline[0] = new GuidanceLine(xoff, yoff, xoff + 7 * day_width + 10); + gline[0]->setVisible(false); + gline[1] = new GuidanceLine(xoff, yoff, xoff + 7 * day_width + 10); + gline[1]->setVisible(false); + addItem(gline[0]); + addItem(gline[1]); + + QRectF r = sceneRect(); + r.setHeight(r.height() + 10); + setSceneRect(r); +} + +QGraphicsItem *WeekScene::addScheduleItem(ScheduleItem *item) +{ + QTime midnight(0, 0, 0, 0); + qreal x = xoff + (item->start_day - 1) * day_width; + // qreal min_h = hour_height / 60.0; + qreal y = timeToY(item->start); + qreal ye = timeToY(item->end); + + QRectF rect(x, y, day_width * (item->end_day - item->start_day + 1), ye - y); + QRectF cst(xoff, yoff, 7 * day_width, 24 * hour_height); + ScheduleGraphicsItem *gi = new ScheduleGraphicsItem(item, rect, cst, this); + addItem(gi); + gi->update(rect); + return gi; +} + +void WeekScene::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *ev) +{ + const QList gis = items(ev->scenePos()); + for (QGraphicsItem *gi : gis) { + if (gi->zValue() == 3) { + itemDoubleClicked(gi); + break; + } + } +} + +void WeekScene::mousePressEvent(QGraphicsSceneMouseEvent *ev) +{ + if (ev->button() == Qt::RightButton) { + const QList gis = items(ev->scenePos()); + for (QGraphicsItem *gi : gis) { + if (gi->zValue() == 3) { + clearSelection(); + gi->setSelected(true); + break; + } + } + } else + QGraphicsScene::mousePressEvent(ev); +} + +qreal WeekScene::timeToY(const QTime &time) +{ + QTime midnight(0, 0, 0, 0); + qreal min_h = hour_height / 60.0; + return (midnight.secsTo(time) / 60.0) * min_h + yoff; +} + +QTime WeekScene::yToTime(qreal y) +{ + y = y - yoff; // get rid of offset + qreal min_h = hour_height / 60.0; + return QTime(0, 0, 0, 0).addSecs((y / min_h) * 60); +} + +void WeekScene::itemMoved(ScheduleItem *item, const QPointF &np) +{ + QTime start = yToTime(np.y()); + int d = item->start.secsTo(item->end); // duration in seconds + QTime end = start.addSecs(d); + + int start_day = 1 + floor((np.x() + day_width * 0.5 - xoff) / day_width); + if (start_day < 1) + start_day = 1; + else if (start_day > 7) + start_day = 7; + + int end_day = start_day + (item->end_day - item->start_day); + if (end_day < 1) + end_day = 1; + else if (end_day > 7) + end_day = 7; + itemMoved(item, start, end, start_day, end_day); +} + +bool WeekScene::validMove(ScheduleItem *item, const QPointF &np) +{ + if (!schedule) + return true; + + QTime start = yToTime(np.y()); + int d = item->start.secsTo(item->end); // duration in seconds + QTime end = start.addSecs(d); + + int start_day = 1 + floor((np.x() + day_width * 0.5 - xoff) / day_width); + int end_day = start_day + (item->end_day - item->start_day); + if (end_day > 7) + end_day = 7; + return schedule->validModify(item, start, end, start_day, end_day); +} + +void WeekScene::itemResized(ScheduleItem *item, const QRectF &r) +{ + QTime start = yToTime(r.y()); + QTime end = yToTime(r.y() + r.height()); + + int start_day = 1 + floor((r.x() + day_width * 0.5 - xoff) / day_width); + int end_day = 1 + floor((r.x() + r.width() - day_width * 0.5 - xoff) / day_width); + if (start_day < 1) + start_day = 1; + else if (start_day > 7) + start_day = 7; + + if (end_day < 1) + end_day = 1; + else if (end_day > 7) + end_day = 7; + + itemMoved(item, start, end, start_day, end_day); +} + +bool WeekScene::validResize(ScheduleItem *item, const QRectF &r) +{ + QTime start = yToTime(r.y()); + QTime end = yToTime(r.y() + r.height()); + return schedule->validModify(item, start, end, item->start_day, item->end_day); +} + +void WeekScene::itemChanged(ScheduleItem *item, QGraphicsItem *gi) +{ + ScheduleGraphicsItem *sgi = (ScheduleGraphicsItem *)gi; + qreal x = xoff + (item->start_day - 1) * day_width; + qreal y = timeToY(item->start); + qreal ye = timeToY(item->end); + sgi->update(QRectF(x, y, day_width * (item->end_day - item->start_day + 1), ye - y)); +} + +void WeekScene::colorsChanged() +{ + QPen pen(SchedulerPluginSettings::scheduleLineColor()); + QBrush brush(SchedulerPluginSettings::scheduleBackgroundColor()); + + for (QGraphicsLineItem *line : qAsConst(lines)) + line->setPen(pen); + + for (QGraphicsRectItem *rect : qAsConst(rects)) { + rect->setPen(pen); + rect->setBrush(brush); + } + + pen.setStyle(Qt::DashLine); + gline[0]->setPen(pen); + gline[1]->setPen(pen); +} + +void WeekScene::setShowGuidanceLines(bool on) +{ + gline[0]->setVisible(on); + gline[1]->setVisible(on); +} + +void WeekScene::updateGuidanceLines(qreal y1, qreal y2) +{ + const QString FORMAT = QStringLiteral("hh:mm"); + gline[0]->update(xoff, y1, yToTime(y1).toString(FORMAT)); + gline[1]->update(xoff, y2, yToTime(y2).toString(FORMAT)); +} +} diff --git a/plugins/bwscheduler/weekscene.h b/plugins/bwscheduler/weekscene.h new file mode 100644 index 0000000..362770d --- /dev/null +++ b/plugins/bwscheduler/weekscene.h @@ -0,0 +1,142 @@ +/* + SPDX-FileCopyrightText: 2007 Joris Guisson + SPDX-FileCopyrightText: 2007 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KTWEEKSCENE_H +#define KTWEEKSCENE_H + +#include + +namespace kt +{ +class Schedule; +struct ScheduleItem; +class GuidanceLine; + +/** + @author +*/ +class WeekScene : public QGraphicsScene +{ + Q_OBJECT +public: + WeekScene(QObject *parent); + ~WeekScene() override; + + /** + * Set the current Schedule + * @param s The current schedule + */ + void setSchedule(Schedule *s) + { + schedule = s; + } + + /** + * Add an item to the schedule. + * @param item The item to add + */ + QGraphicsItem *addScheduleItem(ScheduleItem *item); + + /** + * Update the text of the status line + * @param up Up speed + * @param down Down speed + * @param suspended Suspended or not + * @param enabled Enabled or not + */ + void updateStatusText(int up, int down, bool suspended, bool enabled); + + /** + * A schedule item has been moved by the user. + * @param item The item + * @param np New position + */ + void itemMoved(ScheduleItem *item, const QPointF &np); + + /** + * Is a move valid, does it conflict or not ? + * @param item The item + * @param np New position + */ + bool validMove(ScheduleItem *item, const QPointF &np); + + /** + * An item has been resized by the user. + * @param item The item + * @param r It's new rectangle + */ + void itemResized(ScheduleItem *item, const QRectF &r); + + /** + * Is a resize valid, does it conflict or not ? + * @param item The item + * @param np New position + */ + bool validResize(ScheduleItem *item, const QRectF &r); + + /** + * An item has changed, update it. + * @param item The item + * @param gi The GraphicsItem + */ + void itemChanged(ScheduleItem *item, QGraphicsItem *gi); + + /** + * The color settings have changed. + */ + void colorsChanged(); + + /** + * Show or the guidance lines + * @param on + */ + void setShowGuidanceLines(bool on); + + /** + * Show the guidance lines + * @param y1 Height of line 1 + * @param y2 Height of line 2 + */ + void updateGuidanceLines(qreal y1, qreal y2); + +Q_SIGNALS: + /** + * Emitted when an item has been double clicked. + * @param gi Item double clicked + */ + void itemDoubleClicked(QGraphicsItem *gi); + + /** + * An item has been moved + * @param item The item + * @param start The new start time + * @param end The new end time + * @param day The new day + */ + void itemMoved(ScheduleItem *item, const QTime &start, const QTime &end, int start_day, int end_day); + +private: + void addCalendar(); + void mouseDoubleClickEvent(QGraphicsSceneMouseEvent *ev) override; + void mousePressEvent(QGraphicsSceneMouseEvent *ev) override; + qreal timeToY(const QTime &time); + QTime yToTime(qreal y); + +private: + qreal xoff; + qreal yoff; + qreal day_width; + qreal hour_height; + QGraphicsTextItem *status; + QList lines; + QList rects; + GuidanceLine *gline[2]; // guidance lines + Schedule *schedule; +}; + +} + +#endif diff --git a/plugins/bwscheduler/weekview.cpp b/plugins/bwscheduler/weekview.cpp new file mode 100644 index 0000000..eced536 --- /dev/null +++ b/plugins/bwscheduler/weekview.cpp @@ -0,0 +1,135 @@ +/* + SPDX-FileCopyrightText: 2007 Joris Guisson + SPDX-FileCopyrightText: 2007 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "weekview.h" + +#include + +#include "schedule.h" +#include "weekscene.h" +#include +#include + +using namespace bt; + +namespace kt +{ +WeekView::WeekView(QWidget *parent) + : QGraphicsView(parent) + , schedule(nullptr) +{ + scene = new WeekScene(this); + setScene(scene); + + connect(scene, &WeekScene::selectionChanged, this, &WeekView::onSelectionChanged); + connect(scene, &WeekScene::itemDoubleClicked, this, &WeekView::onDoubleClicked); + connect(scene, qOverload(&WeekScene::itemMoved), this, &WeekView::itemMoved); + + menu = new QMenu(this); + setContextMenuPolicy(Qt::CustomContextMenu); + connect(this, &WeekView::customContextMenuRequested, this, &WeekView::showContextMenu); +} + +WeekView::~WeekView() +{ +} + +void WeekView::updateStatusText(int up, int down, bool suspended, bool enabled) +{ + scene->updateStatusText(up, down, suspended, enabled); +} + +void WeekView::onSelectionChanged() +{ + selection.clear(); + + const QList sel = scene->selectedItems(); + for (QGraphicsItem *s : sel) { + QMap::iterator i = item_map.find(s); + if (i != item_map.end()) + selection.append(i.value()); + } + + selectionChanged(); +} + +void WeekView::setSchedule(Schedule *s) +{ + clear(); + schedule = s; + + if (schedule) + s->apply(boost::bind(&WeekView::addScheduleItem, this, boost::placeholders::_1)); + + scene->setSchedule(s); +} + +void WeekView::clear() +{ + QMap::iterator i = item_map.begin(); + while (i != item_map.end()) { + QGraphicsItem *item = i.key(); + scene->removeItem(item); + delete item; + i++; + } + item_map.clear(); + selection.clear(); + schedule = nullptr; +} + +void WeekView::removeSelectedItems() +{ + const QList sel = scene->selectedItems(); + for (QGraphicsItem *s : sel) { + QMap::iterator i = item_map.find(s); + if (i != item_map.end()) { + ScheduleItem *si = i.value(); + scene->removeItem(s); + item_map.erase(i); + schedule->removeItem(si); + } + } +} + +void WeekView::addScheduleItem(ScheduleItem *item) +{ + QGraphicsItem *gi = scene->addScheduleItem(item); + + if (gi) + item_map[gi] = item; +} + +void WeekView::onDoubleClicked(QGraphicsItem *i) +{ + QMap::iterator itr = item_map.find(i); + if (itr != item_map.end()) + editItem(itr.value()); +} + +void WeekView::showContextMenu(const QPoint &pos) +{ + menu->popup(viewport()->mapToGlobal(pos)); +} + +void WeekView::itemChanged(ScheduleItem *item) +{ + QMap::iterator i = item_map.begin(); + while (i != item_map.end()) { + if (item == i.value()) { + QGraphicsItem *gi = i.key(); + scene->itemChanged(item, gi); + break; + } + i++; + } +} + +void WeekView::colorsChanged() +{ + scene->colorsChanged(); +} +} diff --git a/plugins/bwscheduler/weekview.h b/plugins/bwscheduler/weekview.h new file mode 100644 index 0000000..b0780b4 --- /dev/null +++ b/plugins/bwscheduler/weekview.h @@ -0,0 +1,107 @@ +/* + SPDX-FileCopyrightText: 2007 Joris Guisson + SPDX-FileCopyrightText: 2007 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KTWEEKVIEW_H +#define KTWEEKVIEW_H + +#include +#include +#include + +#include + +namespace kt +{ +struct ScheduleItem; +class Schedule; +class WeekScene; + +/** + Displays the schedule of one week. +*/ +class WeekView : public QGraphicsView +{ + Q_OBJECT +public: + WeekView(QWidget *parent); + ~WeekView() override; + + /** + * Set the current Schedule + * @param s The current schedule + */ + void setSchedule(Schedule *s); + + /** + * Clear the current Schedule. + */ + void clear(); + + /// Get the selected items + QList selectedItems() + { + return selection; + } + + /** + * Add an item to the schedule. + * @param item The item to add + */ + void addScheduleItem(ScheduleItem *item); + + /** + * Remove all selected items from the schedule. + */ + void removeSelectedItems(); + + /// Get the right click menu + QMenu *rightClickMenu() + { + return menu; + } + + /** + * Update the text of the status line + * @param up Up speed + * @param down Down speed + * @param suspended Suspended or not + * @param enabled Enabled or not + */ + void updateStatusText(int up, int down, bool suspended, bool enabled); + + /** + * Something has changed about an item + * @param item + */ + void itemChanged(ScheduleItem *item); + + /** + * The color settings have changed. + */ + void colorsChanged(); + +Q_SIGNALS: + void selectionChanged(); + void editItem(ScheduleItem *item); + void itemMoved(ScheduleItem *item, const QTime &start, const QTime &end, int start_day, int end_day); + +private Q_SLOTS: + void onSelectionChanged(); + void showContextMenu(const QPoint &pos); + void onDoubleClicked(QGraphicsItem *i); + +private: + WeekScene *scene; + + Schedule *schedule; + QMap item_map; + QList selection; + QMenu *menu; +}; + +} + +#endif diff --git a/plugins/downloadorder/CMakeLists.txt b/plugins/downloadorder/CMakeLists.txt new file mode 100644 index 0000000..1cb62bc --- /dev/null +++ b/plugins/downloadorder/CMakeLists.txt @@ -0,0 +1,24 @@ +add_library(ktorrent_downloadorder MODULE) + +target_sources(ktorrent_downloadorder PRIVATE + downloadorderplugin.cpp + downloadorderdialog.cpp + downloadordermanager.cpp + downloadordermodel.cpp +) + +ki18n_wrap_ui(ktorrent_downloadorder downloadorderwidget.ui) + +kcoreaddons_desktop_to_json(ktorrent_downloadorder ktorrent_downloadorder.desktop) + +target_link_libraries( + ktorrent_downloadorder + ktcore + KF5::Torrent + KF5::ConfigCore + KF5::CoreAddons + KF5::I18n + KF5::XmlGui +) +install(TARGETS ktorrent_downloadorder DESTINATION ${KTORRENT_PLUGIN_INSTALL_DIR} ) +install(FILES ktorrent_downloadorderui.rc DESTINATION ${KXMLGUI_INSTALL_DIR}/ktorrent ) diff --git a/plugins/downloadorder/downloadorderdialog.cpp b/plugins/downloadorder/downloadorderdialog.cpp new file mode 100644 index 0000000..46ff741 --- /dev/null +++ b/plugins/downloadorder/downloadorderdialog.cpp @@ -0,0 +1,186 @@ +/* + SPDX-FileCopyrightText: 2008 Joris Guisson + SPDX-FileCopyrightText: 2008 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "downloadorderdialog.h" + +#include +#include + +#include + +#include "downloadordermanager.h" +#include "downloadordermodel.h" +#include "downloadorderplugin.h" +#include + +namespace kt +{ +DownloadOrderDialog::DownloadOrderDialog(DownloadOrderPlugin *plugin, bt::TorrentInterface *tor, QWidget *parent) + : QDialog(parent) + , tor(tor) + , plugin(plugin) +{ + setupUi(this); + connect(buttonBox, &QDialogButtonBox::accepted, this, &DownloadOrderDialog::accept); + connect(buttonBox, &QDialogButtonBox::rejected, this, &DownloadOrderDialog::reject); + connect(this, &DownloadOrderDialog::accepted, this, &DownloadOrderDialog::commitDownloadOrder); + setWindowTitle(i18n("File Download Order")); + m_top_label->setText(i18n("File download order for %1:", tor->getDisplayName())); + + DownloadOrderManager *dom = plugin->manager(tor); + m_custom_order_enabled->setChecked(dom != nullptr); + m_order->setEnabled(dom != nullptr); + m_move_up->setEnabled(false); + m_move_down->setEnabled(false); + m_move_top->setEnabled(false); + m_move_bottom->setEnabled(false); + m_search_files->setEnabled(false); + + m_move_up->setIcon(QIcon::fromTheme(QStringLiteral("go-up"))); + connect(m_move_up, &QPushButton::clicked, this, &DownloadOrderDialog::moveUp); + m_move_down->setIcon(QIcon::fromTheme(QStringLiteral("go-down"))); + connect(m_move_down, &QPushButton::clicked, this, &DownloadOrderDialog::moveDown); + m_move_top->setIcon(QIcon::fromTheme(QStringLiteral("go-top"))); + connect(m_move_top, &QPushButton::clicked, this, &DownloadOrderDialog::moveTop); + m_move_bottom->setIcon(QIcon::fromTheme(QStringLiteral("go-bottom"))); + connect(m_move_bottom, &QPushButton::clicked, this, &DownloadOrderDialog::moveBottom); + + m_order->setSelectionMode(QAbstractItemView::ContiguousSelection); + m_order->setDragEnabled(true); + m_order->setAcceptDrops(true); + m_order->setDropIndicatorShown(true); + m_order->setDragDropMode(QAbstractItemView::InternalMove); + + model = new DownloadOrderModel(tor, this); + if (dom) + model->initOrder(dom->downloadOrder()); + m_order->setModel(model); + + QSize s = KSharedConfig::openConfig()->group("DownloadOrderDialog").readEntry("size", size()); + resize(s); + + connect(m_order->selectionModel(), &QItemSelectionModel::selectionChanged, this, &DownloadOrderDialog::itemSelectionChanged); + connect(m_custom_order_enabled, &QCheckBox::toggled, this, &DownloadOrderDialog::customOrderEnableToggled); + connect(m_search_files, &QLineEdit::textChanged, this, &DownloadOrderDialog::search); + + QMenu *sort_by_menu = new QMenu(m_sort_by); + sort_by_menu->addAction(i18n("Name"), model, &DownloadOrderModel::sortByName); + sort_by_menu->addAction(i18n("Seasons and Episodes"), model, &DownloadOrderModel::sortBySeasonsAndEpisodes); + sort_by_menu->addAction(i18n("Album Track Order"), model, &DownloadOrderModel::sortByAlbumTrackOrder); + m_sort_by->setMenu(sort_by_menu); + m_sort_by->setPopupMode(QToolButton::InstantPopup); + m_sort_by->setEnabled(false); +} + +DownloadOrderDialog::~DownloadOrderDialog() +{ + KSharedConfig::openConfig()->group("DownloadOrderDialog").writeEntry("size", size()); +} + +void DownloadOrderDialog::commitDownloadOrder() +{ + if (m_custom_order_enabled->isChecked()) { + DownloadOrderManager *dom = plugin->manager(tor); + if (!dom) { + dom = plugin->createManager(tor); + connect(tor, &bt::TorrentInterface::chunkDownloaded, dom, &DownloadOrderManager::chunkDownloaded); + } + + dom->setDownloadOrder(model->downloadOrder()); + dom->save(); + dom->update(); + } else { + DownloadOrderManager *dom = plugin->manager(tor); + if (dom) { + dom->disable(); + plugin->destroyManager(tor); + } + } +} + +void DownloadOrderDialog::moveUp() +{ + QModelIndexList idx = m_order->selectionModel()->selectedRows(); + model->moveUp(idx.front().row(), idx.count()); + if (idx.front().row() > 0) { + QItemSelection sel(model->index(idx.first().row() - 1), model->index(idx.last().row() - 1)); + m_order->selectionModel()->select(sel, QItemSelectionModel::ClearAndSelect); + } +} + +void DownloadOrderDialog::moveTop() +{ + QModelIndexList idx = m_order->selectionModel()->selectedRows(); + model->moveTop(idx.front().row(), idx.count()); + if (idx.front().row() > 0) { + QItemSelection sel(model->index(0), model->index(idx.count() - 1)); + m_order->selectionModel()->select(sel, QItemSelectionModel::ClearAndSelect); + } +} + +void DownloadOrderDialog::moveDown() +{ + QModelIndexList idx = m_order->selectionModel()->selectedRows(); + model->moveDown(idx.front().row(), idx.count()); + if (idx.back().row() < (int)tor->getNumFiles() - 1) { + QItemSelection sel(model->index(idx.first().row() + 1), model->index(idx.last().row() + 1)); + m_order->selectionModel()->select(sel, QItemSelectionModel::ClearAndSelect); + } +} + +void DownloadOrderDialog::moveBottom() +{ + QModelIndexList idx = m_order->selectionModel()->selectedRows(); + model->moveBottom(idx.front().row(), idx.count()); + if (idx.back().row() < (int)tor->getNumFiles() - 1) { + QItemSelection sel(model->index(tor->getNumFiles() - idx.size()), model->index(tor->getNumFiles() - 1)); + m_order->selectionModel()->select(sel, QItemSelectionModel::ClearAndSelect); + } +} + +void DownloadOrderDialog::itemSelectionChanged(const QItemSelection &new_sel, const QItemSelection &old_sel) +{ + Q_UNUSED(old_sel); + if (new_sel.empty()) { + m_move_down->setEnabled(false); + m_move_up->setEnabled(false); + m_move_top->setEnabled(false); + m_move_down->setEnabled(false); + } else { + bool up_ok = new_sel.front().topLeft().row() > 0; + bool down_ok = new_sel.back().bottomRight().row() != (int)tor->getNumFiles() - 1; + m_move_up->setEnabled(up_ok); + m_move_top->setEnabled(up_ok); + m_move_down->setEnabled(down_ok); + m_move_bottom->setEnabled(down_ok); + } +} + +void DownloadOrderDialog::customOrderEnableToggled(bool on) +{ + m_search_files->setEnabled(on); + m_sort_by->setEnabled(on); + if (!on) { + m_move_down->setEnabled(false); + m_move_up->setEnabled(false); + m_move_top->setEnabled(false); + m_move_down->setEnabled(false); + } else { + itemSelectionChanged(m_order->selectionModel()->selection(), QItemSelection()); + } +} + +void DownloadOrderDialog::search(const QString &text) +{ + if (text.isEmpty()) { + model->clearHighLights(); + } else { + QModelIndex idx = model->find(text); + if (idx.isValid()) + m_order->scrollTo(idx); + } +} +} diff --git a/plugins/downloadorder/downloadorderdialog.h b/plugins/downloadorder/downloadorderdialog.h new file mode 100644 index 0000000..7ee34e5 --- /dev/null +++ b/plugins/downloadorder/downloadorderdialog.h @@ -0,0 +1,51 @@ +/* + SPDX-FileCopyrightText: 2008 Joris Guisson + SPDX-FileCopyrightText: 2008 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KTDOWNLOADORDERDIALOG_H +#define KTDOWNLOADORDERDIALOG_H + +#include "ui_downloadorderwidget.h" +#include + +namespace bt +{ +class TorrentInterface; +} + +namespace kt +{ +class DownloadOrderPlugin; +class DownloadOrderModel; + +/** + Dialog to manipulate the download order. +*/ +class DownloadOrderDialog : public QDialog, public Ui_DownloadOrderWidget +{ + Q_OBJECT +public: + DownloadOrderDialog(DownloadOrderPlugin *plugin, bt::TorrentInterface *tor, QWidget *parent); + ~DownloadOrderDialog() override; + +private Q_SLOTS: + void commitDownloadOrder(); + void moveUp(); + void moveDown(); + void moveTop(); + void moveBottom(); + void itemSelectionChanged(const QItemSelection &new_sel, const QItemSelection &old_sel); + void customOrderEnableToggled(bool on); + void search(const QString &text); + +private: + bt::TorrentInterface *tor; + DownloadOrderPlugin *plugin; + DownloadOrderModel *model; +}; + +} + +#endif diff --git a/plugins/downloadorder/downloadordermanager.cpp b/plugins/downloadorder/downloadordermanager.cpp new file mode 100644 index 0000000..a8105d8 --- /dev/null +++ b/plugins/downloadorder/downloadordermanager.cpp @@ -0,0 +1,162 @@ +/* + SPDX-FileCopyrightText: 2008 Joris Guisson + SPDX-FileCopyrightText: 2008 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include + +#include +#include + +#include "downloadordermanager.h" +#include +#include +#include +#include + +using namespace bt; + +namespace kt +{ +DownloadOrderManager::DownloadOrderManager(bt::TorrentInterface *tor) + : tor(tor) +{ + current_normal_priority_file = current_high_priority_file = tor->getNumFiles(); +} + +DownloadOrderManager::~DownloadOrderManager() +{ +} + +void DownloadOrderManager::save() +{ + if (!enabled()) + return; + + QFile fptr(tor->getTorDir() + QStringLiteral("download_order")); + if (!fptr.open(QIODevice::WriteOnly)) { + Out(SYS_DIO | LOG_IMPORTANT) << "Cannot open download_order file of " << tor->getDisplayName() << " : " << fptr.errorString() << endl; + return; + } + + QTextStream out(&fptr); + for (Uint32 file : qAsConst(order)) + out << file << Qt::endl; +} + +void DownloadOrderManager::load() +{ + if (!bt::Exists(tor->getTorDir() + QStringLiteral("download_order"))) + return; + + QFile fptr(tor->getTorDir() + QStringLiteral("download_order")); + if (!fptr.open(QIODevice::ReadOnly)) { + Out(SYS_DIO | LOG_NOTICE) << "Cannot open download_order file of " << tor->getDisplayName() << " : " << fptr.errorString() << endl; + return; + } + + QTextStream in(&fptr); + while (!in.atEnd()) { + QString file = in.readLine(); + bool ok = false; + Uint32 idx = file.toUInt(&ok); + if (ok && idx < tor->getNumFiles()) + order.append(idx); + } + + // make sure all files are in the order + for (Uint32 i = 0; i < tor->getNumFiles(); i++) + if (!order.contains(i)) + order.append(i); +} + +Uint32 DownloadOrderManager::nextIncompleteFile() +{ + // Look for the next file in the order which is not 100 % complete + for (Uint32 file : qAsConst(order)) { + // skip file if it is complete + if (std::fabs(100.0f - tor->getTorrentFile(file).getDownloadPercentage()) < 0.01) + continue; + + // skip excluded or only seed files + if (tor->getTorrentFile(file).getPriority() < LAST_PRIORITY) + continue; + + // we have found the incomplete file + return file; + } + return tor->getNumFiles(); +} + +void DownloadOrderManager::chunkDownloaded(bt::TorrentInterface *me, Uint32 chunk) +{ + if (!enabled() || tor->getStats().completed || tor != me) + return; + + bt::TorrentFileInterface &high_priority_file = tor->getTorrentFile(current_high_priority_file); + bool in_high_priority_file_range = chunk >= high_priority_file.getFirstChunk() && chunk <= high_priority_file.getLastChunk(); + bt::TorrentFileInterface &normal_priority_file = tor->getTorrentFile(current_normal_priority_file); + bool in_normal_priority_file_range = chunk >= normal_priority_file.getFirstChunk() && chunk <= normal_priority_file.getLastChunk(); + if (in_high_priority_file_range || in_normal_priority_file_range) { + // Check if high or normal are complete + if (std::fabs(100.0f - high_priority_file.getDownloadPercentage()) < 0.01 || std::fabs(100.0f - normal_priority_file.getDownloadPercentage()) < 0.01) { + update(); + } + } +} + +void DownloadOrderManager::update() +{ + if (!enabled() || tor->getStats().completed) + return; + + Uint32 next_file = nextIncompleteFile(); + if (next_file >= tor->getNumFiles()) + return; + + if (next_file != current_high_priority_file) + Out(SYS_DIO | LOG_NOTICE) << "DownloadOrderPlugin: next file to download is " << tor->getTorrentFile(next_file).getUserModifiedPath() << endl; + + bool normal_found = false; + bool high_found = false; + // set the priority of the file to FIRST and all the other files to NORMAL + for (Uint32 file : qAsConst(order)) { + TorrentFileInterface &tf = tor->getTorrentFile(file); + if (tf.getPriority() < LAST_PRIORITY) + continue; + + if (file == next_file) { + tf.setPriority(FIRST_PRIORITY); + high_found = true; + } else if (!normal_found && high_found) { + // the file after the high prio file is set to normal + // so that when the high prio file is finished the selector + // will select it before we can set a new high prio file + tf.setPriority(NORMAL_PRIORITY); + normal_found = true; + current_normal_priority_file = file; + } else + tf.setPriority(LAST_PRIORITY); + } + current_high_priority_file = next_file; +} + +void DownloadOrderManager::enable() +{ + if (enabled()) + return; + + for (Uint32 i = 0; i < tor->getNumFiles(); i++) { + order.append(i); + } +} + +void DownloadOrderManager::disable() +{ + order.clear(); + if (bt::Exists(tor->getTorDir() + QStringLiteral("download_order"))) + bt::Delete(tor->getTorDir() + QStringLiteral("download_order"), true); +} + +} diff --git a/plugins/downloadorder/downloadordermanager.h b/plugins/downloadorder/downloadordermanager.h new file mode 100644 index 0000000..5f1804a --- /dev/null +++ b/plugins/downloadorder/downloadordermanager.h @@ -0,0 +1,86 @@ +/* + SPDX-FileCopyrightText: 2008 Joris Guisson + SPDX-FileCopyrightText: 2008 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KTDOWNLOADORDERMANAGER_H +#define KTDOWNLOADORDERMANAGER_H + +#include +#include +#include + +namespace bt +{ +class TorrentInterface; +} + +namespace kt +{ +/** + Class which manages the file download order for a single torrent +*/ +class DownloadOrderManager : public QObject +{ + Q_OBJECT +public: + DownloadOrderManager(bt::TorrentInterface *tor); + ~DownloadOrderManager() override; + + /// See if the file download order is enabled + bool enabled() const + { + return order.count() > 0; + } + + /// Save the order from torX/download_order + void save(); + + /// Load the order to torX/download_order + void load(); + + /// Enable the download order + void enable(); + + /// Disable the download order + void disable(); + + typedef QList Order; + + /// Get the download order + const Order &downloadOrder() const + { + return order; + } + + /// Set the order + void setDownloadOrder(const Order &norder) + { + order = norder; + } + +public Q_SLOTS: + /** + * Change file priorities if needed + */ + void update(); + + /** + * Change file priorities if needed + */ + void chunkDownloaded(bt::TorrentInterface *me, bt::Uint32 chunk); + +private: + bt::Uint32 nextIncompleteFile(); + +private: + bt::TorrentInterface *tor; + QList order; + bt::Uint32 current_high_priority_file; + bt::Uint32 current_normal_priority_file; +}; + +} + +#endif diff --git a/plugins/downloadorder/downloadordermodel.cpp b/plugins/downloadorder/downloadordermodel.cpp new file mode 100644 index 0000000..b39f6b3 --- /dev/null +++ b/plugins/downloadorder/downloadordermodel.cpp @@ -0,0 +1,366 @@ +/* + SPDX-FileCopyrightText: 2008 Joris Guisson + SPDX-FileCopyrightText: 2008 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "downloadordermodel.h" +#include +#include +#include + +using namespace bt; + +namespace kt +{ +DownloadOrderModel::DownloadOrderModel(bt::TorrentInterface *tor, QObject *parent) + : QAbstractListModel(parent) + , tor(tor) +{ + for (Uint32 i = 0; i < tor->getNumFiles(); i++) { + order.append(i); + } +} + +DownloadOrderModel::~DownloadOrderModel() +{ +} + +int DownloadOrderModel::rowCount(const QModelIndex &parent) const +{ + if (!parent.isValid()) + return tor->getNumFiles(); + else + return 0; +} + +QVariant DownloadOrderModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) + return QVariant(); + + Uint32 idx = order.at(index.row()); + if (idx >= tor->getNumFiles()) + return QVariant(); + + switch (role) { + case Qt::DisplayRole: + return tor->getTorrentFile(idx).getUserModifiedPath(); + case Qt::DecorationRole: + return QIcon::fromTheme(QMimeDatabase().mimeTypeForFile(tor->getTorrentFile(idx).getPath()).iconName()); + case Qt::FontRole: + if (!current_search_text.isEmpty() && tor->getTorrentFile(idx).getUserModifiedPath().contains(current_search_text, Qt::CaseInsensitive)) { + QFont font = QApplication::font(); + font.setBold(true); + return font; + } + Q_FALLTHROUGH(); + default: + return QVariant(); + } +} + +QModelIndex DownloadOrderModel::find(const QString &text) +{ + beginResetModel(); + current_search_text = text; + for (Uint32 i = 0; i < tor->getNumFiles(); i++) { + if (tor->getTorrentFile(i).getUserModifiedPath().contains(current_search_text, Qt::CaseInsensitive)) { + endResetModel(); + return index(i); + } + } + + endResetModel(); + return QModelIndex(); +} + +void DownloadOrderModel::clearHighLights() +{ + beginResetModel(); + current_search_text.clear(); + endResetModel(); +} + +Qt::ItemFlags DownloadOrderModel::flags(const QModelIndex &index) const +{ + Qt::ItemFlags defaultFlags = QAbstractListModel::flags(index); + + if (index.isValid()) + return Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled | defaultFlags; + else + return Qt::ItemIsDropEnabled | defaultFlags; +} + +Qt::DropActions DownloadOrderModel::supportedDropActions() const +{ + return Qt::CopyAction | Qt::MoveAction; +} + +QStringList DownloadOrderModel::mimeTypes() const +{ + QStringList types; + types << QStringLiteral("application/octet-stream"); + return types; +} + +QMimeData *DownloadOrderModel::mimeData(const QModelIndexList &indexes) const +{ + QMimeData *mimeData = new QMimeData(); + QByteArray data; + QDataStream out(&data, QIODevice::WriteOnly); + QList files; + + for (const QModelIndex &index : indexes) { + if (index.isValid()) { + files.append(order.at(index.row())); + } + } + out << files; + mimeData->setData(QStringLiteral("application/octet-stream"), data); + return mimeData; +} + +bool DownloadOrderModel::dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) +{ + Q_UNUSED(column); + if (action == Qt::IgnoreAction) + return true; + + if (!data->hasFormat(QStringLiteral("application/octet-stream"))) + return false; + + int begin_row; + if (row != -1) + begin_row = row; + else if (parent.isValid()) + begin_row = parent.row(); + else + begin_row = rowCount(QModelIndex()); + + QByteArray file_data = data->data(QStringLiteral("application/octet-stream")); + QDataStream in(&file_data, QIODevice::ReadOnly); + QList files; + in >> files; + + // remove all files from order which are in the dragged list + int r = 0; + for (QList::iterator i = order.begin(); i != order.end();) { + if (files.contains(*i)) { + if (r < begin_row) // if we remove something before the begin row, the row to insert decreases + begin_row--; + + i = order.erase(i); + } else + i++; + + r++; + } + + // reinsert dragged files + for (Uint32 file : qAsConst(files)) { + order.insert(begin_row, file); + begin_row++; + } + return true; +} + +void DownloadOrderModel::moveUp(int row, int count) +{ + if (row == 0) + return; + + for (int i = 0; i < count; i++) { + order.swapItemsAt(row + i, row + i - 1); + } + + Q_EMIT dataChanged(createIndex(row - 1, 0), createIndex(row + count, 0)); +} + +void DownloadOrderModel::moveTop(int row, int count) +{ + if (row == 0) + return; + + QList tmp; + for (int i = 0; i < count; i++) { + tmp.append(order.takeAt(row)); + } + + beginResetModel(); + order = tmp + order; + endResetModel(); +} + +void DownloadOrderModel::moveDown(int row, int count) +{ + if (row + count >= (int)tor->getNumFiles()) + return; + + for (int i = count - 1; i >= 0; i--) { + order.swapItemsAt(row + i, row + i + 1); + } + + Q_EMIT dataChanged(createIndex(row, 0), createIndex(row + count + 1, 0)); +} + +void DownloadOrderModel::moveBottom(int row, int count) +{ + if (row + count >= (int)tor->getNumFiles()) + return; + + QList tmp; + for (int i = 0; i < count; i++) { + tmp.append(order.takeAt(row)); + } + + beginResetModel(); + order = order + tmp; + endResetModel(); +} + +struct NameCompare { + NameCompare(bt::TorrentInterface *tor) + : tor(tor) + { + } + + bool operator()(Uint32 a, Uint32 b) + { + return tor->getTorrentFile(a).getUserModifiedPath() < tor->getTorrentFile(b).getUserModifiedPath(); + } + + bt::TorrentInterface *tor; +}; + +void DownloadOrderModel::sortByName() +{ + beginResetModel(); + std::sort(order.begin(), order.end(), NameCompare(tor)); + endResetModel(); +} + +struct AlbumTrackCompare { + AlbumTrackCompare(bt::TorrentInterface *tor) + : tor(tor) + { + } + + int getTrack(const QString &title) + { + QRegExp exp(QLatin1String(".*(\\d+)\\s.*\\.\\w*"), Qt::CaseInsensitive); + int pos = exp.indexIn(title); + if (pos > -1) { + QString track = exp.cap(1); + bool ok = false; + int track_number = track.toInt(&ok); + if (ok) + return track_number; + } + + return -1; + } + + bool operator()(Uint32 a, Uint32 b) + { + QString a_path = tor->getTorrentFile(a).getUserModifiedPath(); + QString b_path = tor->getTorrentFile(b).getUserModifiedPath(); + + int ta = getTrack(a_path); + int tb = getTrack(b_path); + if (ta < 0 && tb < 0) + return a_path < b_path; + else if (ta < 0) + return false; + else if (tb < 0) + return true; + else + return ta < tb; + } + + bt::TorrentInterface *tor; +}; + +void DownloadOrderModel::sortByAlbumTrackOrder() +{ + beginResetModel(); + std::sort(order.begin(), order.end(), AlbumTrackCompare(tor)); + endResetModel(); +} + +struct SeasonEpisodeCompare { + SeasonEpisodeCompare(bt::TorrentInterface *tor) + : tor(tor) + { + } + + bool getSeasonAndEpisode(const QString &title, int &season, int &episode) + { + QStringList se_formats; + se_formats << QStringLiteral("(\\d+)x(\\d+)") << QStringLiteral("S(\\d+)E(\\d+)") << QStringLiteral("(\\d+)\\.(\\d+)") + << QStringLiteral("S(\\d+)\\.E(\\d+)") << QStringLiteral("Season\\s(\\d+).*Episode\\s(\\d+)"); + + for (const QString &format : qAsConst(se_formats)) { + QRegExp exp(format, Qt::CaseInsensitive); + int pos = exp.indexIn(title); + if (pos > -1) { + QString s = exp.cap(1); // Season + QString e = exp.cap(2); // Episode + bool ok = false; + season = s.toInt(&ok); + if (!ok) + continue; + + episode = e.toInt(&ok); + if (!ok) + continue; + + return true; + } + } + + return false; + } + + bool operator()(Uint32 a, Uint32 b) + { + QString a_path = tor->getTorrentFile(a).getUserModifiedPath(); + QString b_path = tor->getTorrentFile(b).getUserModifiedPath(); + int a_season = 0, a_episode = 0; + int b_season = 0, b_episode = 0; + bool a_has_se = getSeasonAndEpisode(a_path, a_season, a_episode); + bool b_has_se = getSeasonAndEpisode(b_path, b_season, b_episode); + if (a_has_se && b_has_se) { + if (a_season == b_season) + return a_episode < b_episode; + else + return a_season < b_season; + } else if (a_has_se && !b_has_se) { + return true; + } else if (!a_has_se && b_has_se) { + return false; + } else { + return a_path < b_path; + } + } + + bt::TorrentInterface *tor; +}; + +void DownloadOrderModel::sortBySeasonsAndEpisodes() +{ + beginResetModel(); + std::sort(order.begin(), order.end(), SeasonEpisodeCompare(tor)); + endResetModel(); +} +} diff --git a/plugins/downloadorder/downloadordermodel.h b/plugins/downloadorder/downloadordermodel.h new file mode 100644 index 0000000..dd0e492 --- /dev/null +++ b/plugins/downloadorder/downloadordermodel.h @@ -0,0 +1,74 @@ +/* + SPDX-FileCopyrightText: 2008 Joris Guisson + SPDX-FileCopyrightText: 2008 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KTDOWNLOADORDERMODEL_H +#define KTDOWNLOADORDERMODEL_H + +#include +#include + +namespace bt +{ +class TorrentInterface; +} + +namespace kt +{ +/** + Model for the download order in the dialog +*/ +class DownloadOrderModel : public QAbstractListModel +{ + Q_OBJECT +public: + DownloadOrderModel(bt::TorrentInterface *tor, QObject *parent); + ~DownloadOrderModel() override; + + /// Initialize the order + void initOrder(const QList &sl) + { + order = sl; + } + + /// Get the order + const QList &downloadOrder() const + { + return order; + } + + /// Find a text in the file list + QModelIndex find(const QString &text); + + /// Clear high lights + void clearHighLights(); + + int rowCount(const QModelIndex &parent) const override; + QVariant data(const QModelIndex &index, int role) const override; + Qt::ItemFlags flags(const QModelIndex &index) const override; + Qt::DropActions supportedDropActions() const override; + QStringList mimeTypes() const override; + QMimeData *mimeData(const QModelIndexList &indexes) const override; + bool dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) override; + + void moveUp(int row, int count); + void moveDown(int row, int count); + void moveTop(int row, int count); + void moveBottom(int row, int count); + +public Q_SLOTS: + void sortByName(); + void sortBySeasonsAndEpisodes(); + void sortByAlbumTrackOrder(); + +private: + bt::TorrentInterface *tor; + QList order; + QString current_search_text; +}; + +} + +#endif diff --git a/plugins/downloadorder/downloadorderplugin.cpp b/plugins/downloadorder/downloadorderplugin.cpp new file mode 100644 index 0000000..5c5e483 --- /dev/null +++ b/plugins/downloadorder/downloadorderplugin.cpp @@ -0,0 +1,122 @@ +/* + SPDX-FileCopyrightText: 2008 Joris Guisson + SPDX-FileCopyrightText: 2008 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include +#include +#include +#include + +#include "downloadorderdialog.h" +#include "downloadordermanager.h" +#include "downloadorderplugin.h" +#include +#include +#include +#include +#include +#include + +K_PLUGIN_FACTORY_WITH_JSON(ktorrent_downloadorder, "ktorrent_downloadorder.json", registerPlugin();) + +using namespace bt; + +namespace kt +{ +DownloadOrderPlugin::DownloadOrderPlugin(QObject *parent, const QVariantList &args) + : Plugin(parent) +{ + Q_UNUSED(args); + download_order_action = new QAction(QIcon::fromTheme(QStringLiteral("view-sort-ascending")), i18n("File Download Order"), this); + connect(download_order_action, &QAction::triggered, this, &DownloadOrderPlugin::showDownloadOrderDialog); + actionCollection()->addAction(QStringLiteral("download_order"), download_order_action); + setXMLFile(QStringLiteral("ktorrent_downloadorderui.rc")); + managers.setAutoDelete(true); +} + +DownloadOrderPlugin::~DownloadOrderPlugin() +{ +} + +bool DownloadOrderPlugin::versionCheck(const QString &version) const +{ + return version == QStringLiteral(VERSION); +} + +void DownloadOrderPlugin::load() +{ + TorrentActivityInterface *ta = getGUI()->getTorrentActivity(); + ta->addViewListener(this); + connect(getCore(), &CoreInterface::torrentAdded, this, &DownloadOrderPlugin::torrentAdded); + connect(getCore(), &CoreInterface::torrentRemoved, this, &DownloadOrderPlugin::torrentRemoved); + currentTorrentChanged(ta->getCurrentTorrent()); + + const kt::QueueManager *const qman = getCore()->getQueueManager(); + for (bt::TorrentInterface *i : *qman) + torrentAdded(i); +} + +void DownloadOrderPlugin::unload() +{ + TorrentActivityInterface *ta = getGUI()->getTorrentActivity(); + ta->removeViewListener(this); + disconnect(getCore(), &CoreInterface::torrentAdded, this, &DownloadOrderPlugin::torrentAdded); + disconnect(getCore(), &CoreInterface::torrentRemoved, this, &DownloadOrderPlugin::torrentRemoved); + managers.clear(); +} + +void DownloadOrderPlugin::showDownloadOrderDialog() +{ + bt::TorrentInterface *tor = getGUI()->getTorrentActivity()->getCurrentTorrent(); + if (!tor || !tor->getStats().multi_file_torrent) + return; + + DownloadOrderDialog dlg(this, tor, getGUI()->getMainWindow()); + dlg.exec(); +} + +void DownloadOrderPlugin::currentTorrentChanged(bt::TorrentInterface *tc) +{ + download_order_action->setEnabled(tc && tc->getStats().multi_file_torrent); +} + +DownloadOrderManager *DownloadOrderPlugin::manager(bt::TorrentInterface *tc) +{ + return managers.find(tc); +} + +DownloadOrderManager *DownloadOrderPlugin::createManager(bt::TorrentInterface *tc) +{ + DownloadOrderManager *m = manager(tc); + if (m) + return m; + + m = new DownloadOrderManager(tc); + managers.insert(tc, m); + return m; +} + +void DownloadOrderPlugin::destroyManager(bt::TorrentInterface *tc) +{ + managers.erase(tc); +} + +void DownloadOrderPlugin::torrentAdded(bt::TorrentInterface *tc) +{ + if (bt::Exists(tc->getTorDir() + QStringLiteral("download_order"))) { + DownloadOrderManager *m = createManager(tc); + m->load(); + m->update(); + connect(tc, &bt::TorrentInterface::chunkDownloaded, m, &DownloadOrderManager::chunkDownloaded); + } +} + +void DownloadOrderPlugin::torrentRemoved(bt::TorrentInterface *tc) +{ + managers.erase(tc); +} +} + +#include diff --git a/plugins/downloadorder/downloadorderplugin.h b/plugins/downloadorder/downloadorderplugin.h new file mode 100644 index 0000000..d4c0bd1 --- /dev/null +++ b/plugins/downloadorder/downloadorderplugin.h @@ -0,0 +1,57 @@ +/* + SPDX-FileCopyrightText: 2008 Joris Guisson + SPDX-FileCopyrightText: 2008 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#ifndef KTDOWNLOADORDERPLUGIN_H +#define KTDOWNLOADORDERPLUGIN_H + +#include +#include +#include + +namespace kt +{ +class DownloadOrderManager; + +/** + @author +*/ +class DownloadOrderPlugin : public Plugin, public ViewListener +{ + Q_OBJECT +public: + DownloadOrderPlugin(QObject *parent, const QVariantList &args); + ~DownloadOrderPlugin() override; + + bool versionCheck(const QString &version) const override; + void load() override; + void unload() override; + void currentTorrentChanged(bt::TorrentInterface *tc) override; + QString parentPart() const override + { + return QStringLiteral("torrentactivity"); + } + + /// Get the download order manager for a torrent (returns 0 if none exists) + DownloadOrderManager *manager(bt::TorrentInterface *tc); + + /// Create a manager for a torrent + DownloadOrderManager *createManager(bt::TorrentInterface *tc); + + /// Destroy a manager + void destroyManager(bt::TorrentInterface *tc); + +private Q_SLOTS: + void showDownloadOrderDialog(); + void torrentAdded(bt::TorrentInterface *tc); + void torrentRemoved(bt::TorrentInterface *tc); + +private: + QAction *download_order_action; + bt::PtrMap managers; +}; + +} + +#endif diff --git a/plugins/downloadorder/downloadorderwidget.ui b/plugins/downloadorder/downloadorderwidget.ui new file mode 100644 index 0000000..f90a7c0 --- /dev/null +++ b/plugins/downloadorder/downloadorderwidget.ui @@ -0,0 +1,152 @@ + + + DownloadOrderWidget + + + + 0 + 0 + 623 + 517 + + + + File Download Order + + + + + + File download order for: + + + + + + + Whether or not to enable a custom download order. + + + Custom file download order enabled + + + + + + + + + Qt::DefaultContextMenu + + + Sort By + + + Qt::ToolButtonTextBesideIcon + + + + + + + Search files + + + true + + + + + + + + + + + Order in which to download the files of a torrent. The file at the top will be downloaded first, followed by the second, then the third ... + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + Qt::Horizontal + + + + + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + m_custom_order_enabled + toggled(bool) + m_order + setEnabled(bool) + + + 141 + 33 + + + 166 + 113 + + + + + diff --git a/plugins/downloadorder/ktorrent_downloadorder.desktop b/plugins/downloadorder/ktorrent_downloadorder.desktop new file mode 100644 index 0000000..7908828 --- /dev/null +++ b/plugins/downloadorder/ktorrent_downloadorder.desktop @@ -0,0 +1,111 @@ +[Desktop Entry] +Name=Download Order +Name[ar]=ترتيب التّنزيل +Name[ast]=Orde de descarga +Name[bg]=Ред на изтегляне +Name[bs]=Redoslijed preuzimanja +Name[ca]=Ordre de baixada +Name[ca@valencia]=Ordre de baixada +Name[cs]=Pořadí stahování +Name[da]=Downloadrækkefølge +Name[de]=Download-Reihenfolge +Name[el]=Σειρά λήψης +Name[en_GB]=Download Order +Name[es]=Orden de descarga +Name[et]=Allalaadimise järjekord +Name[fi]=Latausjärjestys +Name[fr]=Ordre de téléchargement +Name[ga]=Ord Íosluchtaithe +Name[gl]=Orde de descarga +Name[hu]=Letöltési sorrend +Name[ia]=Ordine de discargar +Name[is]=Uppröðun niðurhals +Name[it]=Ordine di scaricamento +Name[ja]=ダウンロード順序 +Name[kk]=Жүктеп алу реті +Name[km]=លំដាប់​ទាញយក +Name[ko]=다운로드 순서 +Name[lt]=Atsiuntimo tvarka +Name[lv]=Lejupielādes kārtÄ«ba +Name[mr]=डाउनलोड क्रम +Name[nb]=Nedlastingsrekkefølge +Name[nds]=Daallaadreeg +Name[nl]=Bestandsdownload-volgorde +Name[nn]=Nedlastingsrekkjefølgje +Name[pl]=Kolejność pobierania +Name[pt]=Ordem de Transferência +Name[pt_BR]=Ordem de download +Name[ro]=Ordinea descărcării +Name[ru]=Порядок загрузки +Name[si]=බාගැනීම් පෙළගැස්ම +Name[sk]=Poradie sÅ¥ahovania +Name[sl]=Vrstni red prejemanja +Name[sq]=Rendi i Shkarkimit +Name[sr]=Редослед преузимања +Name[sr@ijekavian]=Редослијед преузимања +Name[sr@ijekavianlatin]=Redoslijed preuzimanja +Name[sr@latin]=Redosled preuzimanja +Name[sv]=Nerladdningsordning +Name[tr]=İndirme Sırası +Name[uk]=Порядок отримання +Name[x-test]=xxDownload Orderxx +Name[zh_CN]=下载顺序 +Name[zh_TW]=下載順序 +Comment=Specify the download order of a multi-file torrent +Comment[ar]=حدّد ترتيب التّنزيل لسيل فيه عدّة ملفّات +Comment[bs]=Specificirajte redoslijed preuzimanja viÅ¡edatotečnog torenta +Comment[ca]=Especifica l'ordre de baixada d'un torrent amb múltiples fitxers +Comment[ca@valencia]=Especifica l'ordre de baixada d'un torrent amb múltiples fitxers +Comment[cs]=Zvolení pořadí stahovaných souborů u torrentů obsahujících více souborů +Comment[da]=Angiv downloadrækkefølge for en torrent med flere filer +Comment[de]=Download-Reihenfolge bei Torrents festlegen, die aus mehreren Dateien bestehen +Comment[el]=Καθορισμός της σειράς λήψης ενός torrent πολλαπλών αρχείων +Comment[en_GB]=Specify the download order of a multi-file torrent +Comment[es]=Indique el orden de descarga de un torrent de varios archivos +Comment[et]=Mitmest failist koosneva torrenti failide allalaadimine kasutaja määratud järjekorras +Comment[fi]=Määritä torrentin tiedostojen latausjärjestys +Comment[fr]=Spécifie l'ordre de téléchargement d'un torrent multi-fichiers +Comment[ga]=Sonraigh ord íosluchtaithe do thorrent il-chomhad +Comment[gl]=Indicar a orde de descarga dos ficheiros dun torrente. +Comment[hu]=Többfájlos torrentek letöltési sorrendjének megadása +Comment[ia]=Specifica le ordine de discargar de un torrent multi-file +Comment[is]=Tilgreindu í hvaða röð margskráa straumi er halað inn +Comment[it]=Specifica l'ordine di scaricamento di un torrent multi-file +Comment[ja]=マルチファイル torrent のファイルのダウンロード順序を指定します +Comment[kk]=Көп-файлды торрент ретін келтіру +Comment[km]=បញ្ជាក់​លំដាប់​ទាញ​យក​របស់ torrent ទាញ​យក​ឯកសារ​ច្រើន +Comment[ko]=여러 파일이 있는 토렌트의 다운로드 순서 결정 +Comment[lt]=Nustatyti atsiuntimo tvarką iÅ¡ torrent multi-failo +Comment[lv]=Norāda failu lejupielādes kārtÄ«bu torrentā +Comment[nb]=Velg hvilke filer som skal lastes ned først i strømmer med flere filer +Comment[nds]=Daallaadreeg bi en Mehrdateien-Torrent fastleggen +Comment[nl]=Specificeer de volgorde voor het downloaden van een multi-bestands-torrent +Comment[nn]=Oppgje nedlastingsrekkjefølgja i ein torrent som inneheld fleire filer +Comment[pl]=Ustalanie kolejności pobierania plików w torrentach wieloplikowych +Comment[pt]=Um 'plugin' para indicar a ordem de transferência de uma torrente multi-ficheiros para o KTorrent +Comment[pt_BR]=Especifica a ordem de download de um torrent com vários arquivos +Comment[ro]=Specifică ordinea de descărcare a unui torent multifișier +Comment[ru]=Модуль для определения порядка загрузки, если торрент содержит несколько файлов. +Comment[si]=බහු-ගොනු ටොරෙන්ටයක බාගැනීම් පෙළගැස්ම නිවේශනය කරන්න +Comment[sk]=UrčiÅ¥ poradie sÅ¥ahovanie viacsúborových torrentov +Comment[sl]=Določanje vrstnega reda prejemov za torrente z več datotekami +Comment[sr]=Одредите редослед преузимања фајлова унутар торента +Comment[sr@ijekavian]=Одредите редослијед преузимања фајлова унутар торента +Comment[sr@ijekavianlatin]=Odredite redoslijed preuzimanja fajlova unutar torenta +Comment[sr@latin]=Odredite redosled preuzimanja fajlova unutar torenta +Comment[sv]=Ange nerladdningsordningen för ett dataflöde med flera filer +Comment[tr]=Birden fazla dosyaya sahip bir torrentin indirme sırasını belirler +Comment[uk]=Додаток для визначення порядку отримання багатофайлового торента +Comment[x-test]=xxSpecify the download order of a multi-file torrentxx +Comment[zh_CN]=指定多文件种子的下载顺序 +Comment[zh_TW]=在多檔案的 torrent 中指定下載順序 +Type=Service +X-KDE-Library=ktdownloadorderplugin +X-KDE-PluginInfo-Author=Joris Guisson +X-KDE-PluginInfo-Email=joris.guisson@gmail.com +X-KDE-PluginInfo-Name=DownloadOrderPlugin +X-KDE-PluginInfo-Version=0.1 +X-KDE-PluginInfo-Website=http://kde.org/applications/internet/ktorrent/ +X-KDE-PluginInfo-License=GPL +X-KDE-PluginInfo-EnabledByDefault=false +Icon=view-sort-ascending diff --git a/plugins/downloadorder/ktorrent_downloadorderui.rc b/plugins/downloadorder/ktorrent_downloadorderui.rc new file mode 100644 index 0000000..462c927 --- /dev/null +++ b/plugins/downloadorder/ktorrent_downloadorderui.rc @@ -0,0 +1,7 @@ + + + + + + + diff --git a/plugins/infowidget/CMakeLists.txt b/plugins/infowidget/CMakeLists.txt new file mode 100644 index 0000000..488ed6b --- /dev/null +++ b/plugins/infowidget/CMakeLists.txt @@ -0,0 +1,69 @@ +# Add an option to compile & link against system GeoIP +option(BUILD_WITH_GEOIP + "Link InfoWidget plugin against system GeoIP library and use system-wide default GeoIP Country database" + ON) + +if (BUILD_WITH_GEOIP) + find_path(GEOIP_INCLUDE_DIR NAMES GeoIP.h PATHS / ${INCLUDE_INSTALL_DIR}/) + find_library(GEOIP_LIBRARY NAMES GeoIP PATHS ${LIB_INSTALL_DIR}) + + if (NOT GEOIP_INCLUDE_DIR OR NOT GEOIP_LIBRARY) + set(BUILD_WITH_GEOIP OFF CACHE BOOL + "GeoIP development files could not be found on this system. Forcing this option to OFF" + FORCE) + message(WARNING "GeoIP library development files could not be found on your system.") + else() + message(STATUS " Linking InfoWidget against system GeoIP library") + include_directories(GEOIP_INCLUDE_DIR) + set(geoip_link ${GEOIP_LIBRARY}) + endif() +endif(BUILD_WITH_GEOIP) + +add_library(ktorrent_infowidget MODULE) + +target_sources(ktorrent_infowidget PRIVATE + infowidgetplugin.cpp + iwprefpage.cpp + monitor.cpp + availabilitychunkbar.cpp + downloadedchunkbar.cpp + statustab.cpp + fileview.cpp + peerview.cpp + peerviewmodel.cpp + chunkdownloadview.cpp + chunkdownloadmodel.cpp + trackerview.cpp + trackermodel.cpp + addtrackersdialog.cpp + flagdb.cpp + iwfiletreemodel.cpp + iwfilelistmodel.cpp + webseedstab.cpp + webseedsmodel.cpp) + +if (BUILD_WITH_GEOIP) + target_sources(ktorrent_infowidget PRIVATE geoipmanager.cpp) + add_definitions(-DBUILD_WITH_GEOIP=1) +else() + add_definitions(-DBUILD_WITH_GEOIP=0) +endif() + +ki18n_wrap_ui(ktorrent_infowidget iwprefpage.ui statustab.ui chunkdownloadview.ui trackerview.ui webseedstab.ui) +kconfig_add_kcfg_files(ktorrent_infowidget infowidgetpluginsettings.kcfgc) + +kcoreaddons_desktop_to_json(ktorrent_infowidget ktorrent_infowidget.desktop) + +target_link_libraries( + ktorrent_infowidget + ktcore + KF5::Torrent + KF5::ConfigCore + KF5::I18n + KF5::KIOFileWidgets + KF5::KIOWidgets + KF5::WidgetsAddons + ${geoip_link} +) +install(TARGETS ktorrent_infowidget DESTINATION ${KTORRENT_PLUGIN_INSTALL_DIR} ) + diff --git a/plugins/infowidget/addtrackersdialog.cpp b/plugins/infowidget/addtrackersdialog.cpp new file mode 100644 index 0000000..150972e --- /dev/null +++ b/plugins/infowidget/addtrackersdialog.cpp @@ -0,0 +1,58 @@ +/* + SPDX-FileCopyrightText: 2012 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "addtrackersdialog.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace kt +{ +AddTrackersDialog::AddTrackersDialog(QWidget *parent, const QStringList &tracker_hints) + : QDialog(parent) +{ + setWindowTitle(i18n("Add Trackers")); + trackers = new KEditListWidget(this); + trackers->setButtons(KEditListWidget::Add | KEditListWidget::Remove); + + // If we find any urls on the clipboard, add them + QClipboard *clipboard = QApplication::clipboard(); + const QStringList urlStrings = clipboard->text().split(QRegularExpression(QLatin1String("\\s"))); + for (const QString &s : urlStrings) { + QUrl url(s); + if (url.isValid() && (url.scheme() == QLatin1String("http") || url.scheme() == QLatin1String("https") || url.scheme() == QLatin1String("udp"))) { + trackers->insertItem(s); + } + } + + trackers->lineEdit()->setCompleter(new QCompleter(tracker_hints)); + + QDialogButtonBox *box = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this); + connect(box, &QDialogButtonBox::accepted, this, &AddTrackersDialog::accept); + connect(box, &QDialogButtonBox::rejected, this, &AddTrackersDialog::reject); + + QVBoxLayout *layout = new QVBoxLayout(this); + layout->addWidget(trackers); + layout->addWidget(box); +} + +AddTrackersDialog::~AddTrackersDialog() +{ +} + +QStringList AddTrackersDialog::trackerList() const +{ + return trackers->items(); +} + +} diff --git a/plugins/infowidget/addtrackersdialog.h b/plugins/infowidget/addtrackersdialog.h new file mode 100644 index 0000000..1735f09 --- /dev/null +++ b/plugins/infowidget/addtrackersdialog.h @@ -0,0 +1,34 @@ +/* + SPDX-FileCopyrightText: 2012 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KT_ADDTRACKERSDIALOG_H +#define KT_ADDTRACKERSDIALOG_H + +#include + +#include + +namespace kt +{ +/** + * Dialog to add trackers + */ +class AddTrackersDialog : public QDialog +{ + Q_OBJECT +public: + AddTrackersDialog(QWidget *parent, const QStringList &tracker_hints); + ~AddTrackersDialog() override; + + /// Get the tracker list + QStringList trackerList() const; + +private: + KEditListWidget *trackers; +}; + +} + +#endif // KT_ADDTRACKERSDIALOG_H diff --git a/plugins/infowidget/availabilitychunkbar.cpp b/plugins/infowidget/availabilitychunkbar.cpp new file mode 100644 index 0000000..2e95d80 --- /dev/null +++ b/plugins/infowidget/availabilitychunkbar.cpp @@ -0,0 +1,51 @@ +/* + SPDX-FileCopyrightText: 2005-2007 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "availabilitychunkbar.h" + +#include +#include + +#include + +#include +#include + +namespace kt +{ +AvailabilityChunkBar::AvailabilityChunkBar(QWidget *parent) + : ChunkBar(parent) + , curr_tc(nullptr) +{ + setToolTip( + i18n("  - Available Chunks
" + "  - Unavailable Chunks
" + "  - Excluded Chunks")); +} + +AvailabilityChunkBar::~AvailabilityChunkBar() +{ +} + +const bt::BitSet &AvailabilityChunkBar::getBitSet() const +{ + if (curr_tc) + return curr_tc->availableChunksBitSet(); + else + return bt::BitSet::null; +} + +void AvailabilityChunkBar::setTC(bt::TorrentInterface *tc) +{ + curr_tc = tc; + QSize s = contentsRect().size(); + // Out() << "Pixmap : " << s.width() << " " << s.height() << endl; + pixmap = QPixmap(s); + pixmap.fill(palette().color(QPalette::Active, QPalette::Base)); + QPainter painter(&pixmap); + drawBarContents(&painter); + update(); +} +} diff --git a/plugins/infowidget/availabilitychunkbar.h b/plugins/infowidget/availabilitychunkbar.h new file mode 100644 index 0000000..dfca1a5 --- /dev/null +++ b/plugins/infowidget/availabilitychunkbar.h @@ -0,0 +1,33 @@ +/* + SPDX-FileCopyrightText: 2005-2007 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef AVAILABILITYCHUNKBAR_H +#define AVAILABILITYCHUNKBAR_H + +#include +#include + +namespace kt +{ +/** +@author Joris Guisson +*/ +class AvailabilityChunkBar : public ChunkBar +{ + Q_OBJECT +public: + AvailabilityChunkBar(QWidget *parent); + ~AvailabilityChunkBar() override; + + const bt::BitSet &getBitSet() const override; + + void setTC(bt::TorrentInterface *tc); + +private: + bt::TorrentInterface *curr_tc; +}; +} + +#endif diff --git a/plugins/infowidget/chunkdownloadmodel.cpp b/plugins/infowidget/chunkdownloadmodel.cpp new file mode 100644 index 0000000..fa0278e --- /dev/null +++ b/plugins/infowidget/chunkdownloadmodel.cpp @@ -0,0 +1,258 @@ +/* + SPDX-FileCopyrightText: 2007 Joris Guisson + SPDX-FileCopyrightText: 2007 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "chunkdownloadmodel.h" + +#include + +#include +#include +#include + +using namespace bt; + +namespace kt +{ +ChunkDownloadModel::Item::Item(ChunkDownloadInterface *cd, const QString &files) + : cd(cd) + , files(files) +{ + cd->getStats(stats); +} + +bool ChunkDownloadModel::Item::changed() const +{ + ChunkDownloadInterface::Stats s; + cd->getStats(s); + bool ret = s.pieces_downloaded != stats.pieces_downloaded || s.download_speed != stats.download_speed || s.current_peer_id != stats.current_peer_id; + + stats = s; + return ret; +} + +QVariant ChunkDownloadModel::Item::data(int col) const +{ + switch (col) { + case 0: + return stats.chunk_index; + case 1: + return QStringLiteral("%1 / %2").arg(stats.pieces_downloaded).arg(stats.total_pieces); + case 2: + return stats.current_peer_id; + case 3: + return BytesPerSecToString(stats.download_speed); + case 4: + return files; + } + return QVariant(); +} + +QVariant ChunkDownloadModel::Item::sortData(int col) const +{ + switch (col) { + case 0: + return stats.chunk_index; + case 1: + return stats.pieces_downloaded; + case 2: + return stats.current_peer_id; + case 3: + return stats.download_speed; + case 4: + return files; + default: + return QVariant(); + } +} + +///////////////////////////////////////////////////////////// + +ChunkDownloadModel::ChunkDownloadModel(QObject *parent) + : QAbstractTableModel(parent) +{ +} + +ChunkDownloadModel::~ChunkDownloadModel() +{ + qDeleteAll(items); +} + +void ChunkDownloadModel::downloadAdded(bt::ChunkDownloadInterface *cd) +{ + if (!tc) + return; + + bt::ChunkDownloadInterface::Stats stats; + cd->getStats(stats); + QString files; + int n = 0; + if (tc.data()->getStats().multi_file_torrent) { + for (Uint32 i = 0; i < tc.data()->getNumFiles(); i++) { + const bt::TorrentFileInterface &tf = tc.data()->getTorrentFile(i); + if (stats.chunk_index >= tf.getFirstChunk() && stats.chunk_index <= tf.getLastChunk()) { + if (n > 0) + files += QStringLiteral(", "); + + files += tf.getUserModifiedPath(); + n++; + } else if (stats.chunk_index < tf.getFirstChunk()) + break; + } + } + + Item *nitem = new Item(cd, files); + items.append(nitem); + insertRow(items.count() - 1); +} + +void ChunkDownloadModel::downloadRemoved(bt::ChunkDownloadInterface *cd) +{ + int row = 0; + bool found = false; + for (Item *item : qAsConst(items)) { + if (item->cd == cd) { + found = true; + break; + } + row++; + } + + if (found) { + removeRow(row); + } +} + +void ChunkDownloadModel::changeTC(bt::TorrentInterface *tc) +{ + beginResetModel(); + qDeleteAll(items); + items.clear(); + endResetModel(); + this->tc = tc; +} + +void ChunkDownloadModel::clear() +{ + beginResetModel(); + qDeleteAll(items); + items.clear(); + endResetModel(); +} + +void ChunkDownloadModel::update() +{ + int idx = 0; + int lowest = -1; + int highest = -1; + + for (Item *i : qAsConst(items)) { + if (i->changed()) { + if (lowest == -1) + lowest = idx; + highest = idx; + } + idx++; + } + + // emit only one data changed signal + if (lowest != -1) + Q_EMIT dataChanged(index(lowest, 1), index(highest, 3)); +} + +int ChunkDownloadModel::rowCount(const QModelIndex &parent) const +{ + if (parent.isValid()) + return 0; + else + return items.count(); +} + +int ChunkDownloadModel::columnCount(const QModelIndex &parent) const +{ + if (parent.isValid()) + return 0; + else + return 5; +} + +QVariant ChunkDownloadModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if (orientation != Qt::Horizontal) + return QVariant(); + + if (role == Qt::DisplayRole) { + switch (section) { + case 0: + return i18n("Chunk"); + case 1: + return i18n("Progress"); + case 2: + return i18n("Peer"); + case 3: + return i18n("Down Speed"); + case 4: + return i18n("Files"); + default: + return QVariant(); + } + } else if (role == Qt::ToolTipRole) { + switch (section) { + case 0: + return i18n("Number of the chunk"); + case 1: + return i18n("Download progress of the chunk"); + case 2: + return i18n("Which peer we are downloading it from"); + case 3: + return i18n("Download speed of the chunk"); + case 4: + return i18n("Which files the chunk is located in"); + default: + return QVariant(); + } + } + + return QVariant(); +} + +QModelIndex ChunkDownloadModel::index(int row, int column, const QModelIndex &parent) const +{ + if (!hasIndex(row, column, parent) || parent.isValid()) + return QModelIndex(); + else + return createIndex(row, column, items[row]); +} + +QVariant ChunkDownloadModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid() || index.row() >= items.count() || index.row() < 0) + return QVariant(); + + if (role == Qt::DisplayRole) + return items[index.row()]->data(index.column()); + else if (role == Qt::UserRole) + return items[index.row()]->sortData(index.column()); + + return QVariant(); +} + +bool ChunkDownloadModel::removeRows(int row, int count, const QModelIndex & /*parent*/) +{ + beginRemoveRows(QModelIndex(), row, row + count - 1); + for (int i = 0; i < count; i++) + delete items[row + i]; + items.remove(row, count); + endRemoveRows(); + return true; +} + +bool ChunkDownloadModel::insertRows(int row, int count, const QModelIndex & /*parent*/) +{ + beginInsertRows(QModelIndex(), row, row + count - 1); + endInsertRows(); + return true; +} +} diff --git a/plugins/infowidget/chunkdownloadmodel.h b/plugins/infowidget/chunkdownloadmodel.h new file mode 100644 index 0000000..a573fd5 --- /dev/null +++ b/plugins/infowidget/chunkdownloadmodel.h @@ -0,0 +1,74 @@ +/* + SPDX-FileCopyrightText: 2007 Joris Guisson + SPDX-FileCopyrightText: 2007 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KTCHUNKDOWNLOADMODEL_H +#define KTCHUNKDOWNLOADMODEL_H + +#include +#include +#include +#include + +namespace kt +{ +/** + @author +*/ +class ChunkDownloadModel : public QAbstractTableModel +{ + Q_OBJECT +public: + ChunkDownloadModel(QObject *parent); + ~ChunkDownloadModel() override; + + /// A peer has been added + void downloadAdded(bt::ChunkDownloadInterface *cd); + + /// A download has been removed + void downloadRemoved(bt::ChunkDownloadInterface *cd); + + /// change the current torrent + void changeTC(bt::TorrentInterface *tc); + + /** + * Update the model + */ + void update(); + + /** + Clear the model + */ + void clear(); + + int rowCount(const QModelIndex &parent) const override; + int columnCount(const QModelIndex &parent) const override; + QVariant headerData(int section, Qt::Orientation orientation, int role) const override; + QVariant data(const QModelIndex &index, int role) const override; + bool removeRows(int row, int count, const QModelIndex &parent) override; + bool insertRows(int row, int count, const QModelIndex &parent) override; + QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override; + +public: + struct Item { + mutable bt::ChunkDownloadInterface::Stats stats; + bt::ChunkDownloadInterface *cd; + QString files; + + Item(bt::ChunkDownloadInterface *cd, const QString &files); + + bool changed() const; + QVariant data(int col) const; + QVariant sortData(int col) const; + }; + +private: + QVector items; + bt::TorrentInterface::WPtr tc; +}; + +} + +#endif diff --git a/plugins/infowidget/chunkdownloadview.cpp b/plugins/infowidget/chunkdownloadview.cpp new file mode 100644 index 0000000..4a9ca6b --- /dev/null +++ b/plugins/infowidget/chunkdownloadview.cpp @@ -0,0 +1,117 @@ +/* + SPDX-FileCopyrightText: 2007 Joris Guisson + SPDX-FileCopyrightText: 2007 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "chunkdownloadview.h" + +#include +#include + +#include +#include + +#include "chunkdownloadmodel.h" +#include +#include +#include +#include +#include + +using namespace bt; + +namespace kt +{ +ChunkDownloadView::ChunkDownloadView(QWidget *parent) + : QWidget(parent) +{ + setupUi(this); + + model = new ChunkDownloadModel(this); + pm = new QSortFilterProxyModel(this); + pm->setSourceModel(model); + pm->setDynamicSortFilter(true); + pm->setSortRole(Qt::UserRole); + + m_chunk_view->setModel(pm); + m_chunk_view->setRootIsDecorated(false); + m_chunk_view->setSortingEnabled(true); + m_chunk_view->setAlternatingRowColors(true); + m_chunk_view->setUniformRowHeights(true); + + QFont f = font(); + f.setBold(true); + m_chunks_downloaded->setFont(f); + m_chunks_downloading->setFont(f); + m_chunks_left->setFont(f); + m_excluded_chunks->setFont(f); + m_size_chunks->setFont(f); + m_total_chunks->setFont(f); +} + +ChunkDownloadView::~ChunkDownloadView() +{ +} + +void ChunkDownloadView::downloadAdded(ChunkDownloadInterface *cd) +{ + model->downloadAdded(cd); +} + +void ChunkDownloadView::downloadRemoved(ChunkDownloadInterface *cd) +{ + model->downloadRemoved(cd); +} + +void ChunkDownloadView::update() +{ + if (!curr_tc) + return; + + model->update(); + const TorrentStats &s = curr_tc.data()->getStats(); + m_chunks_downloading->setText(QString::number(s.num_chunks_downloading)); + m_chunks_downloaded->setText(QString::number(s.num_chunks_downloaded)); + m_excluded_chunks->setText(QString::number(s.num_chunks_excluded)); + m_chunks_left->setText(QString::number(s.num_chunks_left)); +} + +void ChunkDownloadView::changeTC(TorrentInterface *tc) +{ + curr_tc = tc; + if (!curr_tc) { + setEnabled(false); + } else { + setEnabled(true); + const TorrentStats &s = curr_tc.data()->getStats(); + m_total_chunks->setText(QString::number(s.total_chunks)); + m_size_chunks->setText(BytesToString(s.chunk_size)); + } + model->changeTC(tc); +} + +void ChunkDownloadView::removeAll() +{ + model->clear(); +} + +void ChunkDownloadView::saveState(KSharedConfigPtr cfg) +{ + KConfigGroup g = cfg->group("ChunkDownloadView"); + QByteArray s = m_chunk_view->header()->saveState(); + g.writeEntry("state", s.toBase64()); +} + +void ChunkDownloadView::loadState(KSharedConfigPtr cfg) +{ + KConfigGroup g = cfg->group("ChunkDownloadView"); + QByteArray s = QByteArray::fromBase64(g.readEntry("state", QByteArray())); + if (!s.isEmpty()) { + QHeaderView *v = m_chunk_view->header(); + v->restoreState(s); + m_chunk_view->sortByColumn(v->sortIndicatorSection(), v->sortIndicatorOrder()); + model->sort(v->sortIndicatorSection(), v->sortIndicatorOrder()); + } +} +} diff --git a/plugins/infowidget/chunkdownloadview.h b/plugins/infowidget/chunkdownloadview.h new file mode 100644 index 0000000..ef18136 --- /dev/null +++ b/plugins/infowidget/chunkdownloadview.h @@ -0,0 +1,57 @@ +/* + SPDX-FileCopyrightText: 2007 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KT_CHUNKDOWNLOADVIEW_HH +#define KT_CHUNKDOWNLOADVIEW_HH + +#include +#include + +#include + +#include "ui_chunkdownloadview.h" +#include +#include + +namespace kt +{ +class ChunkDownloadModel; + +/** + * View which shows a list of downloading chunks, of a torrent. + * */ +class ChunkDownloadView : public QWidget, public Ui_ChunkDownloadView +{ + Q_OBJECT +public: + ChunkDownloadView(QWidget *parent); + ~ChunkDownloadView() override; + + /// A peer has been added + void downloadAdded(bt::ChunkDownloadInterface *cd); + + /// A download has been removed + void downloadRemoved(bt::ChunkDownloadInterface *cd); + + /// Check to see if the GUI needs to be updated + void update(); + + /// Change the torrent to display + void changeTC(bt::TorrentInterface *tc); + + /// Remove all items + void removeAll(); + + void saveState(KSharedConfigPtr cfg); + void loadState(KSharedConfigPtr cfg); + +private: + bt::TorrentInterface::WPtr curr_tc; + ChunkDownloadModel *model; + QSortFilterProxyModel *pm; +}; +} + +#endif diff --git a/plugins/infowidget/chunkdownloadview.ui b/plugins/infowidget/chunkdownloadview.ui new file mode 100644 index 0000000..0aaba0f --- /dev/null +++ b/plugins/infowidget/chunkdownloadview.ui @@ -0,0 +1,273 @@ + + ChunkDownloadView + + + + 0 + 0 + 830 + 203 + + + + Chunks + + + + 0 + + + + + + + + + Total: + + + false + + + + + + + + 50 + 0 + + + + QFrame::NoFrame + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + false + + + + + + + + + + + Currently downloading: + + + false + + + + + + + + 50 + 0 + + + + QFrame::NoFrame + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + false + + + + + + + + + + + Downloaded: + + + false + + + + + + + + 50 + 0 + + + + QFrame::NoFrame + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + false + + + + + + + + + + + Excluded: + + + false + + + + + + + + 50 + 0 + + + + QFrame::NoFrame + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + false + + + + + + + + + + + Left: + + + false + + + + + + + + 50 + 0 + + + + QFrame::NoFrame + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + false + + + + + + + + + + + Size: + + + false + + + + + + + + 80 + 0 + + + + QFrame::NoFrame + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + false + + + + + + + + + Qt::Horizontal + + + QSizePolicy::Expanding + + + + 40 + 20 + + + + + + + + + + true + + + false + + + true + + + + + + + + + diff --git a/plugins/infowidget/downloadedchunkbar.cpp b/plugins/infowidget/downloadedchunkbar.cpp new file mode 100644 index 0000000..089a33b --- /dev/null +++ b/plugins/infowidget/downloadedchunkbar.cpp @@ -0,0 +1,96 @@ +/* + SPDX-FileCopyrightText: 2005-2007 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include +#include + +#include "downloadedchunkbar.h" + +#include +#include + +using namespace bt; + +namespace kt +{ +DownloadedChunkBar::DownloadedChunkBar(QWidget *parent) + : ChunkBar(parent) + , curr_tc(nullptr) +{ +} + +DownloadedChunkBar::~DownloadedChunkBar() +{ +} + +const bt::BitSet &DownloadedChunkBar::getBitSet() const +{ + if (curr_tc) + return curr_tc->downloadedChunksBitSet(); + else + return bt::BitSet::null; +} + +void DownloadedChunkBar::setTC(bt::TorrentInterface *tc) +{ + curr_tc = tc; + QSize s = contentsRect().size(); + // Out() << "Pixmap : " << s.width() << " " << s.height() << endl; + pixmap = QPixmap(s); + pixmap.fill(palette().color(QPalette::Active, QPalette::Base)); + QPainter painter(&pixmap); + drawBarContents(&painter); + update(); +} + +void DownloadedChunkBar::updateBar(bool force) +{ + const BitSet &bs = getBitSet(); + QSize s = contentsRect().size(); + bool changed = !(curr == bs); + + if (curr_tc) { + BitSet ebs = curr_tc->excludedChunksBitSet(); + ebs.orBitSet(curr_tc->onlySeedChunksBitSet()), changed = changed || !(curr_ebs == ebs); + curr_ebs = ebs; + } + + if (changed || pixmap.isNull() || pixmap.width() != s.width() || force) { + pixmap = QPixmap(s); + pixmap.fill(palette().color(QPalette::Active, QPalette::Base)); + QPainter painter(&pixmap); + drawBarContents(&painter); + update(); + } +} + +void DownloadedChunkBar::drawBarContents(QPainter *p) +{ + if (!curr_tc) + return; + + Uint32 w = contentsRect().width(); + const BitSet &bs = getBitSet(); + curr = bs; + QColor highlight_color = palette().color(QPalette::Active, QPalette::Highlight); + if (bs.allOn()) + drawAllOn(p, highlight_color, contentsRect()); + else if (curr.getNumBits() > w) + drawMoreChunksThenPixels(p, bs, highlight_color, contentsRect()); + else + drawEqual(p, bs, highlight_color, contentsRect()); + + const TorrentStats &s = curr_tc->getStats(); + if (s.num_chunks_excluded > 0) { + QColor c = palette().color(QPalette::Active, QPalette::Mid); + if (curr_ebs.allOn()) + drawAllOn(p, c, contentsRect()); + else if (s.total_chunks > w) + drawMoreChunksThenPixels(p, curr_ebs, c, contentsRect()); + else + drawEqual(p, curr_ebs, c, contentsRect()); + } +} +} diff --git a/plugins/infowidget/downloadedchunkbar.h b/plugins/infowidget/downloadedchunkbar.h new file mode 100644 index 0000000..b198fd9 --- /dev/null +++ b/plugins/infowidget/downloadedchunkbar.h @@ -0,0 +1,39 @@ +/* + SPDX-FileCopyrightText: 2005-2007 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef DOWNLOADEDCHUNKBAR_H +#define DOWNLOADEDCHUNKBAR_H + +#include +namespace bt +{ +class TorrentInterface; +} + +namespace kt +{ +/** +@author Joris Guisson +*/ +class DownloadedChunkBar : public ChunkBar +{ + Q_OBJECT +public: + DownloadedChunkBar(QWidget *parent); + ~DownloadedChunkBar() override; + + const bt::BitSet &getBitSet() const override; + void updateBar(bool force = false) override; + void drawBarContents(QPainter *p) override; + + void setTC(bt::TorrentInterface *tc); + +private: + bt::TorrentInterface *curr_tc; + bt::BitSet curr_ebs; +}; +} + +#endif diff --git a/plugins/infowidget/fileview.cpp b/plugins/infowidget/fileview.cpp new file mode 100644 index 0000000..f87534d --- /dev/null +++ b/plugins/infowidget/fileview.cpp @@ -0,0 +1,611 @@ +/* + SPDX-FileCopyrightText: 2005 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "fileview.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "iwfilelistmodel.h" +#include "iwfiletreemodel.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace bt; + +namespace kt +{ +FileView::FileView(QWidget *parent) + : QWidget(parent) + , model(nullptr) + , show_list_of_files(false) + , header_state_loaded(false) +{ + QHBoxLayout *layout = new QHBoxLayout(this); + layout->setMargin(0); + layout->setSpacing(0); + QVBoxLayout *vbox = new QVBoxLayout(); + vbox->setMargin(0); + vbox->setSpacing(0); + view = new QTreeView(this); + toolbar = new QToolBar(this); + toolbar->setOrientation(Qt::Vertical); + toolbar->setToolButtonStyle(Qt::ToolButtonIconOnly); + layout->addWidget(toolbar); + + filter = new QLineEdit(this); + filter->setPlaceholderText(i18n("Filter")); + filter->setClearButtonEnabled(true); + filter->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); + connect(filter, &QLineEdit::textChanged, this, &FileView::setFilter); + filter->hide(); + vbox->addWidget(filter); + vbox->addWidget(view); + layout->addItem(vbox); + + view->setContextMenuPolicy(Qt::CustomContextMenu); + view->setRootIsDecorated(false); + view->setSortingEnabled(true); + view->setAlternatingRowColors(true); + view->setSelectionMode(QAbstractItemView::ExtendedSelection); + view->setSelectionBehavior(QAbstractItemView::SelectRows); + view->setUniformRowHeights(true); + + proxy_model = new TreeFilterModel(this); + proxy_model->setSortRole(Qt::UserRole); + if (show_list_of_files) + model = new IWFileListModel(nullptr, this); + else + model = new IWFileTreeModel(nullptr, this); + proxy_model->setSourceModel(model); + view->setModel(proxy_model); + + setupActions(); + + connect(view, &QTreeView::customContextMenuRequested, this, &FileView::showContextMenu); + connect(view, &QTreeView::doubleClicked, this, &FileView::onDoubleClicked); + + setEnabled(false); +} + +FileView::~FileView() +{ +} + +void FileView::setupActions() +{ + context_menu = new QMenu(this); + open_action = context_menu->addAction(QIcon::fromTheme(QStringLiteral("document-open")), i18nc("Open file", "Open"), this, &FileView::open); + open_with_action = + context_menu->addAction(QIcon::fromTheme(QStringLiteral("document-open")), i18nc("Open file with", "Open With"), this, &FileView::openWith); + check_data = context_menu->addAction(QIcon::fromTheme(QStringLiteral("kt-check-data")), i18n("Check File"), this, &FileView::checkFile); + context_menu->addSeparator(); + download_first_action = context_menu->addAction(i18n("Download first"), this, &FileView::downloadFirst); + download_normal_action = context_menu->addAction(i18n("Download normally"), this, &FileView::downloadNormal); + download_last_action = context_menu->addAction(i18n("Download last"), this, &FileView::downloadLast); + context_menu->addSeparator(); + dnd_action = context_menu->addAction(i18n("Do not download"), this, &FileView::doNotDownload); + delete_action = context_menu->addAction(i18n("Delete File(s)"), this, &FileView::deleteFiles); + context_menu->addSeparator(); + move_files_action = context_menu->addAction(i18n("Move File"), this, &FileView::moveFiles); + context_menu->addSeparator(); + collapse_action = context_menu->addAction(i18n("Collapse Folder Tree"), this, &FileView::collapseTree); + expand_action = context_menu->addAction(i18n("Expand Folder Tree"), this, &FileView::expandTree); + + QActionGroup *ag = new QActionGroup(this); + show_tree_action = new QAction(QIcon::fromTheme(QStringLiteral("view-list-tree")), i18n("File Tree"), this); + connect(show_tree_action, &QAction::triggered, this, &FileView::showTree); + show_list_action = new QAction(QIcon::fromTheme(QStringLiteral("view-list-text")), i18n("File List"), this); + connect(show_list_action, &QAction::triggered, this, &FileView::showList); + ag->addAction(show_list_action); + ag->addAction(show_tree_action); + ag->setExclusive(true); + show_list_action->setCheckable(true); + show_tree_action->setCheckable(true); + toolbar->addAction(show_tree_action); + toolbar->addAction(show_list_action); + + show_filter_action = new QAction(QIcon::fromTheme(QStringLiteral("view-filter")), i18n("Show Filter"), this); + show_filter_action->setCheckable(true); + connect(show_filter_action, &QAction::toggled, filter, &QLineEdit::setVisible); + toolbar->addAction(show_filter_action); +} + +void FileView::changeTC(bt::TorrentInterface *tc) +{ + if (tc == curr_tc.data()) + return; + + if (curr_tc) + expanded_state_map[curr_tc.data()] = model->saveExpandedState(proxy_model, view); + + curr_tc = tc; + setEnabled(tc != nullptr); + model->changeTorrent(tc); + if (tc) { + connect(tc, &bt::TorrentInterface::missingFilesMarkedDND, this, &FileView::onMissingFileMarkedDND); + + view->setRootIsDecorated(!show_list_of_files && tc->getStats().multi_file_torrent); + if (!show_list_of_files) { + auto i = expanded_state_map.constFind(tc); + if (i != expanded_state_map.constEnd()) + model->loadExpandedState(proxy_model, view, i.value()); + else + view->expandAll(); + } + } + +#if 0 + if (!header_state_loaded) { + view->resizeColumnToContents(0); + header_state_loaded = true; + } +#else + view->resizeColumnToContents(0); +#endif +} + +void FileView::onMissingFileMarkedDND(bt::TorrentInterface *tc) +{ + if (curr_tc.data() == tc) + model->missingFilesMarkedDND(); +} + +void FileView::showContextMenu(const QPoint &p) +{ + if (!curr_tc) + return; + + bt::TorrentInterface *tc = curr_tc.data(); + const TorrentStats &s = tc->getStats(); + + QModelIndexList sel = view->selectionModel()->selectedRows(); + if (sel.count() == 0) + return; + + if (sel.count() > 1) { + download_first_action->setEnabled(true); + download_normal_action->setEnabled(true); + download_last_action->setEnabled(true); + open_action->setEnabled(false); + open_with_action->setEnabled(false); + dnd_action->setEnabled(true); + delete_action->setEnabled(true); + context_menu->popup(view->viewport()->mapToGlobal(p)); + move_files_action->setEnabled(true); + collapse_action->setEnabled(!show_list_of_files); + expand_action->setEnabled(!show_list_of_files); + check_data->setEnabled(true); + return; + } + + QModelIndex item = proxy_model->mapToSource(sel.front()); + bt::TorrentFileInterface *file = model->indexToFile(item); + + download_first_action->setEnabled(false); + download_last_action->setEnabled(false); + download_normal_action->setEnabled(false); + dnd_action->setEnabled(false); + delete_action->setEnabled(false); + + if (!s.multi_file_torrent) { + open_action->setEnabled(true); + open_with_action->setEnabled(true); + move_files_action->setEnabled(true); + preview_path = tc->getStats().output_path; + collapse_action->setEnabled(false); + expand_action->setEnabled(false); + check_data->setEnabled(true); + } else if (file) { + check_data->setEnabled(true); + move_files_action->setEnabled(true); + collapse_action->setEnabled(false); + expand_action->setEnabled(false); + if (!file->isNull()) { + open_action->setEnabled(true); + open_with_action->setEnabled(true); + preview_path = file->getPathOnDisk(); + + download_first_action->setEnabled(file->getPriority() != FIRST_PRIORITY); + download_normal_action->setEnabled(file->getPriority() != NORMAL_PRIORITY); + download_last_action->setEnabled(file->getPriority() != LAST_PRIORITY); + dnd_action->setEnabled(file->getPriority() != ONLY_SEED_PRIORITY); + delete_action->setEnabled(file->getPriority() != EXCLUDED); + } else { + open_action->setEnabled(false); + open_with_action->setEnabled(false); + } + } else { + check_data->setEnabled(false); + move_files_action->setEnabled(false); + download_first_action->setEnabled(true); + download_normal_action->setEnabled(true); + download_last_action->setEnabled(true); + dnd_action->setEnabled(true); + delete_action->setEnabled(true); + open_action->setEnabled(true); + open_with_action->setEnabled(true); + preview_path = tc->getStats().output_path + model->dirPath(item); + collapse_action->setEnabled(!show_list_of_files); + expand_action->setEnabled(!show_list_of_files); + } + + context_menu->popup(view->viewport()->mapToGlobal(p)); +} + +void FileView::open() +{ + auto *job = new KIO::OpenUrlJob(QUrl::fromLocalFile(preview_path)); + job->start(); +} + +void FileView::openWith() +{ + auto *job = new KIO::ApplicationLauncherJob(); + job->setUrls({QUrl::fromLocalFile(preview_path)}); + job->setUiDelegate(new KIO::JobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, this)); + job->start(); +} + +void FileView::changePriority(bt::Priority newpriority) +{ + QModelIndexList sel = view->selectionModel()->selectedRows(2); + for (QModelIndex &i : sel) + i = proxy_model->mapToSource(i); + + model->changePriority(sel, newpriority); + proxy_model->invalidate(); +} + +void FileView::downloadFirst() +{ + changePriority(FIRST_PRIORITY); +} + +void FileView::downloadLast() +{ + changePriority(LAST_PRIORITY); +} + +void FileView::downloadNormal() +{ + changePriority(NORMAL_PRIORITY); +} + +void FileView::doNotDownload() +{ + changePriority(ONLY_SEED_PRIORITY); +} + +void FileView::deleteFiles() +{ + QModelIndexList sel = view->selectionModel()->selectedRows(); + Uint32 n = sel.count(); + if (n == 1) { // single item can be a directory + if (!model->indexToFile(proxy_model->mapToSource(sel.front()))) + n++; + } + + QString msg = i18np("You will lose all data in this file, are you sure you want to do this?", + "You will lose all data in these files, are you sure you want to do this?", + n); + + if (KMessageBox::warningYesNo(nullptr, msg) == KMessageBox::Yes) + changePriority(EXCLUDED); +} + +void FileView::moveFiles() +{ + if (!curr_tc) + return; + + bt::TorrentInterface *tc = curr_tc.data(); + if (tc->getStats().multi_file_torrent) { + const QModelIndexList sel = view->selectionModel()->selectedRows(); + QMap moves; + + QString recentDirClass; + QString dir = + QFileDialog::getExistingDirectory(this, + i18n("Select a directory to move the data to."), + KFileWidget::getStartUrl(QUrl(QLatin1String("kfiledialog:///saveTorrentData")), recentDirClass).toLocalFile()); + + if (dir.isEmpty()) + return; + + if (!recentDirClass.isEmpty()) + KRecentDirs::add(recentDirClass, dir); + + for (const QModelIndex &idx : sel) { + bt::TorrentFileInterface *tfi = model->indexToFile(proxy_model->mapToSource(idx)); + if (!tfi) + continue; + + moves.insert(tfi, dir); + } + + if (moves.count() > 0) { + tc->moveTorrentFiles(moves); + } + } else { + QString recentDirClass; + QString dir = + QFileDialog::getExistingDirectory(this, + i18n("Select a directory to move the data to."), + KFileWidget::getStartUrl(QUrl(QStringLiteral("kfiledialog:///saveTorrentData")), recentDirClass).toLocalFile()); + + if (dir.isEmpty()) + return; + + if (!recentDirClass.isEmpty()) + KRecentDirs::add(recentDirClass, dir); + + tc->changeOutputDir(dir, bt::TorrentInterface::MOVE_FILES); + } +} + +void FileView::expandCollapseTree(const QModelIndex &idx, bool expand) +{ + int rowCount = proxy_model->rowCount(idx); + for (int i = 0; i < rowCount; i++) { + const QModelIndex &ridx = proxy_model->index(i, 0, idx); + if (proxy_model->hasChildren(ridx)) + expandCollapseTree(ridx, expand); + } + view->setExpanded(idx, expand); +} + +void FileView::expandCollapseSelected(bool expand) +{ + const QModelIndexList sel = view->selectionModel()->selectedRows(); + for (const QModelIndex &i : sel) { + if (proxy_model->hasChildren(i)) + expandCollapseTree(i, expand); + } +} + +void FileView::collapseTree() +{ + expandCollapseSelected(false); +} + +void FileView::expandTree() +{ + expandCollapseSelected(true); +} + +void FileView::onDoubleClicked(const QModelIndex &index) +{ + if (!curr_tc) + return; + + bt::TorrentInterface *tc = curr_tc.data(); + const TorrentStats &s = tc->getStats(); + + QString pathToOpen; + bool isMultimedia = false; + bool isPreviewAvailable = false; + int downloadPercentage = 0; + int fileIndex = 0; + + if (s.multi_file_torrent) { + bt::TorrentFileInterface *file = model->indexToFile(proxy_model->mapToSource(index)); + if (!file) { + // directory + pathToOpen = s.output_path + model->dirPath(proxy_model->mapToSource(index)); + } else { + isMultimedia = (file->isVideo() || file->isAudio() || file->isMultimedia()) && !file->doNotDownload(); + if (isMultimedia) { + isPreviewAvailable = file->isPreviewAvailable(); + downloadPercentage = file->getDownloadPercentage(); + fileIndex = file->getIndex(); + } + pathToOpen = file->getPathOnDisk(); + } + } else { + isMultimedia = tc->isMultimedia(); + isPreviewAvailable = tc->readyForPreview(); + if (s.total_bytes) + downloadPercentage = 100 - 100 * s.total_bytes_to_download / s.total_bytes; + pathToOpen = s.output_path; + } + + if (isMultimedia) { + static QList streams; + bool doStream = false; + if (!isPreviewAvailable) { + doStream = KMessageBox::Yes + == KMessageBox::questionYesNo(this, + i18n("Not enough data downloaded for opening the file.\n\n" + "Enable sequential download mode for it to obtain necessary data with a higher priority?"), + QString(), + KStandardGuiItem::yes(), + KStandardGuiItem::no()); + } else if (downloadPercentage < 90) { + doStream = true; + // doStream = KMessageBox::Yes==KMessageBox::questionYesNo(this, i18n("Enable sequential download mode for this file?"), + // QString(), KStandardGuiItem::yes(), KStandardGuiItem::no(), QStringLiteral("SequentialModeOnFileOpen")); + } + if (doStream) { + streams << tc->createTorrentFileStream(fileIndex, true, nullptr); + if (streams.last().isNull()) + streams << tc->createTorrentFileStream(fileIndex, false, nullptr); + } + if (!isPreviewAvailable) + return; + } + auto *job = new KIO::OpenUrlJob(QUrl::fromLocalFile(pathToOpen)); + job->start(); +} + +void FileView::saveState(KSharedConfigPtr cfg) +{ + if (!model) + return; + + KConfigGroup g = cfg->group("FileView"); + QByteArray s = view->header()->saveState(); + g.writeEntry("state", s.toBase64()); + g.writeEntry("show_list_of_files", show_list_of_files); +} + +void FileView::loadState(KSharedConfigPtr cfg) +{ + KConfigGroup g = cfg->group("FileView"); + QByteArray s = g.readEntry("state", QByteArray()); + if (!s.isEmpty()) { + QHeaderView *v = view->header(); + v->restoreState(QByteArray::fromBase64(s)); + view->sortByColumn(v->sortIndicatorSection(), v->sortIndicatorOrder()); + header_state_loaded = true; + } + + bool show_list = g.readEntry("show_list_of_files", false); + if (show_list_of_files != show_list) + setShowListOfFiles(show_list); + + show_list_action->setChecked(show_list); + show_tree_action->setChecked(!show_list); +} + +void FileView::update() +{ + if (model) + model->update(); +} + +void FileView::onTorrentRemoved(bt::TorrentInterface *tc) +{ + expanded_state_map.remove(tc); +} + +void FileView::setShowListOfFiles(bool on) +{ + if (show_list_of_files == on) + return; + + QByteArray header_state = view->header()->saveState(); + show_list_of_files = on; + if (!curr_tc) { + // no torrent, but still need to change the model + proxy_model->setSourceModel(nullptr); + delete model; + if (show_list_of_files) + model = new IWFileListModel(nullptr, this); + else + model = new IWFileTreeModel(nullptr, this); + proxy_model->setSourceModel(model); + view->header()->restoreState(header_state); + return; + } + + bt::TorrentInterface *tc = curr_tc.data(); + if (on) + expanded_state_map[tc] = model->saveExpandedState(proxy_model, view); + + proxy_model->setSourceModel(nullptr); + delete model; + model = nullptr; + + if (show_list_of_files) + model = new IWFileListModel(tc, this); + else + model = new IWFileTreeModel(tc, this); + + proxy_model->setSourceModel(model); + view->setRootIsDecorated(!show_list_of_files && tc->getStats().multi_file_torrent); + view->header()->restoreState(header_state); + + if (!on) { + auto i = expanded_state_map.constFind(tc); + if (i != expanded_state_map.constEnd()) + model->loadExpandedState(proxy_model, view, i.value()); + else + view->expandAll(); + } + + collapse_action->setEnabled(!show_list_of_files); + expand_action->setEnabled(!show_list_of_files); +} + +void FileView::showTree() +{ + if (show_list_of_files) + setShowListOfFiles(false); +} + +void FileView::showList() +{ + if (!show_list_of_files) + setShowListOfFiles(true); +} + +void FileView::filePercentageChanged(bt::TorrentFileInterface *file, float percentage) +{ + if (model) + model->filePercentageChanged(file, percentage); +} + +void FileView::filePreviewChanged(bt::TorrentFileInterface *file, bool preview) +{ + if (model) + model->filePreviewChanged(file, preview); +} + +void FileView::setFilter(const QString &f) +{ + Q_UNUSED(f); + proxy_model->setFilterFixedString(filter->text()); +} + +void FileView::checkFile() +{ + const QModelIndexList sel = view->selectionModel()->selectedRows(); + if (!curr_tc || sel.isEmpty()) + return; + + if (curr_tc.data()->getStats().multi_file_torrent) { + bt::Uint32 from = curr_tc.data()->getStats().total_chunks; + bt::Uint32 to = 0; + for (const QModelIndex &idx : sel) { + bt::TorrentFileInterface *tfi = model->indexToFile(proxy_model->mapToSource(idx)); + if (!tfi) + continue; + + if (tfi->getFirstChunk() < from) + from = tfi->getFirstChunk(); + if (tfi->getLastChunk() > to) + to = tfi->getLastChunk(); + } + curr_tc.data()->startDataCheck(false, from, to); + } else + curr_tc.data()->startDataCheck(false, 0, curr_tc.data()->getStats().total_chunks); +} + +} diff --git a/plugins/infowidget/fileview.h b/plugins/infowidget/fileview.h new file mode 100644 index 0000000..eb6fd05 --- /dev/null +++ b/plugins/infowidget/fileview.h @@ -0,0 +1,110 @@ +/* + SPDX-FileCopyrightText: 2005-2007 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KTFILEVIEW_H +#define KTFILEVIEW_H + +#include +#include + +#include +#include + +class QLineEdit; +class QMenu; +class QSortFilterProxyModel; +class QToolBar; + +namespace bt +{ +class TorrentFileInterface; +} + +namespace kt +{ +class TorrentFileModel; + +/** + @author Joris Guisson +*/ +class FileView : public QWidget +{ + Q_OBJECT +public: + FileView(QWidget *parent); + ~FileView() override; + + void changeTC(bt::TorrentInterface *tc); + void setShowListOfFiles(bool on); + void saveState(KSharedConfigPtr cfg); + void loadState(KSharedConfigPtr cfg); + void update(); + void filePercentageChanged(bt::TorrentFileInterface *file, float percentage); + void filePreviewChanged(bt::TorrentFileInterface *file, bool preview); + +public Q_SLOTS: + void onTorrentRemoved(bt::TorrentInterface *tc); + +private Q_SLOTS: + void showContextMenu(const QPoint &p); + void onDoubleClicked(const QModelIndex &index); + void onMissingFileMarkedDND(bt::TorrentInterface *tc); + +private: + void changePriority(bt::Priority newpriority); + void expandCollapseTree(const QModelIndex &idx, bool expand); + void expandCollapseSelected(bool expand); + void setupActions(); + +private Q_SLOTS: + void open(); + void openWith(); + void downloadFirst(); + void downloadLast(); + void downloadNormal(); + void doNotDownload(); + void deleteFiles(); + void moveFiles(); + void collapseTree(); + void expandTree(); + void showTree(); + void showList(); + void setFilter(const QString &f); + void checkFile(); + +private: + bt::TorrentInterface::WPtr curr_tc; + TorrentFileModel *model; + + QMenu *context_menu; + QAction *open_action; + QAction *open_with_action; + QAction *download_first_action; + QAction *download_normal_action; + QAction *download_last_action; + QAction *dnd_action; + QAction *delete_action; + QAction *move_files_action; + QAction *collapse_action; + QAction *expand_action; + QAction *show_tree_action; + QAction *show_list_action; + QAction *show_filter_action; + QAction *check_data; + + QString preview_path; + bool show_list_of_files; + QMap expanded_state_map; + QSortFilterProxyModel *proxy_model; + bool header_state_loaded; + + QTreeView *view; + QToolBar *toolbar; + QLineEdit *filter; +}; + +} + +#endif diff --git a/plugins/infowidget/flagdb.cpp b/plugins/infowidget/flagdb.cpp new file mode 100644 index 0000000..5d57a6f --- /dev/null +++ b/plugins/infowidget/flagdb.cpp @@ -0,0 +1,101 @@ +/* + SPDX-FileCopyrightText: 2007 Modestas Vainius + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "flagdb.h" +#include +#include +#include +#include + +kt::FlagDBSource::FlagDBSource(const QString &pathPattern) + : pathPattern(pathPattern) +{ +} + +kt::FlagDBSource::FlagDBSource() +{ +} + +QString kt::FlagDBSource::getPath(const QString &country) const +{ + // pathPattern = QStringLiteral("locale/l10n/%1/flag.png"); + // QStandardPaths::locate(QStandardPaths::GenericDataLocation, flagPath.arg(code)); + // example: /usr/share/locale/l10n/ru/flag.png (part of kde-runtime-data package) + return pathPattern.arg(country); +} + +const QPixmap &kt::FlagDB::nullPixmap = QPixmap(); + +kt::FlagDB::FlagDB(int preferredWidth, int preferredHeight) + : preferredWidth(preferredWidth) + , preferredHeight(preferredHeight) + , sources() + , db() +{ +} + +kt::FlagDB::FlagDB(const FlagDB &other) + : preferredWidth(other.preferredWidth) + , preferredHeight(other.preferredHeight) + , sources(other.sources) + , db(other.db) +{ +} + +kt::FlagDB::~FlagDB() +{ +} + +void kt::FlagDB::addFlagSource(const FlagDBSource &source) +{ + sources.append(source); +} + +void kt::FlagDB::addFlagSource(const QString &pathPattern) +{ + addFlagSource(FlagDBSource(pathPattern)); +} + +const QList &kt::FlagDB::listSources() const +{ + return sources; +} + +bool kt::FlagDB::isFlagAvailable(const QString &country) +{ + return getFlag(country).isNull(); +} + +const QPixmap &kt::FlagDB::getFlag(const QString &country) +{ + const QString &c = country.toLower(); + auto it = db.constFind(c); + if (it != db.constEnd()) + return *it; + + QImage img; + QPixmap pixmap; + for (const FlagDBSource &s : qAsConst(sources)) { + const QString &path = s.getPath(c); + // e.g.: /usr/share/locale/l10n/ru/flag.png + if (QFile::exists(path) && img.load(path)) { + if (img.width() != preferredWidth || img.height() != preferredHeight) { + const QImage &imgScaled = img.scaled(preferredWidth, preferredHeight, Qt::KeepAspectRatio, Qt::SmoothTransformation); + if (!imgScaled.isNull()) { + pixmap = QPixmap::fromImage(imgScaled); + break; + } else if (img.width() <= preferredWidth || img.height() <= preferredHeight) { + pixmap = QPixmap::fromImage(img); + break; + } + } else { + pixmap = QPixmap::fromImage(img); + break; + } + } + } + + return (db[c] = (!pixmap.isNull()) ? pixmap : nullPixmap); +} diff --git a/plugins/infowidget/flagdb.h b/plugins/infowidget/flagdb.h new file mode 100644 index 0000000..bafa264 --- /dev/null +++ b/plugins/infowidget/flagdb.h @@ -0,0 +1,55 @@ +/* + SPDX-FileCopyrightText: 2007 Modestas Vainius + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#ifndef FLAGDB_H +#define FLAGDB_H + +#include +#include +#include +#include + +namespace kt +{ +class FlagDBSource +{ +public: + FlagDBSource(); + FlagDBSource(const QString &pathPattern); + QString getPath(const QString &country) const; + + const QString &getPathPattern() + { + return pathPattern; + }; + +private: + QString pathPattern; +}; + +/** +@author Modestas Vainius +*/ +class FlagDB +{ +public: + FlagDB(int preferredWidth, int preferredHeight); + FlagDB(const FlagDB &m); + ~FlagDB(); + + void addFlagSource(const FlagDBSource &source); + void addFlagSource(const QString &pathPattern); + const QList &listSources() const; + bool isFlagAvailable(const QString &country); + const QPixmap &getFlag(const QString &country); + +private: + static const QPixmap &nullPixmap; + int preferredWidth, preferredHeight; + QList sources; + QMap db; +}; +} + +#endif diff --git a/plugins/infowidget/geoipmanager.cpp b/plugins/infowidget/geoipmanager.cpp new file mode 100644 index 0000000..de1c156 --- /dev/null +++ b/plugins/infowidget/geoipmanager.cpp @@ -0,0 +1,59 @@ +/* + SPDX-FileCopyrightText: 2009 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include + +#include +#include +#include +#include + +#include "geoipmanager.h" +#include +#include +#include +#include + +using namespace bt; + +namespace kt +{ +GeoIPManager::GeoIPManager(QObject *parent) + : QObject(parent) +{ + geo_ip = GeoIP_open_type(GEOIP_COUNTRY_EDITION, GEOIP_STANDARD); +} + +GeoIPManager::~GeoIPManager() +{ + if (geo_ip) + GeoIP_delete(geo_ip); +} + +int GeoIPManager::findCountry(const QString &addr) +{ + if (!geo_ip) + return 0; + else + return GeoIP_id_by_name(geo_ip, addr.toLatin1().data()); +} + +QString GeoIPManager::countryCode(int country_id) +{ + if (country_id > 0 && country_id < 247) + return QString::fromLatin1(GeoIP_country_code[country_id]); + else + return QString(); +} + +QString GeoIPManager::countryName(int country_id) +{ + if (country_id > 0 && country_id < 247) + return QString::fromUtf8(GeoIP_country_name[country_id]); + else + return QString(); +} + +} diff --git a/plugins/infowidget/geoipmanager.h b/plugins/infowidget/geoipmanager.h new file mode 100644 index 0000000..4d5c7ca --- /dev/null +++ b/plugins/infowidget/geoipmanager.h @@ -0,0 +1,51 @@ +/* + SPDX-FileCopyrightText: 2009 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KT_GEOIPMANAGER_H +#define KT_GEOIPMANAGER_H + +#include +#include + +namespace kt +{ +/** + * Manages GeoIP database. Downloads it from the internet and handles all queries to it. + */ +class GeoIPManager : public QObject +{ + Q_OBJECT +public: + GeoIPManager(QObject *parent = 0); + ~GeoIPManager() override; + + /** + * Find the country given an IP address + * @param addr The IP address + * @return The country ID + */ + int findCountry(const QString &addr); + + /** + * Get the name of the country + * @param country_id The country ID + * @return The name + */ + QString countryName(int country_id); + + /** + * Get the code of the country + * @param country_id The country ID + * @return The name + */ + QString countryCode(int country_id); + +private: + GeoIP *geo_ip; +}; + +} + +#endif // KT_GEOIPMANAGER_H diff --git a/plugins/infowidget/infowidgetplugin.cpp b/plugins/infowidget/infowidgetplugin.cpp new file mode 100644 index 0000000..5a5d962 --- /dev/null +++ b/plugins/infowidget/infowidgetplugin.cpp @@ -0,0 +1,288 @@ +/* + SPDX-FileCopyrightText: 2005-2007 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "infowidgetplugin.h" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "chunkdownloadview.h" +#include "fileview.h" +#include "infowidgetpluginsettings.h" +#include "iwprefpage.h" +#include "monitor.h" +#include "peerview.h" +#include "statustab.h" +#include "trackerview.h" +#include "webseedstab.h" + +K_PLUGIN_FACTORY_WITH_JSON(ktorrent_infowidget, "ktorrent_infowidget.json", registerPlugin();) + +using namespace bt; + +namespace kt +{ +InfoWidgetPlugin::InfoWidgetPlugin(QObject *parent, const QVariantList &) + : Plugin(parent) + , peer_view(nullptr) + , cd_view(nullptr) + , tracker_view(nullptr) + , file_view(nullptr) + , status_tab(nullptr) + , webseeds_tab(nullptr) + , monitor(nullptr) + , pref(nullptr) +{ +} + +InfoWidgetPlugin::~InfoWidgetPlugin() +{ +} + +void InfoWidgetPlugin::load() +{ + LogSystemManager::instance().registerSystem(i18n("Info Widget"), SYS_INW); + connect(getCore(), &CoreInterface::settingsChanged, this, &InfoWidgetPlugin::applySettings); + + status_tab = new StatusTab(nullptr); + file_view = new FileView(nullptr); + file_view->loadState(KSharedConfig::openConfig()); + connect(getCore(), &CoreInterface::torrentRemoved, this, &InfoWidgetPlugin::torrentRemoved); + + pref = new IWPrefPage(nullptr); + TorrentActivityInterface *ta = getGUI()->getTorrentActivity(); + ta->addViewListener(this); + ta->addToolWidget(status_tab, i18nc("@title:tab", "Status"), QStringLiteral("dialog-information"), i18n("Displays status information about a torrent")); + ta->addToolWidget(file_view, i18nc("@title:tab", "Files"), QStringLiteral("folder"), i18n("Shows all the files in a torrent")); + + applySettings(); + + getGUI()->addPrefPage(pref); + currentTorrentChanged(const_cast(ta->getCurrentTorrent())); +} + +void InfoWidgetPlugin::unload() +{ + LogSystemManager::instance().unregisterSystem(i18n("Bandwidth Scheduler")); + disconnect(getCore(), &CoreInterface::settingsChanged, this, &InfoWidgetPlugin::applySettings); + disconnect(getCore(), &CoreInterface::torrentRemoved, this, &InfoWidgetPlugin::torrentRemoved); + if (cd_view) + cd_view->saveState(KSharedConfig::openConfig()); + if (peer_view) + peer_view->saveState(KSharedConfig::openConfig()); + if (file_view) + file_view->saveState(KSharedConfig::openConfig()); + if (webseeds_tab) + webseeds_tab->saveState(KSharedConfig::openConfig()); + if (tracker_view) + tracker_view->saveState(KSharedConfig::openConfig()); + KSharedConfig::openConfig()->sync(); + + TorrentActivityInterface *ta = getGUI()->getTorrentActivity(); + ta->removeViewListener(this); + getGUI()->removePrefPage(pref); + ta->removeToolWidget(status_tab); + ta->removeToolWidget(file_view); + if (cd_view) + ta->removeToolWidget(cd_view); + if (tracker_view) + ta->removeToolWidget(tracker_view); + if (peer_view) + ta->removeToolWidget(peer_view); + if (webseeds_tab) + ta->removeToolWidget(webseeds_tab); + + delete monitor; + monitor = nullptr; + delete status_tab; + status_tab = nullptr; + delete file_view; + file_view = nullptr; + delete cd_view; + cd_view = nullptr; + delete peer_view; + peer_view = nullptr; + delete tracker_view; + tracker_view = nullptr; + delete webseeds_tab; + webseeds_tab = nullptr; + delete pref; + pref = nullptr; +} + +void InfoWidgetPlugin::guiUpdate() +{ + if (status_tab && status_tab->isVisible()) + status_tab->update(); + + if (file_view && file_view->isVisible()) + file_view->update(); + + if (peer_view && peer_view->isVisible()) + peer_view->update(); + + if (cd_view && cd_view->isVisible()) + cd_view->update(); + + if (tracker_view && tracker_view->isVisible()) + tracker_view->update(); + + if (webseeds_tab && webseeds_tab->isVisible()) + webseeds_tab->update(); +} + +void InfoWidgetPlugin::currentTorrentChanged(bt::TorrentInterface *tc) +{ + if (status_tab) + status_tab->changeTC(tc); + if (file_view) + file_view->changeTC(tc); + if (cd_view) + cd_view->changeTC(tc); + if (tracker_view) + tracker_view->changeTC(tc); + if (webseeds_tab) + webseeds_tab->changeTC(tc); + + if (peer_view) + peer_view->setEnabled(tc != nullptr); + + createMonitor(tc); +} + +bool InfoWidgetPlugin::versionCheck(const QString &version) const +{ + return version == QStringLiteral(VERSION); +} + +void InfoWidgetPlugin::applySettings() +{ + // if the colors are invalid, set the default colors + bool save = false; + if (!InfoWidgetPluginSettings::firstColor().isValid()) { + save = true; + InfoWidgetPluginSettings::setFirstColor(Qt::green); + } + + if (!InfoWidgetPluginSettings::lastColor().isValid()) { + save = true; + InfoWidgetPluginSettings::setLastColor(Qt::red); + } + + if (save) + InfoWidgetPluginSettings::self()->save(); + + showWebSeedsTab(InfoWidgetPluginSettings::showWebSeedsTab()); + showPeerView(InfoWidgetPluginSettings::showPeerView()); + showChunkView(InfoWidgetPluginSettings::showChunkView()); + showTrackerView(InfoWidgetPluginSettings::showTrackersView()); +} + +void InfoWidgetPlugin::showPeerView(bool show) +{ + TorrentActivityInterface *ta = getGUI()->getTorrentActivity(); + bt::TorrentInterface *tc = ta->getCurrentTorrent(); + + if (show && !peer_view) { + peer_view = new PeerView(nullptr); + ta->addToolWidget(peer_view, i18n("Peers"), QStringLiteral("system-users"), i18n("Displays all the peers you are connected to for a torrent")); + peer_view->loadState(KSharedConfig::openConfig()); + createMonitor(tc); + } else if (!show && peer_view) { + peer_view->saveState(KSharedConfig::openConfig()); + ta->removeToolWidget(peer_view); + delete peer_view; + peer_view = nullptr; + createMonitor(tc); + } +} + +void InfoWidgetPlugin::showChunkView(bool show) +{ + TorrentActivityInterface *ta = getGUI()->getTorrentActivity(); + bt::TorrentInterface *tc = ta->getCurrentTorrent(); + + if (show && !cd_view) { + cd_view = new ChunkDownloadView(nullptr); + ta->addToolWidget(cd_view, i18n("Chunks"), QStringLiteral("kt-chunks"), i18n("Displays all the chunks you are downloading, of a torrent")); + + cd_view->loadState(KSharedConfig::openConfig()); + cd_view->changeTC(tc); + createMonitor(tc); + } else if (!show && cd_view) { + cd_view->saveState(KSharedConfig::openConfig()); + ta->removeToolWidget(cd_view); + delete cd_view; + cd_view = nullptr; + createMonitor(tc); + } +} + +void InfoWidgetPlugin::showTrackerView(bool show) +{ + TorrentActivityInterface *ta = getGUI()->getTorrentActivity(); + if (show && !tracker_view) { + tracker_view = new TrackerView(nullptr); + ta->addToolWidget(tracker_view, i18n("Trackers"), QStringLiteral("network-server"), i18n("Displays information about all the trackers of a torrent")); + tracker_view->loadState(KSharedConfig::openConfig()); + tracker_view->changeTC(ta->getCurrentTorrent()); + } else if (!show && tracker_view) { + tracker_view->saveState(KSharedConfig::openConfig()); + ta->removeToolWidget(tracker_view); + delete tracker_view; + tracker_view = nullptr; + } +} + +void InfoWidgetPlugin::showWebSeedsTab(bool show) +{ + TorrentActivityInterface *ta = getGUI()->getTorrentActivity(); + if (show && !webseeds_tab) { + webseeds_tab = new WebSeedsTab(nullptr); + ta->addToolWidget(webseeds_tab, i18n("Webseeds"), QStringLiteral("network-server"), i18n("Displays all the webseeds of a torrent")); + webseeds_tab->loadState(KSharedConfig::openConfig()); + webseeds_tab->changeTC(ta->getCurrentTorrent()); + } else if (!show && webseeds_tab) { + webseeds_tab->saveState(KSharedConfig::openConfig()); + ta->removeToolWidget(webseeds_tab); + delete webseeds_tab; + webseeds_tab = nullptr; + } +} + +void InfoWidgetPlugin::createMonitor(bt::TorrentInterface *tc) +{ + delete monitor; + monitor = nullptr; + + if (peer_view) + peer_view->removeAll(); + if (cd_view) + cd_view->removeAll(); + + if (tc && (peer_view || cd_view)) + monitor = new Monitor(tc, peer_view, cd_view, file_view); +} + +void InfoWidgetPlugin::torrentRemoved(bt::TorrentInterface *tc) +{ + file_view->onTorrentRemoved(tc); + // for some reason currentTorrentChanged doesn't always get called + // when the current torrent is removed, this leads to crashes + // so manually call it here, to prevent crashes + currentTorrentChanged(getGUI()->getTorrentActivity()->getCurrentTorrent()); +} + +} + +#include "infowidgetplugin.moc" diff --git a/plugins/infowidget/infowidgetplugin.h b/plugins/infowidget/infowidgetplugin.h new file mode 100644 index 0000000..ec4e425 --- /dev/null +++ b/plugins/infowidget/infowidgetplugin.h @@ -0,0 +1,75 @@ +/* + SPDX-FileCopyrightText: 2005 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KTINFOWIDGETPLUGIN_H +#define KTINFOWIDGETPLUGIN_H + +#include +#include +#include + +namespace bt +{ +class TorrentInterface; +} + +namespace kt +{ +class PeerView; +class TrackerView; +class StatusTab; +class FileView; +class ChunkDownloadView; +class IWPrefPage; +class Monitor; +class WebSeedsTab; + +/** +@author Joris Guisson +*/ +class InfoWidgetPlugin : public Plugin, public ViewListener +{ + Q_OBJECT +public: + InfoWidgetPlugin(QObject *parent, const QVariantList &args); + ~InfoWidgetPlugin() override; + + void load() override; + void unload() override; + void guiUpdate() override; + void currentTorrentChanged(bt::TorrentInterface *tc) override; + bool versionCheck(const QString &version) const override; + + /// Show PeerView in main window + void showPeerView(bool show); + /// Show ChunkDownloadView in main window + void showChunkView(bool show); + /// Show TrackerView in main window + void showTrackerView(bool show); + /// Show WebSeedsTab in main window + void showWebSeedsTab(bool show); + +private: + void createMonitor(bt::TorrentInterface *tc); + +private Q_SLOTS: + void applySettings(); + void torrentRemoved(bt::TorrentInterface *tc); + +private: + PeerView *peer_view; + ChunkDownloadView *cd_view; + TrackerView *tracker_view; + FileView *file_view; + StatusTab *status_tab; + WebSeedsTab *webseeds_tab; + Monitor *monitor; + + IWPrefPage *pref; +}; + +} + +#endif diff --git a/plugins/infowidget/infowidgetpluginsettings.kcfgc b/plugins/infowidget/infowidgetpluginsettings.kcfgc new file mode 100644 index 0000000..39b3109 --- /dev/null +++ b/plugins/infowidget/infowidgetpluginsettings.kcfgc @@ -0,0 +1,7 @@ +# Code generation options for kconfig_compiler +File=ktinfowidgetplugin.kcfg +ClassName=InfoWidgetPluginSettings +Namespace=kt +Singleton=true +Mutators=true +# will create the necessary code for setting those variables diff --git a/plugins/infowidget/iwfilelistmodel.cpp b/plugins/infowidget/iwfilelistmodel.cpp new file mode 100644 index 0000000..516a33e --- /dev/null +++ b/plugins/infowidget/iwfilelistmodel.cpp @@ -0,0 +1,275 @@ +/* + SPDX-FileCopyrightText: 2007 Joris Guisson + SPDX-FileCopyrightText: 2007 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "iwfilelistmodel.h" + +#include + +#include + +#include "infowidgetpluginsettings.h" +#include +#include +#include +#include + +using namespace bt; + +namespace kt +{ +IWFileListModel::IWFileListModel(bt::TorrentInterface *tc, QObject *parent) + : TorrentFileListModel(tc, KEEP_FILES, parent) +{ + mmfile = tc ? IsMultimediaFile(tc->getStats().output_path) : 0; + preview = false; + percentage = 0; +} + +IWFileListModel::~IWFileListModel() +{ +} + +void IWFileListModel::changeTorrent(bt::TorrentInterface *tc) +{ + kt::TorrentFileListModel::changeTorrent(tc); + mmfile = tc ? IsMultimediaFile(tc->getStats().output_path) : 0; + preview = false; + percentage = 0; +} + +int IWFileListModel::columnCount(const QModelIndex &parent) const +{ + if (!parent.isValid()) + return 5; + else + return 0; +} + +QVariant IWFileListModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if (role != Qt::DisplayRole || orientation != Qt::Horizontal) + return QVariant(); + + if (section < 2) + return TorrentFileListModel::headerData(section, orientation, role); + + switch (section) { + case 2: + return i18n("Priority"); + case 3: + return i18nc("@title:column", "Preview"); + // xgettext: no-c-format + case 4: + return i18nc("Percent of File Downloaded", "% Complete"); + default: + return QVariant(); + } +} + +static QString PriorityString(const bt::TorrentFileInterface *file) +{ + switch (file->getPriority()) { + case FIRST_PREVIEW_PRIORITY: + case FIRST_PRIORITY: + return i18nc("Download first", "First"); + case LAST_PREVIEW_PRIORITY: + case LAST_PRIORITY: + return i18nc("Download last", "Last"); + case ONLY_SEED_PRIORITY: + case EXCLUDED: + return QString(); + case NORMAL_PREVIEW_PRIORITY: + default: + return i18nc("Download Normal (not as first or last)", "Normal"); + } +} + +QVariant IWFileListModel::data(const QModelIndex &index, int role) const +{ + if (index.column() < 2 && role != Qt::ForegroundRole) + return TorrentFileListModel::data(index, role); + + if (!tc || !index.isValid() || index.row() >= rowCount(QModelIndex())) + return QVariant(); + + if (role == Qt::ForegroundRole && index.column() == 2 && tc->getStats().multi_file_torrent) { + const bt::TorrentFileInterface *file = &tc->getTorrentFile(index.row()); + switch (file->getPriority()) { + case FIRST_PREVIEW_PRIORITY: + case FIRST_PRIORITY: + return InfoWidgetPluginSettings::firstColor(); + case LAST_PREVIEW_PRIORITY: + case LAST_PRIORITY: + return InfoWidgetPluginSettings::lastColor(); + case NORMAL_PREVIEW_PRIORITY: + case NORMAL_PRIORITY: + return QVariant(); + case ONLY_SEED_PRIORITY: + case EXCLUDED: + default: + return QVariant(); + } + } + + if (role == Qt::DisplayRole) + return displayData(index); + else if (role == Qt::UserRole) + return sortData(index); + + return QVariant(); +} + +QVariant IWFileListModel::displayData(const QModelIndex &index) const +{ + if (tc->getStats().multi_file_torrent) { + const bt::TorrentFileInterface *file = &tc->getTorrentFile(index.row()); + switch (index.column()) { + case 2: + return PriorityString(file); + case 3: + if (file->isMultimedia()) { + if (file->isPreviewAvailable()) + return i18nc("Preview available", "Available"); + else + return i18nc("Preview pending", "Pending"); + } else + return i18nc("No preview available", "No"); + case 4: { + float percent = file->getDownloadPercentage(); + return ki18n("%1 %").subs(percent, 0, 'f', 2).toString(); + } + default: + return QVariant(); + } + } else { + switch (index.column()) { + case 2: + return QVariant(); + case 3: + if (mmfile) { + if (tc->readyForPreview()) + return i18nc("Preview available", "Available"); + else + return i18nc("Preview pending", "Pending"); + } else + return i18nc("No preview available", "No"); + case 4: { + double percent = bt::Percentage(tc->getStats()); + return ki18n("%1 %").subs(percent, 0, 'f', 2).toString(); + } + default: + return QVariant(); + } + } + return QVariant(); +} + +QVariant IWFileListModel::sortData(const QModelIndex &index) const +{ + if (tc->getStats().multi_file_torrent) { + const bt::TorrentFileInterface *file = &tc->getTorrentFile(index.row()); + switch (index.column()) { + case 2: + return (int)file->getPriority(); + case 3: + if (file->isMultimedia()) { + if (file->isPreviewAvailable()) + return 3; + else + return 2; + } else + return 1; + case 4: + return file->getDownloadPercentage(); + } + } else { + switch (index.column()) { + case 2: + return QVariant(); + case 3: + if (mmfile) { + if (tc->readyForPreview()) + return 3; + else + return 2; + } else + return 1; + case 4: + return bt::Percentage(tc->getStats()); + } + } + return QVariant(); +} + +bool IWFileListModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + if (role == Qt::CheckStateRole) + return TorrentFileListModel::setData(index, value, role); + + if (!tc || !index.isValid() || role != Qt::UserRole) + return false; + + int r = index.row(); + if (r < 0 || r >= rowCount(QModelIndex())) + return false; + + bt::TorrentFileInterface &file = tc->getTorrentFile(r); + ; + Priority prio = (bt::Priority)value.toInt(); + Priority old = file.getPriority(); + + if (prio != old) { + file.setPriority(prio); + dataChanged(createIndex(index.row(), 0), createIndex(index.row(), 4)); + } + + return true; +} + +void IWFileListModel::filePercentageChanged(bt::TorrentFileInterface *file, float percentage) +{ + Q_UNUSED(percentage); + if (!tc) + return; + + QModelIndex idx = createIndex(file->getIndex(), 4, file); + Q_EMIT dataChanged(idx, idx); +} + +void IWFileListModel::filePreviewChanged(bt::TorrentFileInterface *file, bool preview) +{ + Q_UNUSED(preview); + if (!tc) + return; + + QModelIndex idx = createIndex(file->getIndex(), 3, file); + Q_EMIT dataChanged(idx, idx); +} + +void IWFileListModel::update() +{ + if (!tc) + return; + + if (!tc->getStats().multi_file_torrent) { + bool changed = false; + bool np = mmfile && tc->readyForPreview(); + if (preview != np) { + preview = np; + changed = true; + } + + double perc = bt::Percentage(tc->getStats()); + if (fabs(perc - percentage) > 0.001) { + percentage = perc; + changed = true; + } + + if (changed) + dataChanged(createIndex(0, 0), createIndex(0, 4)); + } +} +} diff --git a/plugins/infowidget/iwfilelistmodel.h b/plugins/infowidget/iwfilelistmodel.h new file mode 100644 index 0000000..69df69d --- /dev/null +++ b/plugins/infowidget/iwfilelistmodel.h @@ -0,0 +1,49 @@ +/* + SPDX-FileCopyrightText: 2007 Joris Guisson + SPDX-FileCopyrightText: 2007 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KTIWFILELISTMODEL_H +#define KTIWFILELISTMODEL_H + +#include + +namespace kt +{ +/** + * + * @author Joris Guisson + * + * Expands the standard TorrentFileListModel to show more information. + */ +class IWFileListModel : public TorrentFileListModel +{ + Q_OBJECT +public: + IWFileListModel(bt::TorrentInterface *tc, QObject *parent); + ~IWFileListModel() override; + + void changeTorrent(bt::TorrentInterface *tc) override; + int columnCount(const QModelIndex &parent) const override; + QVariant headerData(int section, Qt::Orientation orientation, int role) const override; + QVariant data(const QModelIndex &index, int role) const override; + bool setData(const QModelIndex &index, const QVariant &value, int role) override; + void update() override; + + void filePercentageChanged(bt::TorrentFileInterface *file, float percentage) override; + void filePreviewChanged(bt::TorrentFileInterface *file, bool preview) override; + +private: + QVariant displayData(const QModelIndex &index) const; + QVariant sortData(const QModelIndex &index) const; + +private: + bool preview; + bool mmfile; + double percentage; +}; + +} + +#endif diff --git a/plugins/infowidget/iwfiletreemodel.cpp b/plugins/infowidget/iwfiletreemodel.cpp new file mode 100644 index 0000000..6bd6a24 --- /dev/null +++ b/plugins/infowidget/iwfiletreemodel.cpp @@ -0,0 +1,330 @@ +/* + SPDX-FileCopyrightText: 2007 Joris Guisson + SPDX-FileCopyrightText: 2007 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "iwfiletreemodel.h" + +#include + +#include +#include + +#include "infowidgetpluginsettings.h" +#include +#include +#include +#include + +using namespace bt; + +namespace kt +{ +IWFileTreeModel::IWFileTreeModel(bt::TorrentInterface *tc, QObject *parent) + : TorrentFileTreeModel(tc, KEEP_FILES, parent) +{ + mmfile = tc ? IsMultimediaFile(tc->getStats().output_path) : 0; + preview = false; + percentage = 0; + + if (root && tc) { + BitSet d = tc->downloadedChunksBitSet(); + d -= tc->onlySeedChunksBitSet(); + root->initPercentage(tc, d); + } +} + +IWFileTreeModel::~IWFileTreeModel() +{ +} + +void IWFileTreeModel::changeTorrent(bt::TorrentInterface *tc) +{ + kt::TorrentFileTreeModel::changeTorrent(tc); + mmfile = tc ? IsMultimediaFile(tc->getStats().output_path) : 0; + preview = false; + percentage = 0; + + if (root && tc) { + BitSet d = tc->downloadedChunksBitSet(); + d -= tc->onlySeedChunksBitSet(); + root->initPercentage(tc, d); + } +} + +int IWFileTreeModel::columnCount(const QModelIndex & /*parent*/) const +{ + return 5; +} + +QVariant IWFileTreeModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if (role != Qt::DisplayRole || orientation != Qt::Horizontal) + return QVariant(); + + if (section < 2) + return TorrentFileTreeModel::headerData(section, orientation, role); + + switch (section) { + case 2: + return i18n("Priority"); + case 3: + return i18nc("@title:column", "Preview"); + // xgettext: no-c-format + case 4: + return i18nc("Percent of File Downloaded", "% Complete"); + default: + return QVariant(); + } +} + +static QString PriorityString(const bt::TorrentFileInterface *file) +{ + switch (file->getPriority()) { + case FIRST_PREVIEW_PRIORITY: + case FIRST_PRIORITY: + return i18nc("Download first", "First"); + case LAST_PREVIEW_PRIORITY: + case LAST_PRIORITY: + return i18nc("Download last", "Last"); + case ONLY_SEED_PRIORITY: + case EXCLUDED: + return QString(); + case NORMAL_PREVIEW_PRIORITY: + default: + return i18nc("Download normally(not as first or last)", "Normal"); + } +} + +QVariant IWFileTreeModel::data(const QModelIndex &index, int role) const +{ + Node *n = nullptr; + if (index.column() < 2 && role != Qt::ForegroundRole) + return TorrentFileTreeModel::data(index, role); + + if (!tc || !index.isValid() || !(n = (Node *)index.internalPointer())) + return QVariant(); + + if (role == Qt::ForegroundRole && index.column() == 2 && tc->getStats().multi_file_torrent && n->file) { + const bt::TorrentFileInterface *file = n->file; + switch (file->getPriority()) { + case FIRST_PREVIEW_PRIORITY: + case FIRST_PRIORITY: + return InfoWidgetPluginSettings::firstColor(); + case LAST_PREVIEW_PRIORITY: + case LAST_PRIORITY: + return InfoWidgetPluginSettings::lastColor(); + case NORMAL_PREVIEW_PRIORITY: + case NORMAL_PRIORITY: + return QVariant(); + case ONLY_SEED_PRIORITY: + case EXCLUDED: + default: + return QVariant(); + } + } + + if (role == Qt::DisplayRole) + return displayData(n, index); + else if (role == Qt::UserRole) + return sortData(n, index); + + return QVariant(); +} + +QVariant IWFileTreeModel::displayData(Node *n, const QModelIndex &index) const +{ + if (tc->getStats().multi_file_torrent && n->file) { + const bt::TorrentFileInterface *file = n->file; + switch (index.column()) { + case 2: + return PriorityString(file); + case 3: + if (file->isMultimedia()) { + if (file->isPreviewAvailable()) + return i18nc("preview available", "Available"); + else + return i18nc("Preview pending", "Pending"); + } else + return i18nc("No preview available", "No"); + case 4: + if (file->getPriority() == ONLY_SEED_PRIORITY || file->getPriority() == EXCLUDED) + return QVariant(); + else + return ki18n("%1 %").subs(n->percentage, 0, 'f', 2).toString(); + default: + return QVariant(); + } + } else if (!tc->getStats().multi_file_torrent) { + switch (index.column()) { + case 2: + return QVariant(); + case 3: + if (mmfile) { + if (tc->readyForPreview()) + return i18nc("Preview available", "Available"); + else + return i18nc("Preview pending", "Pending"); + } else + return i18nc("No preview available", "No"); + case 4: + return ki18n("%1 %").subs(bt::Percentage(tc->getStats()), 0, 'f', 2).toString(); + default: + return QVariant(); + } + } else if (tc->getStats().multi_file_torrent && index.column() == 4) { + return ki18n("%1 %").subs(n->percentage, 0, 'f', 2).toString(); + } + + return QVariant(); +} + +QVariant IWFileTreeModel::sortData(Node *n, const QModelIndex &index) const +{ + if (tc->getStats().multi_file_torrent && n->file) { + const bt::TorrentFileInterface *file = n->file; + switch (index.column()) { + case 2: + return (int)file->getPriority(); + case 3: + if (file->isMultimedia()) { + if (file->isPreviewAvailable()) + return 3; + else + return 2; + } else + return 1; + case 4: + return n->percentage; + } + } else if (!tc->getStats().multi_file_torrent) { + switch (index.column()) { + case 2: + return QVariant(); + case 3: + if (mmfile) { + if (tc->readyForPreview()) + return 3; + else + return 2; + } else + return 1; + case 4: + return bt::Percentage(tc->getStats()); + } + } else if (tc->getStats().multi_file_torrent && index.column() == 4) { + return n->percentage; + } + + return QVariant(); +} + +void IWFileTreeModel::changePriority(const QModelIndexList &indexes, Priority newpriority) +{ + if (!tc) + return; + + for (const QModelIndex &idx : indexes) { + Node *n = (Node *)idx.internalPointer(); + if (n) + setPriority(n, newpriority, true); + } +} + +void IWFileTreeModel::setPriority(TorrentFileTreeModel::Node *n, Priority newpriority, bool selected_node) +{ + if (!n->file) { + for (int i = 0; i < n->children.count(); i++) { + // recurse down the tree + setPriority(n->children.at(i), newpriority, false); + } + + Q_EMIT dataChanged(createIndex(n->row(), 0, n), createIndex(n->row(), 4, n)); + } else { + bt::TorrentFileInterface *file = n->file; + Priority old = file->getPriority(); + + // When recursing down the tree don't reinclude files + if ((old == EXCLUDED || old == ONLY_SEED_PRIORITY) && !selected_node) + return; + + if (newpriority != old) { + file->setPriority(newpriority); + Q_EMIT dataChanged(createIndex(n->row(), 0, n), createIndex(n->row(), 4, n)); + } + } +} + +void IWFileTreeModel::filePercentageChanged(bt::TorrentFileInterface *file, float percentage) +{ + Q_UNUSED(percentage); + if (tc) + update(index(0, 0, QModelIndex()), file, 4); +} + +void IWFileTreeModel::filePreviewChanged(bt::TorrentFileInterface *file, bool preview) +{ + Q_UNUSED(preview); + if (tc) + update(index(0, 0, QModelIndex()), file, 3); +} + +void IWFileTreeModel::update(const QModelIndex &idx, bt::TorrentFileInterface *file, int col) +{ + if (!tc) + return; + + Node *n = (Node *)idx.internalPointer(); + if (n->file && n->file == file) { + QModelIndex i = createIndex(idx.row(), col, n); + Q_EMIT dataChanged(i, i); + if (col == 4) { + // update percentages along the tree + // this will go back up the tree and update the percentage of + // all directories involved + BitSet d = tc->downloadedChunksBitSet(); + d -= tc->onlySeedChunksBitSet(); + n->updatePercentage(d); + + // emit necessary signals + QModelIndex parent = idx.parent(); + while (parent.isValid()) { + Node *nd = (Node *)parent.internalPointer(); + i = createIndex(parent.row(), 4, nd); + Q_EMIT dataChanged(i, i); + parent = parent.parent(); + } + } + } else { + for (int i = 0; i < n->children.count(); i++) { + // recurse down the tree + update(this->index(i, 0, idx), file, col); + } + } +} + +void IWFileTreeModel::update() +{ + if (!tc) + return; + + if (!tc->getStats().multi_file_torrent) { + bool changed = false; + bool np = mmfile && tc->readyForPreview(); + if (preview != np) { + preview = np; + changed = true; + } + + double perc = bt::Percentage(tc->getStats()); + if (fabs(perc - percentage) > 0.001) { + percentage = perc; + changed = true; + } + + if (changed) + dataChanged(createIndex(0, 2), createIndex(0, 4)); + } +} +} diff --git a/plugins/infowidget/iwfiletreemodel.h b/plugins/infowidget/iwfiletreemodel.h new file mode 100644 index 0000000..696f823 --- /dev/null +++ b/plugins/infowidget/iwfiletreemodel.h @@ -0,0 +1,51 @@ +/* + SPDX-FileCopyrightText: 2007 Joris Guisson + SPDX-FileCopyrightText: 2007 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KTIWFILETREEMODEL_H +#define KTIWFILETREEMODEL_H + +#include + +namespace kt +{ +/** + * + * @author Joris Guisson + * + * Expands the standard TorrentFileTreeModel to show more information. + */ +class IWFileTreeModel : public TorrentFileTreeModel +{ + Q_OBJECT +public: + IWFileTreeModel(bt::TorrentInterface *tc, QObject *parent); + ~IWFileTreeModel() override; + + void changeTorrent(bt::TorrentInterface *tc) override; + int columnCount(const QModelIndex &parent) const override; + QVariant headerData(int section, Qt::Orientation orientation, int role) const override; + QVariant data(const QModelIndex &index, int role) const override; + void update() override; + void changePriority(const QModelIndexList &indexes, bt::Priority newpriority) override; + + void filePercentageChanged(bt::TorrentFileInterface *file, float percentage) override; + void filePreviewChanged(bt::TorrentFileInterface *file, bool preview) override; + +private: + void update(const QModelIndex &index, bt::TorrentFileInterface *file, int col); + QVariant displayData(Node *n, const QModelIndex &index) const; + QVariant sortData(Node *n, const QModelIndex &index) const; + void setPriority(Node *n, bt::Priority newpriority, bool selected_node); + +private: + bool preview; + bool mmfile; + double percentage; +}; + +} + +#endif diff --git a/plugins/infowidget/iwprefpage.cpp b/plugins/infowidget/iwprefpage.cpp new file mode 100644 index 0000000..cead78a --- /dev/null +++ b/plugins/infowidget/iwprefpage.cpp @@ -0,0 +1,20 @@ +/* + SPDX-FileCopyrightText: 2007 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "iwprefpage.h" +#include "infowidgetpluginsettings.h" + +namespace kt +{ +IWPrefPage::IWPrefPage(QWidget *parent) + : PrefPageInterface(InfoWidgetPluginSettings::self(), i18n("Info Widget"), QStringLiteral("ktip"), parent) +{ + setupUi(this); +} + +IWPrefPage::~IWPrefPage() +{ +} +} diff --git a/plugins/infowidget/iwprefpage.h b/plugins/infowidget/iwprefpage.h new file mode 100644 index 0000000..18c8c5a --- /dev/null +++ b/plugins/infowidget/iwprefpage.h @@ -0,0 +1,23 @@ +/* + SPDX-FileCopyrightText: 2007 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KTIWPREFPAGE_HH +#define KTIWPREFPAGE_HH + +#include "ui_iwprefpage.h" +#include + +namespace kt +{ +class IWPrefPage : public PrefPageInterface, public Ui_IWPrefPage +{ + Q_OBJECT +public: + IWPrefPage(QWidget *parent); + ~IWPrefPage() override; +}; +} + +#endif diff --git a/plugins/infowidget/iwprefpage.ui b/plugins/infowidget/iwprefpage.ui new file mode 100644 index 0000000..8e80627 --- /dev/null +++ b/plugins/infowidget/iwprefpage.ui @@ -0,0 +1,124 @@ + + + IWPrefPage + + + + 0 + 0 + 400 + 471 + + + + + + + Tabs + + + + + + Whether or not to show the peers tab in the bottom of the window. + + + Show list of peers + + + + + + + Whether or not to show the chunks tab in the bottom of the window. + + + Show list of chunks currently downloading + + + + + + + Whether or not to show the trackers tab in the bottom of the window. + + + Show list of trackers + + + + + + + Whether or not to show the webseeds tab at the bottom of the window. + + + Show list of webseeds + + + + + + + + + + File Priority Colors + + + + + + First priority: + + + + + + + Color to use for first priority files. + + + + + + + Last priority: + + + + + + + Color to use for last priority files. + + + + + + + + + + Qt::Vertical + + + + 392 + 51 + + + + + + + + + KColorButton + QPushButton +
kcolorbutton.h
+
+
+ + +
diff --git a/plugins/infowidget/ktinfowidgetplugin.kcfg b/plugins/infowidget/ktinfowidgetplugin.kcfg new file mode 100644 index 0000000..3f54054 --- /dev/null +++ b/plugins/infowidget/ktinfowidgetplugin.kcfg @@ -0,0 +1,31 @@ + + + + + + + + true + + + + true + + + + true + + + true + + + QColor() + + + QColor() + + + diff --git a/plugins/infowidget/ktorrent_infowidget.desktop b/plugins/infowidget/ktorrent_infowidget.desktop new file mode 100644 index 0000000..ff61eb0 --- /dev/null +++ b/plugins/infowidget/ktorrent_infowidget.desktop @@ -0,0 +1,112 @@ +[Desktop Entry] +Name=Information Widget +Name[ar]=ودجة المعلومات +Name[ast]=Widget d'información +Name[bg]=Джаджа с данни +Name[bs]=Widget podataka +Name[ca]=Giny d'informació +Name[ca@valencia]=Giny d'informació +Name[cs]=Informační widget +Name[da]=Informations-widget +Name[de]=Informationen +Name[el]=Συστατικό πληροφοριών +Name[en_GB]=Information Widget +Name[es]=Elemento de información +Name[et]=Teabevidim +Name[fi]=Tietoliitännäinen +Name[fr]=Composant graphique d'informations +Name[ga]=Giuirléid Eolais +Name[gl]=Trebello de información +Name[hr]=Informacijski widget +Name[hu]=Információs kisalkalmazás +Name[ia]=Widget de Information +Name[is]=Upplýsingagræja +Name[it]=Pannello informazioni +Name[ja]=情報ウィジェット +Name[kk]=Ақпарат виджеті +Name[km]=ធាតុ​ក្រាហ្វិក​របស់​ព័ត៌មាន +Name[ko]=정보 위젯 +Name[lt]=Informacinis valdiklis +Name[lv]=Informācijas sīkrīks +Name[mr]=माहिती विजेट +Name[nb]=Informasjonselement +Name[nds]=Informatschonen-Element +Name[nl]=Informatie-widget +Name[nn]=Informasjonsvindauge +Name[pl]=Informacyjny element interfejsu +Name[pt]=Item de Informação +Name[pt_BR]=Widget de informações +Name[ro]=Control grafic informativ +Name[ru]=Сведения +Name[si]=තොරතුරු විජෙට්ටුව +Name[sk]=Informačný komponent +Name[sl]=Gradnik s podrobnostmi +Name[sr]=Виџет података +Name[sr@ijekavian]=Виџет података +Name[sr@ijekavianlatin]=Vidžet podataka +Name[sr@latin]=Vidžet podataka +Name[sv]=Informationskomponent +Name[tr]=Bilgi Penceresi +Name[uk]=Інформаційний віджет +Name[x-test]=xxInformation Widgetxx +Name[zh_CN]=信息部件 +Name[zh_TW]=資訊元件 +Comment=Displays general information about a torrent in several tabs +Comment[ar]=تعرض معلومات عامة عن أحد السّيول في عدّة ألسنة +Comment[bg]=Показване на различни данни за торентите в няколко подпрозореца +Comment[bs]=Prikazuje opšte podatke o torentu u nekoliko kartica +Comment[ca]=Mostra informació general quant a un torrent en diverses pestanyes +Comment[ca@valencia]=Mostra informació general quant a un torrent en diverses pestanyes +Comment[cs]=Zobrazuje v kartách obecné informace o torrentu +Comment[da]=Viser generel information om en torrent i flere faneblade +Comment[de]=Anzeige von allgemeinen Informationen über einen Torrent in mehreren Unterfenstern +Comment[el]=Εμφάνιση γενικών πληροφοριών για ένα torrent σε διάφορες καρτέλες +Comment[en_GB]=Displays general information about a torrent in several tabs +Comment[es]=Muestra información general sobre un torrent en varias pestañas +Comment[et]=Torrenti üldise teabe kuvamine mitmel kaardil +Comment[fi]=Näyttää eri välilehdillä yleistietoja torrentista +Comment[fr]=Affiche des informations générales sur un torrent dans plusieurs onglets +Comment[ga]=Breiseán a thaispeánann eolas ginearálta maidir le torrent i gcluaisíní éagsúla +Comment[gl]=Mostrar información sobre un torrente en varios separadores +Comment[hu]=Általános információk megjelenítése több lapon egy torrentről +Comment[ia]=Monstra information general re un torrnt in multe schedas +Comment[is]=Birtir í nokkrum flipum almennar upplýsingar um torrent straum +Comment[it]=Visualizza in diverse schede informazioni generali sul torrent +Comment[ja]=torrent に関する一般的な情報を複数のタブに表示します +Comment[kk]=Бірнеше қойындыда торрент туралы жалпы ақпарат беру +Comment[km]=បង្ហាញ​ព័ត៌មាន​ទូទៅ​អំពី torrent ក្នុង​ច្រើន​ផ្ទាំង +Comment[ko]=여러 탭에 토렌트 일반 정보 표시 +Comment[lt]=Pateikia bendrą informaciją apie torrent failą keliose kortelėse +Comment[lv]=Vairākās cilnēs parāda pamata informāciju par torrentu +Comment[nb]=Viser generell informasjon om en strøm i flere faner +Comment[nds]=Wiest Informatschonen över en Torrent binnen en Reeg Paneels +Comment[nl]=Geef informatie over een torrent, verdeeld over meerdere tabbladen +Comment[nn]=Viser generell informasjon om ein torrent i fleire faner +Comment[pl]=Wyświetlanie informacji o torrencie w osobnych kartach +Comment[pt]=Um 'plugin' que mostra informações gerais sobre uma torrente em várias páginas +Comment[pt_BR]=Exibe informações gerais sobre um torrent em diversas abas +Comment[ro]=Afișează informații generale despre un torent în cîteva file +Comment[ru]=Показывает разные сведения о торрентах, распределяя их по нескольким вкладкам +Comment[si]=ටොරෙන්ටයක් පිලිබඳ සාමාන්‍යය තොරතුරු ටැබ කිහිපයක පෙන්වයි +Comment[sk]=Ukáže základné informácie o torrente v niekoľkých záložkách +Comment[sl]=V več zavihkih prikazuje splošne podrobnosti o torentu +Comment[sr]=Приказује опште податке о торенту у неколико језичака +Comment[sr@ijekavian]=Приказује опште податке о торенту у неколико језичака +Comment[sr@ijekavianlatin]=Prikazuje opšte podatke o torentu u nekoliko jezičaka +Comment[sr@latin]=Prikazuje opšte podatke o torentu u nekoliko jezičaka +Comment[sv]=Visa allmän information om ett dataflöde under flera flikar +Comment[tr]=Çeşitli sekmelerde bir torrent hakkındaki genel bilgileri gösterir +Comment[uk]=Показ загальної інформації про торент у декількох вкладках +Comment[x-test]=xxDisplays general information about a torrent in several tabsxx +Comment[zh_CN]=在若干标签内显示一个种子的常规信息 +Comment[zh_TW]=使用數個分頁顯示 Torrent 的一般資訊 +Type=Service +X-KDE-Library=ktinfowidgetplugin +X-KDE-PluginInfo-Author=Joris Guisson, Ivan Vasic +X-KDE-PluginInfo-Email=joris.guisson@gmail.com, ivasic@gmail.com +X-KDE-PluginInfo-Name=InfoWidgetPlugin +X-KDE-PluginInfo-Version=0.1 +X-KDE-PluginInfo-Website=http://kde.org/applications/internet/ktorrent/ +X-KDE-PluginInfo-License=GPL +X-KDE-PluginInfo-EnabledByDefault=true +Icon=kt-info-widget diff --git a/plugins/infowidget/monitor.cpp b/plugins/infowidget/monitor.cpp new file mode 100644 index 0000000..0456849 --- /dev/null +++ b/plugins/infowidget/monitor.cpp @@ -0,0 +1,88 @@ +/* + SPDX-FileCopyrightText: 2005-2007 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "monitor.h" + +#include "chunkdownloadview.h" +#include "fileview.h" +#include "peerview.h" +#include +#include +#include +#include + +using namespace bt; + +namespace kt +{ +Monitor::Monitor(bt::TorrentInterface *tc, PeerView *pv, ChunkDownloadView *cdv, FileView *fv) + : tc(tc) + , pv(pv) + , cdv(cdv) + , fv(fv) +{ + if (tc) + tc->setMonitor(this); +} + +Monitor::~Monitor() +{ + if (tc) + tc->setMonitor(nullptr); +} + +void Monitor::downloadRemoved(bt::ChunkDownloadInterface *cd) +{ + if (cdv) + cdv->downloadRemoved(cd); +} + +void Monitor::downloadStarted(bt::ChunkDownloadInterface *cd) +{ + if (cdv) + cdv->downloadAdded(cd); +} + +void Monitor::peerAdded(bt::PeerInterface *peer) +{ + if (pv) + pv->peerAdded(peer); +} + +void Monitor::peerRemoved(bt::PeerInterface *peer) +{ + if (pv) + pv->peerRemoved(peer); +} + +void Monitor::stopped() +{ + if (pv) + pv->removeAll(); + if (cdv) + cdv->removeAll(); +} + +void Monitor::destroyed() +{ + if (pv) + pv->removeAll(); + if (cdv) + cdv->removeAll(); + tc = nullptr; +} + +void Monitor::filePercentageChanged(bt::TorrentFileInterface *file, float percentage) +{ + if (fv) + fv->filePercentageChanged(file, percentage); +} + +void Monitor::filePreviewChanged(bt::TorrentFileInterface *file, bool preview) +{ + if (fv) + fv->filePreviewChanged(file, preview); +} +} diff --git a/plugins/infowidget/monitor.h b/plugins/infowidget/monitor.h new file mode 100644 index 0000000..c37c2a0 --- /dev/null +++ b/plugins/infowidget/monitor.h @@ -0,0 +1,47 @@ +/* + SPDX-FileCopyrightText: 2005-2007 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KTMONITOR_H +#define KTMONITOR_H + +#include + +namespace bt +{ +class TorrentInterface; +} + +namespace kt +{ +class PeerView; +class ChunkDownloadView; +class FileView; + +/** +@author Joris Guisson +*/ +class Monitor : public bt::MonitorInterface +{ + bt::TorrentInterface *tc; + PeerView *pv; + ChunkDownloadView *cdv; + FileView *fv; + +public: + Monitor(bt::TorrentInterface *tc, PeerView *pv, ChunkDownloadView *cdv, FileView *fv); + ~Monitor() override; + + void downloadRemoved(bt::ChunkDownloadInterface *cd) override; + void downloadStarted(bt::ChunkDownloadInterface *cd) override; + void peerAdded(bt::PeerInterface *peer) override; + void peerRemoved(bt::PeerInterface *peer) override; + void stopped() override; + void destroyed() override; + void filePercentageChanged(bt::TorrentFileInterface *file, float percentage) override; + void filePreviewChanged(bt::TorrentFileInterface *file, bool preview) override; +}; +} + +#endif diff --git a/plugins/infowidget/peerview.cpp b/plugins/infowidget/peerview.cpp new file mode 100644 index 0000000..8b72baf --- /dev/null +++ b/plugins/infowidget/peerview.cpp @@ -0,0 +1,122 @@ +/* + SPDX-FileCopyrightText: 2007 Joris Guisson + SPDX-FileCopyrightText: 2007 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "peerview.h" + +#include +#include +#include +#include + +#include +#include + +#include "peerviewmodel.h" +#include +#include +#include + +using namespace bt; + +namespace kt +{ +PeerView::PeerView(QWidget *parent) + : QTreeView(parent) +{ + setContextMenuPolicy(Qt::CustomContextMenu); + setRootIsDecorated(false); + setSortingEnabled(true); + setAlternatingRowColors(true); + setUniformRowHeights(true); + + pm = new QSortFilterProxyModel(this); + pm->setSortRole(Qt::UserRole); + pm->setDynamicSortFilter(true); + model = new PeerViewModel(this); + pm->setSourceModel(model); + setModel(pm); + + context_menu = new QMenu(this); + context_menu->addAction(QIcon::fromTheme(QStringLiteral("list-remove-user")), i18n("Kick Peer"), this, &PeerView::kickPeer); + context_menu->addAction(QIcon::fromTheme(QStringLiteral("view-filter")), i18n("Ban Peer"), this, &PeerView::banPeer); + connect(this, &PeerView::customContextMenuRequested, this, &PeerView::showContextMenu); +} + +PeerView::~PeerView() +{ +} + +void PeerView::showContextMenu(const QPoint &pos) +{ + if (selectionModel()->selectedRows().count() == 0) + return; + + context_menu->popup(viewport()->mapToGlobal(pos)); +} + +void PeerView::banPeer() +{ + AccessManager &aman = AccessManager::instance(); + + const QModelIndexList indices = selectionModel()->selectedRows(); + for (const QModelIndex &idx : indices) { + bt::PeerInterface *peer = model->indexToPeer(pm->mapToSource(idx)); + if (peer) { + aman.banPeer(peer->getStats().ip_address); + peer->kill(); + } + } +} + +void PeerView::kickPeer() +{ + const QModelIndexList indices = selectionModel()->selectedRows(); + for (const QModelIndex &idx : indices) { + bt::PeerInterface *peer = model->indexToPeer(pm->mapToSource(idx)); + if (peer) + peer->kill(); + } +} + +void PeerView::peerAdded(PeerInterface *peer) +{ + model->peerAdded(peer); +} + +void PeerView::peerRemoved(PeerInterface *peer) +{ + model->peerRemoved(peer); +} + +void PeerView::update() +{ + model->update(); +} + +void PeerView::removeAll() +{ + model->clear(); +} + +void PeerView::saveState(KSharedConfigPtr cfg) +{ + KConfigGroup g = cfg->group("PeerView"); + QByteArray s = header()->saveState(); + g.writeEntry("state", s.toBase64()); +} + +void PeerView::loadState(KSharedConfigPtr cfg) +{ + KConfigGroup g = cfg->group("PeerView"); + QByteArray s = QByteArray::fromBase64(g.readEntry("state", QByteArray())); + if (!s.isEmpty()) { + QHeaderView *v = header(); + v->restoreState(s); + sortByColumn(v->sortIndicatorSection(), v->sortIndicatorOrder()); + pm->sort(v->sortIndicatorSection(), v->sortIndicatorOrder()); + } +} +} diff --git a/plugins/infowidget/peerview.h b/plugins/infowidget/peerview.h new file mode 100644 index 0000000..fa11817 --- /dev/null +++ b/plugins/infowidget/peerview.h @@ -0,0 +1,59 @@ +/* + SPDX-FileCopyrightText: 2007 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KT_PEERVIEW_HH +#define KT_PEERVIEW_HH + +#include +#include + +#include +#include + +class QSortFilterProxyModel; +class QMenu; + +namespace kt +{ +class PeerViewModel; + +/** + * View which shows a list of peers, of a torrent. + * */ +class PeerView : public QTreeView +{ + Q_OBJECT +public: + PeerView(QWidget *parent); + ~PeerView() override; + + /// A peer has been added + void peerAdded(bt::PeerInterface *peer); + + /// A peer has been removed + void peerRemoved(bt::PeerInterface *peer); + + /// Check to see if the GUI needs to be updated + void update(); + + /// Remove all items + void removeAll(); + + void saveState(KSharedConfigPtr cfg); + void loadState(KSharedConfigPtr cfg); + +private Q_SLOTS: + void showContextMenu(const QPoint &pos); + void banPeer(); + void kickPeer(); + +private: + QMenu *context_menu; + QSortFilterProxyModel *pm; + PeerViewModel *model; +}; +} + +#endif diff --git a/plugins/infowidget/peerviewmodel.cpp b/plugins/infowidget/peerviewmodel.cpp new file mode 100644 index 0000000..27ca8e5 --- /dev/null +++ b/plugins/infowidget/peerviewmodel.cpp @@ -0,0 +1,410 @@ +/* + SPDX-FileCopyrightText: 2008 Joris Guisson + SPDX-FileCopyrightText: 2008 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "peerviewmodel.h" + +#include +#include +#include + +#include + +#include "flagdb.h" +#include +#include + +#if BUILD_WITH_GEOIP +#include "geoipmanager.h" +#endif + +using namespace bt; + +namespace kt +{ +static QIcon yes, no; +static bool icons_loaded = false; +static FlagDB flagDB(22, 18); + +PeerViewModel::Item::Item(bt::PeerInterface *peer +#if BUILD_WITH_GEOIP + , + GeoIPManager *geo_ip +#endif + ) + : peer(peer) +{ + stats = peer->getStats(); + if (!icons_loaded) { + yes = QIcon::fromTheme(QStringLiteral("dialog-ok")); + no = QIcon::fromTheme(QStringLiteral("dialog-cancel")); + icons_loaded = true; + + QString path = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("kf5/locale/countries"), QStandardPaths::LocateDirectory); + if (!path.isEmpty()) + flagDB.addFlagSource(path + QStringLiteral("/%1/flag.png")); + } + +#if BUILD_WITH_GEOIP + int country_id = geo_ip->findCountry(stats.ip_address); + if (country_id > 0) { + country = geo_ip->countryName(country_id); + flag = flagDB.getFlag(geo_ip->countryCode(country_id)); + } +#endif +} + +bool PeerViewModel::Item::changed() const +{ + const PeerInterface::Stats &s = peer->getStats(); + // clang-format off + bool ret = + s.download_rate != stats.download_rate || + s.upload_rate != stats.upload_rate || + s.choked != stats.choked || + s.snubbed != stats.snubbed || + s.perc_of_file != stats.perc_of_file || + s.aca_score != stats.aca_score || + s.has_upload_slot != stats.has_upload_slot || + s.num_down_requests != stats.num_down_requests || + s.num_up_requests != stats.num_up_requests || + s.bytes_downloaded != stats.bytes_downloaded || + s.bytes_uploaded != stats.bytes_uploaded || + s.interested != stats.interested || + s.am_interested != stats.am_interested; + // clang-format on + stats = s; + return ret; +} + +QVariant PeerViewModel::Item::data(int col) const +{ + switch (col) { + case 0: + if (stats.transport_protocol == bt::UTP) + return QString(stats.address() + i18n(" (µTP)")); + else + return stats.address(); + case 1: + return country; + case 2: + return stats.client; + case 3: + if (stats.download_rate >= 103) + return BytesPerSecToString(stats.download_rate); + else + return QVariant(); + case 4: + if (stats.upload_rate >= 103) + return BytesPerSecToString(stats.upload_rate); + else + return QVariant(); + case 5: + return stats.choked ? i18nc("Choked", "Yes") : i18nc("Not choked", "No"); + case 6: + return stats.snubbed ? i18nc("Snubbed", "Yes") : i18nc("Not snubbed", "No"); + case 7: + return QString(QString::number((int)stats.perc_of_file) + QLatin1String(" %")); + case 8: + return QVariant(); + case 9: + return QLocale().toString(stats.aca_score, 'f', 2); + case 10: + return QVariant(); + case 11: + return QString(QString::number(stats.num_down_requests) + QLatin1String(" / ") + QString::number(stats.num_up_requests)); + case 12: + return BytesToString(stats.bytes_downloaded); + case 13: + return BytesToString(stats.bytes_uploaded); + case 14: + return stats.interested ? i18nc("Interested", "Yes") : i18nc("Not Interested", "No"); + case 15: + return stats.am_interested ? i18nc("Interesting", "Yes") : i18nc("Not Interesting", "No"); + default: + return QVariant(); + } + return QVariant(); +} + +QVariant PeerViewModel::Item::sortData(int col) const +{ + switch (col) { + case 0: + return stats.address(); + case 1: + return country; + case 2: + return stats.client; + case 3: + return stats.download_rate; + case 4: + return stats.upload_rate; + case 5: + return stats.choked; + case 6: + return stats.snubbed; + case 7: + return stats.perc_of_file; + case 8: + return stats.dht_support; + case 9: + return stats.aca_score; + case 10: + return stats.has_upload_slot; + case 11: + return stats.num_down_requests + stats.num_up_requests; + case 12: + return stats.bytes_downloaded; + case 13: + return stats.bytes_uploaded; + case 14: + return stats.interested; + case 15: + return stats.am_interested; + default: + return QVariant(); + } +} + +QVariant PeerViewModel::Item::decoration(int col) const +{ + switch (col) { + case 0: + if (stats.encrypted) + return QIcon::fromTheme(QStringLiteral("kt-encrypted")); + break; + case 1: + return flag; + case 8: + return stats.dht_support ? yes : no; + case 10: + return stats.has_upload_slot ? yes : QIcon(); + } + + return QVariant(); +} + +///////////////////////////////////////////////////////////// + +PeerViewModel::PeerViewModel(QObject *parent) + : QAbstractTableModel(parent) +{ +#if BUILD_WITH_GEOIP + geo_ip = new GeoIPManager(this); +#endif +} + +PeerViewModel::~PeerViewModel() +{ + qDeleteAll(items); +} + +void PeerViewModel::peerAdded(bt::PeerInterface *peer) +{ + items.append(new Item(peer +#if BUILD_WITH_GEOIP + , + geo_ip +#endif + )); + insertRow(items.count() - 1); +} + +void PeerViewModel::peerRemoved(bt::PeerInterface *peer) +{ + int row = 0; + bool found = false; + for (Item *i : qAsConst(items)) { + if (i->peer == peer) { + found = true; + break; + } + row++; + } + + if (found) { + removeRow(row); + } +} + +void PeerViewModel::clear() +{ + beginResetModel(); + qDeleteAll(items); + items.clear(); + endResetModel(); +} + +void PeerViewModel::update() +{ + int idx = 0; + int lowest = -1; + int highest = -1; + + for (Item *i : qAsConst(items)) { + if (i->changed()) { + if (lowest == -1) + lowest = idx; + highest = idx; + } + idx++; + } + + // emit only one data changed signal + if (lowest != -1) + Q_EMIT dataChanged(index(lowest, 3), index(highest, 15)); +} + +QModelIndex PeerViewModel::index(int row, int column, const QModelIndex &parent) const +{ + if (!hasIndex(row, column, parent) || parent.isValid()) + return QModelIndex(); + else + return createIndex(row, column, items[row]); +} + +int PeerViewModel::rowCount(const QModelIndex &parent) const +{ + if (parent.isValid()) + return 0; + else + return items.count(); +} + +int PeerViewModel::columnCount(const QModelIndex &parent) const +{ + if (parent.isValid()) + return 0; + else + return 16; +} + +QVariant PeerViewModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if (orientation != Qt::Horizontal) + return QVariant(); + + if (role == Qt::DisplayRole) { + switch (section) { + case 0: + return i18n("Address"); + case 1: + return i18n("Country"); + case 2: + return i18n("Client"); + case 3: + return i18n("Down Speed"); + case 4: + return i18n("Up Speed"); + case 5: + return i18n("Choked"); + case 6: + return i18n("Snubbed"); + case 7: + return i18n("Availability"); + case 8: + return i18n("DHT"); + case 9: + return i18n("Score"); + case 10: + return i18n("Upload Slot"); + case 11: + return i18n("Requests"); + case 12: + return i18n("Downloaded"); + case 13: + return i18n("Uploaded"); + case 14: + return i18n("Interested"); + case 15: + return i18n("Interesting"); + default: + return QVariant(); + } + } else if (role == Qt::ToolTipRole) { + switch (section) { + case 0: + return i18n("IP address of the peer"); + case 1: + return i18n("Country the peer is in"); + case 2: + return i18n("Which client the peer is using"); + case 3: + return i18n("Download speed"); + case 4: + return i18n("Upload speed"); + case 5: + return i18n("Whether or not the peer has choked us - when we are choked the peer will not send us any data"); + case 6: + return i18n("Snubbed means the peer has not sent us any data in the last 2 minutes"); + case 7: + return i18n("How much data the peer has of the torrent"); + case 8: + return i18n("Whether or not the peer has DHT enabled"); + case 9: + return i18n("The score of the peer, KTorrent uses this to determine who to upload to"); + case 10: + return i18n("Only peers which have an upload slot will get data from us"); + case 11: + return i18n("The number of download and upload requests"); + case 12: + return i18n("How much data we have downloaded from this peer"); + case 13: + return i18n("How much data we have uploaded to this peer"); + case 14: + return i18n("Whether the peer is interested in downloading data from us"); + case 15: + return i18n("Whether we are interested in downloading from this peer"); + default: + return QVariant(); + } + } + + return QVariant(); +} + +QVariant PeerViewModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid() || index.row() >= items.count()) + return QVariant(); + + Item *item = items[index.row()]; + if (role == Qt::DisplayRole) + return item->data(index.column()); + else if (role == Qt::UserRole) + return item->sortData(index.column()); + else if (role == Qt::DecorationRole) + return item->decoration(index.column()); + + return QVariant(); +} + +bool PeerViewModel::removeRows(int row, int count, const QModelIndex & /*parent*/) +{ + beginRemoveRows(QModelIndex(), row, row + count - 1); + for (int i = 0; i < count; i++) + delete items[row + i]; + items.remove(row, count); + endRemoveRows(); + return true; +} + +bool PeerViewModel::insertRows(int row, int count, const QModelIndex & /*parent*/) +{ + beginInsertRows(QModelIndex(), row, row + count - 1); + endInsertRows(); + return true; +} + +bt::PeerInterface *PeerViewModel::indexToPeer(const QModelIndex &index) +{ + if (!index.isValid() || index.row() >= items.count()) + return nullptr; + else + return ((Item *)index.internalPointer())->peer; +} + +} diff --git a/plugins/infowidget/peerviewmodel.h b/plugins/infowidget/peerviewmodel.h new file mode 100644 index 0000000..0213f37 --- /dev/null +++ b/plugins/infowidget/peerviewmodel.h @@ -0,0 +1,86 @@ +/* + SPDX-FileCopyrightText: 2008 Joris Guisson + SPDX-FileCopyrightText: 2008 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KTPEERVIEWMODEL_H +#define KTPEERVIEWMODEL_H + +#include +#include +#include + +#include + +namespace kt +{ +class GeoIPManager; + +/** + @author Joris Guisson + Model for the PeerView +*/ +class PeerViewModel : public QAbstractTableModel +{ + Q_OBJECT +public: + PeerViewModel(QObject *parent); + ~PeerViewModel() override; + + /// A peer has been added + void peerAdded(bt::PeerInterface *peer); + + /// A peer has been removed + void peerRemoved(bt::PeerInterface *peer); + + /** + * Update the model + */ + void update(); + + /** + Clear the model + */ + void clear(); + + int rowCount(const QModelIndex &parent) const override; + int columnCount(const QModelIndex &parent) const override; + QVariant headerData(int section, Qt::Orientation orientation, int role) const override; + QVariant data(const QModelIndex &index, int role) const override; + bool removeRows(int row, int count, const QModelIndex &parent) override; + bool insertRows(int row, int count, const QModelIndex &parent) override; + QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override; + + bt::PeerInterface *indexToPeer(const QModelIndex &idx); + +public: + struct Item { + bt::PeerInterface *peer; + mutable bt::PeerInterface::Stats stats; + QString country; + QIcon flag; + + Item(bt::PeerInterface *peer +#if BUILD_WITH_GEOIP + , + GeoIPManager *geo_ip +#endif + ); + + bool changed() const; + QVariant data(int col) const; + QVariant decoration(int col) const; + QVariant sortData(int col) const; + }; + +private: + QVector items; +#if BUILD_WITH_GEOIP + GeoIPManager *geo_ip = nullptr; +#endif +}; + +} + +#endif diff --git a/plugins/infowidget/statustab.cpp b/plugins/infowidget/statustab.cpp new file mode 100644 index 0000000..0a9c4ad --- /dev/null +++ b/plugins/infowidget/statustab.cpp @@ -0,0 +1,304 @@ +/* + SPDX-FileCopyrightText: 2005-2007 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include + +#include +#include + +#include +#include + +#include "availabilitychunkbar.h" +#include "downloadedchunkbar.h" +#include "settings.h" +#include "statustab.h" +#include +#include +#include + +using namespace bt; + +namespace kt +{ +StatusTab::StatusTab(QWidget *parent) + : QWidget(parent) +{ + setupUi(this); + // do not use hardcoded colors + hdr_info->setBackgroundRole(QPalette::Mid); + hdr_chunks->setBackgroundRole(QPalette::Mid); + hdr_sharing->setBackgroundRole(QPalette::Mid); + + QFont f = font(); + f.setBold(true); + share_ratio->setFont(f); + avg_down_speed->setFont(f); + avg_up_speed->setFont(f); + type->setFont(f); + comments->setFont(f); + info_hash->setFont(f); + + ratio_limit->setMinimum(0.0f); + ratio_limit->setMaximum(100.0f); + ratio_limit->setSingleStep(0.1f); + ratio_limit->setKeyboardTracking(false); + connect(ratio_limit, qOverload(&QDoubleSpinBox::valueChanged), this, &StatusTab::maxRatioChanged); + connect(use_ratio_limit, &QCheckBox::toggled, this, &StatusTab::useRatioLimitToggled); + + time_limit->setMinimum(0.0f); + time_limit->setMaximum(10000000.0f); + time_limit->setSingleStep(0.05f); + time_limit->setSpecialValueText(i18n("No limit")); + time_limit->setKeyboardTracking(false); + connect(use_time_limit, &QCheckBox::toggled, this, &StatusTab::useTimeLimitToggled); + connect(time_limit, qOverload(&QDoubleSpinBox::valueChanged), this, &StatusTab::maxTimeChanged); + + int h = (int)ceil(fontMetrics().height() * 1.25); + downloaded_bar->setFixedHeight(h); + availability_bar->setFixedHeight(h); + + comments->setTextInteractionFlags(Qt::LinksAccessibleByMouse | Qt::LinksAccessibleByKeyboard | Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard); + connect(comments, &KSqueezedTextLabel::linkActivated, this, &StatusTab::linkActivated); + + // initialize everything with curr_tc == 0 + setEnabled(false); + ratio_limit->setValue(0.00f); + share_ratio->clear(); + type->clear(); + comments->clear(); + avg_up_speed->clear(); + avg_down_speed->clear(); + info_hash->clear(); +} + +StatusTab::~StatusTab() +{ +} + +void StatusTab::changeTC(bt::TorrentInterface *tc) +{ + if (tc == curr_tc.data()) + return; + + curr_tc = tc; + + downloaded_bar->setTC(tc); + availability_bar->setTC(tc); + setEnabled(tc != nullptr); + + if (curr_tc) { + info_hash->setText(tc->getInfoHash().toString()); + type->setText(tc->getStats().priv_torrent ? i18n("Private") : i18n("Public")); + + // Don't allow multiple lines in the comments field + QString text = tc->getComments(); + if (text.contains(QLatin1String("\n"))) + text = text.replace(QLatin1Char('\n'), QLatin1Char(' ')); + + // Make links clickable + QStringList words = text.split(QLatin1Char(' '), Qt::KeepEmptyParts); + for (QString &w : words) { + if (w.startsWith(QLatin1String("http://")) || w.startsWith(QLatin1String("https://")) || w.startsWith(QLatin1String("ftp://"))) + w = QStringLiteral("") + w + QStringLiteral(""); + } + + comments->setText(words.join(QStringLiteral(" "))); + + float ratio = tc->getMaxShareRatio(); + if (ratio > 0) { + use_ratio_limit->setChecked(true); + ratio_limit->setValue(ratio); + ratio_limit->setEnabled(true); + } else { + ratio_limit->setValue(0.0); + use_ratio_limit->setChecked(false); + ratio_limit->setEnabled(false); + } + + float hours = tc->getMaxSeedTime(); + if (hours > 0) { + time_limit->setEnabled(true); + use_time_limit->setChecked(true); + time_limit->setValue(hours); + } else { + time_limit->setEnabled(false); + time_limit->setValue(0.0); + use_time_limit->setChecked(false); + } + } else { + info_hash->clear(); + ratio_limit->setValue(0.00f); + time_limit->setValue(0.0); + share_ratio->clear(); + type->clear(); + comments->clear(); + avg_up_speed->clear(); + avg_down_speed->clear(); + } + + update(); +} + +void StatusTab::update() +{ + if (!curr_tc) + return; + + bt::TorrentInterface *tc = curr_tc.data(); + const bt::TorrentStats &s = tc->getStats(); + + downloaded_bar->updateBar(); + availability_bar->updateBar(); + + float ratio = s.shareRatio(); + if (!ratio_limit->hasFocus()) + maxRatioUpdate(); + + if (!time_limit->hasFocus()) + maxSeedTimeUpdate(); + + static QLocale locale; + share_ratio->setText(QStringLiteral("%2") + .arg(ratio <= Settings::greenRatio() ? QStringLiteral("#ff0000") : QStringLiteral("#1c9a1c")) + .arg(locale.toString(ratio, 'f', 2))); + + Uint32 secs = tc->getRunningTimeUL(); + if (secs == 0) { + avg_up_speed->setText(BytesPerSecToString(0)); + } else { + double r = (double)s.bytes_uploaded; + avg_up_speed->setText(BytesPerSecToString(r / secs)); + } + + secs = tc->getRunningTimeDL(); + if (secs == 0) { + avg_down_speed->setText(BytesPerSecToString(0)); + } else { + double r = 0; + if (s.imported_bytes <= s.bytes_downloaded) + r = (double)(s.bytes_downloaded - s.imported_bytes); + else + r = (double)s.bytes_downloaded; + + avg_down_speed->setText(BytesPerSecToString(r / secs)); + } +} + +void StatusTab::maxRatioChanged(double v) +{ + if (!curr_tc) + return; + + curr_tc.data()->setMaxShareRatio(v); +} + +void StatusTab::useRatioLimitToggled(bool state) +{ + if (!curr_tc) + return; + + bt::TorrentInterface *tc = curr_tc.data(); + + ratio_limit->setEnabled(state); + if (!state) { + tc->setMaxShareRatio(0.00f); + ratio_limit->setValue(0.00f); + } else { + float msr = tc->getMaxShareRatio(); + if (msr == 0.00f) { + tc->setMaxShareRatio(1.00f); + ratio_limit->setValue(1.00f); + } + + float sr = tc->getStats().shareRatio(); + if (sr >= 1.00f) { + // always add 1 to max share ratio to prevent stopping if torrent is running. + tc->setMaxShareRatio(sr + 1.00f); + ratio_limit->setValue(sr + 1.00f); + } + } +} + +void StatusTab::maxRatioUpdate() +{ + if (!curr_tc) + return; + + float ratio = curr_tc.data()->getMaxShareRatio(); + if (ratio > 0.00f) { + // only update when needed + if (ratio_limit->isEnabled() && use_ratio_limit->isChecked() && ratio_limit->value() == ratio) + return; + + ratio_limit->setEnabled(true); + use_ratio_limit->setChecked(true); + ratio_limit->setValue(ratio); + } else { + // only update when needed + if (!ratio_limit->isEnabled() && !use_ratio_limit->isChecked() && ratio_limit->value() != 0.00f) + return; + + ratio_limit->setEnabled(false); + use_ratio_limit->setChecked(false); + ratio_limit->setValue(0.00f); + } +} + +void StatusTab::maxSeedTimeUpdate() +{ + if (!curr_tc) + return; + + float time = curr_tc.data()->getMaxSeedTime(); + if (time > 0.00f) { + // only update when needed + if (time_limit->isEnabled() && use_time_limit->isChecked() && time_limit->value() == time) + return; + + time_limit->setEnabled(true); + use_time_limit->setChecked(true); + time_limit->setValue(time); + } else { + // only update when needed + if (!time_limit->isEnabled() && !use_time_limit->isChecked() && time_limit->value() != 0.00f) + return; + + time_limit->setEnabled(false); + use_time_limit->setChecked(false); + time_limit->setValue(0.00f); + } +} + +void StatusTab::useTimeLimitToggled(bool on) +{ + if (!curr_tc) + return; + + bt::TorrentInterface *tc = curr_tc.data(); + time_limit->setEnabled(on); + if (on) { + Uint32 dl = tc->getRunningTimeDL(); + Uint32 ul = tc->getRunningTimeUL(); + float hours = (ul - dl) / 3600.0f + 1.0; // add one hour to current seed time to not stop immediately + time_limit->setValue(hours); + tc->setMaxSeedTime(hours); + } else { + tc->setMaxSeedTime(0.0f); + } +} + +void StatusTab::maxTimeChanged(double v) +{ + if (curr_tc) + curr_tc.data()->setMaxSeedTime(v); +} + +void StatusTab::linkActivated(const QString &link) +{ + new KRun(QUrl(link), QApplication::activeWindow()); +} + +} diff --git a/plugins/infowidget/statustab.h b/plugins/infowidget/statustab.h new file mode 100644 index 0000000..04b6a0d --- /dev/null +++ b/plugins/infowidget/statustab.h @@ -0,0 +1,42 @@ +/* + SPDX-FileCopyrightText: 2005-2007 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef STATUSTAB_H +#define STATUSTAB_H + +#include "ui_statustab.h" +#include +#include +#include + +namespace kt +{ +class StatusTab : public QWidget, public Ui_StatusTab +{ + Q_OBJECT + +public: + StatusTab(QWidget *parent); + ~StatusTab() override; + +public Q_SLOTS: + void changeTC(bt::TorrentInterface *tc); + void update(); + void maxRatioChanged(double v); + void useRatioLimitToggled(bool on); + void useTimeLimitToggled(bool on); + void maxTimeChanged(double v); + void linkActivated(const QString &link); + +private: + void maxRatioUpdate(); + void maxSeedTimeUpdate(); + +private: + QPointer curr_tc; +}; +} + +#endif diff --git a/plugins/infowidget/statustab.ui b/plugins/infowidget/statustab.ui new file mode 100644 index 0000000..501004a --- /dev/null +++ b/plugins/infowidget/statustab.ui @@ -0,0 +1,426 @@ + + + StatusTab + + + + 0 + 0 + 707 + 186 + + + + + + + + 75 + true + + + + true + + + QFrame::NoFrame + + + QFrame::Plain + + + Info + + + + + + + + + Average down speed: + + + + + + + + 100 + 0 + + + + QFrame::NoFrame + + + + + + + + + + Type: + + + + + + + + 100 + 0 + + + + QFrame::NoFrame + + + + + + + + + + Qt::Horizontal + + + + 13 + 41 + + + + + + + + Average up speed: + + + + + + + + 50 + 0 + + + + QFrame::NoFrame + + + + + + + + + + Info hash: + + + + + + + + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + Comments: + + + + + + + + 50 + 0 + + + + QFrame::NoFrame + + + KSqueezedTextLabel + + + Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + + + + + + + + 75 + true + + + + true + + + Chunks + + + + + + + Downloaded chunks: + + + + + + + + 0 + 0 + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + + Available chunks: + + + + + + + + 0 + 0 + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + QFrame::Sunken + + + 1 + + + Qt::Vertical + + + + + + + + + + 75 + true + + + + true + + + Sharing + + + + + + + + + Share ratio: + + + + + + + QFrame::NoFrame + + + + + + + + + + Ratio limit: + + + + + + + No limit + + + 0.000000000000000 + + + 1000000.000000000000000 + + + 0.010000000000000 + + + 1.500000000000000 + + + + + + + Time limit: + + + + + + + No limit + + + Hours + + + 0.000000000000000 + + + 10000000.000000000000000 + + + 0.050000000000000 + + + 72.000000000000000 + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + + KSqueezedTextLabel + QLabel +
ksqueezedtextlabel.h
+
+ + kt::DownloadedChunkBar + QFrame +
downloadedchunkbar.h
+ 1 +
+ + kt::AvailabilityChunkBar + QFrame +
availabilitychunkbar.h
+ 1 +
+
+ + + + use_ratio_limit + toggled(bool) + ratio_limit + setEnabled(bool) + + + 444 + 151 + + + 526 + 151 + + + + + use_time_limit + toggled(bool) + time_limit + setEnabled(bool) + + + 453 + 186 + + + 502 + 185 + + + + +
diff --git a/plugins/infowidget/trackermodel.cpp b/plugins/infowidget/trackermodel.cpp new file mode 100644 index 0000000..9d8a2de --- /dev/null +++ b/plugins/infowidget/trackermodel.cpp @@ -0,0 +1,293 @@ +/* + SPDX-FileCopyrightText: 2008 Joris Guisson + SPDX-FileCopyrightText: 2008 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "trackermodel.h" + +#include +#include + +#include +#include +#include + +namespace kt +{ +TrackerModel::TrackerModel(QObject *parent) + : QAbstractTableModel(parent) + , tc(nullptr) + , running(false) +{ +} + +TrackerModel::~TrackerModel() +{ + qDeleteAll(trackers); +} + +void TrackerModel::changeTC(bt::TorrentInterface *tc) +{ + beginResetModel(); + qDeleteAll(trackers); + trackers.clear(); + this->tc = tc; + if (tc) { + const QList tracker_list = tc->getTrackersList()->getTrackers(); + for (bt::TrackerInterface *trk : tracker_list) { + trackers.append(new Item(trk)); + } + } + endResetModel(); +} + +void TrackerModel::update() +{ + if (!tc) + return; + + int idx = 0; + for (Item *t : qAsConst(trackers)) { + if (t->update()) + Q_EMIT dataChanged(index(idx, 1), index(idx, 5)); + idx++; + } + + running = tc->getStats().running; +} + +int TrackerModel::rowCount(const QModelIndex &parent) const +{ + if (parent.isValid() || !tc) + return 0; + else + return trackers.count(); +} + +int TrackerModel::columnCount(const QModelIndex &parent) const +{ + if (parent.isValid()) + return 0; + else + return 6; +} + +QVariant TrackerModel::data(const QModelIndex &index, int role) const +{ + if (!tc || !index.isValid() || index.row() < 0 || index.row() >= trackers.count()) + return QVariant(); + + Item *item = (Item *)index.internalPointer(); + if (!item) + return QVariant(); + + bt::TrackerInterface *trk = item->trk; + + if (role == Qt::CheckStateRole && index.column() == 0) { + return trk->isEnabled() ? Qt::Checked : Qt::Unchecked; + } else if (role == Qt::DisplayRole) { + return item->displayData(index.column()); + } else if (role == Qt::UserRole) { + return item->sortData(index.column()); + } else if (role == Qt::ForegroundRole && index.column() == 1 && trk->trackerStatus() == bt::TRACKER_ERROR) { + return QColor(Qt::red); + } + + return QVariant(); +} + +bool TrackerModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + if (!tc || !index.isValid() || index.row() < 0 || index.row() >= trackers.count()) + return false; + + if (role == Qt::CheckStateRole) { + QUrl url = trackers.at(index.row())->trk->trackerURL(); + tc->getTrackersList()->setTrackerEnabled(url, (Qt::CheckState)value.toUInt() == Qt::Checked); + return true; + } + return false; +} + +QVariant TrackerModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if (orientation != Qt::Horizontal) + return QVariant(); + + if (role == Qt::DisplayRole) { + switch (section) { + case 0: + return i18n("URL"); + case 1: + return i18n("Status"); + case 2: + return i18n("Seeders"); + case 3: + return i18n("Leechers"); + case 4: + return i18n("Times Downloaded"); + case 5: + return i18n("Next Update"); + } + } + return QVariant(); +} + +void TrackerModel::addTrackers(QList &tracker_list) +{ + if (tracker_list.isEmpty()) + return; + + int row = trackers.count(); + for (bt::TrackerInterface *trk : qAsConst(tracker_list)) + trackers.append(new Item(trk)); + + insertRows(row, tracker_list.count(), QModelIndex()); +} + +bool TrackerModel::insertRows(int row, int count, const QModelIndex &parent) +{ + Q_UNUSED(parent); + beginInsertRows(QModelIndex(), row, row + count - 1); + endInsertRows(); + return true; +} + +bool TrackerModel::removeRows(int row, int count, const QModelIndex &parent) +{ + Q_UNUSED(parent); + beginRemoveRows(QModelIndex(), row, row + count - 1); + if (tc) { + for (int i = 0; i < count; i++) { + Item *item = trackers.takeAt(row); + QUrl url = item->trk->trackerURL(); + tc->getTrackersList()->removeTracker(url); + delete item; + } + } + endRemoveRows(); + return true; +} + +Qt::ItemFlags TrackerModel::flags(const QModelIndex &index) const +{ + if (!tc || !index.isValid() || index.row() >= trackers.count() || index.row() < 0 || index.column() != 0) + return QAbstractItemModel::flags(index); + else + return QAbstractItemModel::flags(index) | Qt::ItemIsUserCheckable; +} + +QModelIndex TrackerModel::index(int row, int column, const QModelIndex &parent) const +{ + if (parent.isValid() || row < 0 || row >= trackers.count() || column < 0 || column >= 6) + return QModelIndex(); + else + return createIndex(row, column, trackers.at(row)); +} + +QUrl TrackerModel::trackerUrl(const QModelIndex &index) +{ + if (!tc || !index.isValid() || index.row() < 0 || index.row() >= trackers.count()) + return QUrl(); + + return ((Item *)index.internalPointer())->trk->trackerURL(); +} + +bt::TrackerInterface *TrackerModel::tracker(const QModelIndex &index) +{ + if (!tc || !index.isValid() || index.row() < 0 || index.row() >= trackers.count()) + return nullptr; + + return ((Item *)index.internalPointer())->trk; +} + +////////////////////////////////////////// + +TrackerModel::Item::Item(bt::TrackerInterface *tracker) + : trk(tracker) + , status(tracker->trackerStatus()) + , seeders(-1) + , leechers(-1) + , times_downloaded(-1) + , time_to_next_update(0) +{ +} + +bool TrackerModel::Item::update() +{ + bool ret = false; + if (status != trk->trackerStatus()) { + status = trk->trackerStatus(); + ret = true; + } + + if (seeders != trk->getNumSeeders()) { + seeders = trk->getNumSeeders(); + ret = true; + } + + if (leechers != trk->getNumLeechers()) { + leechers = trk->getNumLeechers(); + ret = true; + } + + if (times_downloaded != trk->getTotalTimesDownloaded()) { + times_downloaded = trk->getTotalTimesDownloaded(); + ret = true; + } + + if (time_to_next_update != trk->timeToNextUpdate()) { + time_to_next_update = trk->timeToNextUpdate(); + ret = true; + } + + return ret; +} + +QVariant TrackerModel::Item::displayData(int column) const +{ + switch (column) { + case 0: + return trk->trackerURL().toString(); + case 1: + return trk->trackerStatusString(); + case 2: + return seeders >= 0 ? seeders : QVariant(); + case 3: + return leechers >= 0 ? leechers : QVariant(); + case 4: + return times_downloaded >= 0 ? times_downloaded : QVariant(); + case 5: { + int secs = time_to_next_update; + if (secs) + return QTime(0, 0, 0, 0).addSecs(secs).toString(QStringLiteral("mm:ss")); + else + return QVariant(); + } + default: + return QVariant(); + } +} + +QVariant TrackerModel::Item::sortData(int column) const +{ + switch (column) { + case 0: + return trk->trackerURL().toString(); + case 1: + return status; + case 2: + return seeders; + case 3: + return leechers; + case 4: + return times_downloaded; + case 5: + return time_to_next_update; + default: + return QVariant(); + } +} + +} diff --git a/plugins/infowidget/trackermodel.h b/plugins/infowidget/trackermodel.h new file mode 100644 index 0000000..38c82de --- /dev/null +++ b/plugins/infowidget/trackermodel.h @@ -0,0 +1,77 @@ +/* + SPDX-FileCopyrightText: 2008 Joris Guisson + SPDX-FileCopyrightText: 2008 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KTTRACKERMODEL_H +#define KTTRACKERMODEL_H + +#include +#include +#include + +#include + +namespace bt +{ +class TorrentInterface; +} + +namespace kt +{ +/** + @author +*/ +class TrackerModel : public QAbstractTableModel +{ + Q_OBJECT +public: + TrackerModel(QObject *parent); + ~TrackerModel() override; + + void changeTC(bt::TorrentInterface *tc); + void update(); + + int rowCount(const QModelIndex &parent) const override; + int columnCount(const QModelIndex &parent) const override; + QVariant data(const QModelIndex &index, int role) const override; + bool setData(const QModelIndex &index, const QVariant &value, int role) override; + QVariant headerData(int section, Qt::Orientation orientation, int role) const override; + bool insertRows(int row, int count, const QModelIndex &parent) override; + bool removeRows(int row, int count, const QModelIndex &parent) override; + Qt::ItemFlags flags(const QModelIndex &index) const override; + QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override; + + /// Get a tracker url given a model index + QUrl trackerUrl(const QModelIndex &idx); + + /// Get a tracker given a model index + bt::TrackerInterface *tracker(const QModelIndex &idx); + + /// Add trackers to the model + void addTrackers(QList &tracker_list); + +private: + struct Item { + bt::TrackerInterface *trk; + bt::TrackerStatus status; + int seeders; + int leechers; + int times_downloaded; + unsigned int time_to_next_update; + + Item(bt::TrackerInterface *tracker); + bool update(); + QVariant displayData(int column) const; + QVariant sortData(int column) const; + }; + + bt::TorrentInterface *tc; + QList trackers; + bool running; +}; + +} + +#endif diff --git a/plugins/infowidget/trackerview.cpp b/plugins/infowidget/trackerview.cpp new file mode 100644 index 0000000..dfade2e --- /dev/null +++ b/plugins/infowidget/trackerview.cpp @@ -0,0 +1,270 @@ +/* + SPDX-FileCopyrightText: 2006-2009 Joris Guisson + SPDX-FileCopyrightText: 2006-2009 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "trackerview.h" + +#include +#include +#include + +#include +#include +#include + +#include "addtrackersdialog.h" +#include "trackermodel.h" +#include +#include +#include +#include +#include + +using namespace bt; + +namespace kt +{ +TrackerView::TrackerView(QWidget *parent) + : QWidget(parent) + , header_state_loaded(false) +{ + setupUi(this); + model = new TrackerModel(this); + proxy_model = new QSortFilterProxyModel(this); + proxy_model->setSortRole(Qt::UserRole); + proxy_model->setSourceModel(model); + m_tracker_list->setModel(proxy_model); + m_tracker_list->setAllColumnsShowFocus(true); + m_tracker_list->setRootIsDecorated(false); + m_tracker_list->setAlternatingRowColors(true); + m_tracker_list->setSortingEnabled(true); + m_tracker_list->setUniformRowHeights(true); + connect(m_add_tracker, &QPushButton::clicked, this, &TrackerView::addClicked); + connect(m_remove_tracker, &QPushButton::clicked, this, &TrackerView::removeClicked); + connect(m_change_tracker, &QPushButton::clicked, this, &TrackerView::changeClicked); + connect(m_restore_defaults, &QPushButton::clicked, this, &TrackerView::restoreClicked); + connect(m_tracker_list->selectionModel(), &QItemSelectionModel::currentChanged, this, &TrackerView::currentChanged); + connect(m_scrape, &QPushButton::clicked, this, &TrackerView::scrapeClicked); + + m_add_tracker->setIcon(QIcon::fromTheme(QLatin1String("list-add"))); + m_remove_tracker->setIcon(QIcon::fromTheme(QLatin1String("list-remove"))); + m_restore_defaults->setIcon(QIcon::fromTheme(QLatin1String("kt-restore-defaults"))); + m_change_tracker->setIcon(QIcon::fromTheme(QLatin1String("kt-change-tracker"))); + + m_ContextMenu = new QMenu(this); + QAction *copy_URL = m_ContextMenu->addAction(i18n("Copy Tracker URL")); + connect(copy_URL, &QAction::triggered, [=]() { + bt::TrackerInterface *trk = selectedTracker(); + if (trk) + QApplication::clipboard()->setText(trk->trackerURL().toDisplayString()); + }); + + QAction *copy_status = m_ContextMenu->addAction(i18n("Copy Tracker status")); + connect(copy_status, &QAction::triggered, [=]() { + bt::TrackerInterface *trk = selectedTracker(); + if (trk) + QApplication::clipboard()->setText(trk->trackerStatusString()); + }); + + m_tracker_list->setContextMenuPolicy(Qt::CustomContextMenu); + connect(m_tracker_list, &QTreeView::customContextMenuRequested, [=](const QPoint &point) { + QModelIndex index = m_tracker_list->indexAt(point); + if (index.isValid()) { + m_ContextMenu->exec(m_tracker_list->viewport()->mapToGlobal(point)); + } + }); + + setEnabled(false); + torrentChanged(nullptr); +} + +TrackerView::~TrackerView() +{ +} + +void TrackerView::addClicked() +{ + if (!tc) + return; + + AddTrackersDialog dlg(this, tracker_hints); + if (dlg.exec() != QDialog::Accepted) + return; + + const QStringList trackers = dlg.trackerList(); + QList urls; + QStringList invalid; + // check for invalid urls + for (const QString &t : trackers) { + if (t.isEmpty()) + continue; + + QUrl url(t.trimmed()); + if (!url.isValid() || (url.scheme() != QLatin1String("udp") && url.scheme() != QLatin1String("http") && url.scheme() != QLatin1String("https"))) + invalid.append(t); + else { + if (!tracker_hints.contains(url.toDisplayString())) + tracker_hints.append(url.toDisplayString()); + urls.append(url); + } + } + + if (!invalid.isEmpty()) { + KMessageBox::errorList(this, i18n("Several URL's could not be added because they are malformed:"), invalid); + } + + QList dupes; + QList tl; + for (const QUrl &url : qAsConst(urls)) { + bt::TrackerInterface *trk = tc.data()->getTrackersList()->addTracker(url, true); + if (!trk) + dupes.append(url); + else + tl.append(trk); + } + + if (dupes.size() == 1) + KMessageBox::sorry(nullptr, i18n("There already is a tracker named %1.", dupes.front().toDisplayString())); + else if (dupes.size() > 1) + KMessageBox::informationList(nullptr, i18n("The following duplicate trackers were not added:"), QUrl::toStringList(dupes)); + + if (!tl.isEmpty()) + model->addTrackers(tl); +} + +void TrackerView::removeClicked() +{ + QModelIndex current = proxy_model->mapToSource(m_tracker_list->selectionModel()->currentIndex()); + if (!current.isValid()) + return; + + model->removeRow(current.row()); +} + +bt::TrackerInterface *TrackerView::selectedTracker() const +{ + QModelIndex current = m_tracker_list->selectionModel()->currentIndex(); + if (!current.isValid() || tc.isNull()) + return nullptr; + + return model->tracker(proxy_model->mapToSource(current)); +} + +void TrackerView::changeClicked() +{ + bt::TrackerInterface *trk = selectedTracker(); + if (trk && trk->isEnabled()) { + bt::TrackersList *tlist = tc.data()->getTrackersList(); + tlist->setCurrentTracker(trk); + } +} + +void TrackerView::restoreClicked() +{ + if (tc) { + tc.data()->getTrackersList()->restoreDefault(); + tc.data()->updateTracker(); + model->changeTC(tc.data()); // trigger reset + } +} + +void TrackerView::updateClicked() +{ + if (!tc) + return; + + tc.data()->updateTracker(); +} + +void TrackerView::scrapeClicked() +{ + if (!tc) + return; + + tc.data()->scrapeTracker(); +} + +void TrackerView::changeTC(TorrentInterface *ti) +{ + if (tc.data() == ti) + return; + + setEnabled(ti != nullptr); + torrentChanged(ti); + update(); + + if (!header_state_loaded) { + m_tracker_list->resizeColumnToContents(0); + header_state_loaded = true; + } +} + +void TrackerView::update() +{ + if (tc) + model->update(); +} + +void TrackerView::torrentChanged(TorrentInterface *ti) +{ + tc = ti; + if (!tc) { + m_add_tracker->setEnabled(false); + m_remove_tracker->setEnabled(false); + m_restore_defaults->setEnabled(false); + m_change_tracker->setEnabled(false); + m_scrape->setEnabled(false); + model->changeTC(nullptr); + } else { + m_add_tracker->setEnabled(true); + m_remove_tracker->setEnabled(true); + m_restore_defaults->setEnabled(true); + m_scrape->setEnabled(true); + model->changeTC(ti); + currentChanged(m_tracker_list->selectionModel()->currentIndex(), QModelIndex()); + m_tracker_list->resizeColumnToContents(0); + } +} + +void TrackerView::currentChanged(const QModelIndex ¤t, const QModelIndex &previous) +{ + Q_UNUSED(previous); + if (!tc) { + m_change_tracker->setEnabled(false); + m_remove_tracker->setEnabled(false); + return; + } + + const TorrentStats &s = tc.data()->getStats(); + + bt::TrackerInterface *trk = model->tracker(proxy_model->mapToSource(current)); + bool enabled = trk ? trk->isEnabled() : false; + m_change_tracker->setEnabled(s.running && model->rowCount(QModelIndex()) > 1 && enabled && s.priv_torrent); + m_remove_tracker->setEnabled(trk && tc.data()->getTrackersList()->canRemoveTracker(trk)); +} + +void TrackerView::saveState(KSharedConfigPtr cfg) +{ + KConfigGroup g = cfg->group("TrackerView"); + QByteArray s = m_tracker_list->header()->saveState(); + g.writeEntry("state", s.toBase64()); + g.writeEntry("tracker_hints", tracker_hints); +} + +void TrackerView::loadState(KSharedConfigPtr cfg) +{ + KConfigGroup g = cfg->group("TrackerView"); + QByteArray s = g.readEntry("state", QByteArray()); + if (!s.isEmpty()) { + QHeaderView *v = m_tracker_list->header(); + v->restoreState(QByteArray::fromBase64(s)); + header_state_loaded = true; + } + + QStringList default_hints; + default_hints << QStringLiteral("udp://tracker.publicbt.com:80/announce") << QStringLiteral("udp://tracker.openbittorrent.com:80/announce"); + tracker_hints = g.readEntry("tracker_hints", default_hints); +} +} diff --git a/plugins/infowidget/trackerview.h b/plugins/infowidget/trackerview.h new file mode 100644 index 0000000..c2104c2 --- /dev/null +++ b/plugins/infowidget/trackerview.h @@ -0,0 +1,59 @@ +/* + SPDX-FileCopyrightText: 2006-2007 Joris Guisson + SPDX-FileCopyrightText: 2006-2007 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef TRACKERVIEW_H +#define TRACKERVIEW_H + +#include "ui_trackerview.h" + +#include +#include +#include + +#include + +namespace kt +{ +class TrackerModel; + +/** + * @author Ivan Vasic + */ +class TrackerView : public QWidget, public Ui_TrackerView +{ + Q_OBJECT +public: + TrackerView(QWidget *parent); + ~TrackerView() override; + + void update(); + void changeTC(bt::TorrentInterface *ti); + void saveState(KSharedConfigPtr cfg); + void loadState(KSharedConfigPtr cfg); + +public Q_SLOTS: + void updateClicked(); + void restoreClicked(); + void changeClicked(); + void removeClicked(); + void addClicked(); + void scrapeClicked(); + void currentChanged(const QModelIndex ¤t, const QModelIndex &previous); + +private: + void torrentChanged(bt::TorrentInterface *ti); + bt::TrackerInterface *selectedTracker() const; + +private: + bt::TorrentInterface::WPtr tc; + TrackerModel *model; + QSortFilterProxyModel *proxy_model; + QStringList tracker_hints; + bool header_state_loaded; + QMenu *m_ContextMenu; +}; +} +#endif diff --git a/plugins/infowidget/trackerview.ui b/plugins/infowidget/trackerview.ui new file mode 100644 index 0000000..fa30106 --- /dev/null +++ b/plugins/infowidget/trackerview.ui @@ -0,0 +1,76 @@ + + + TrackerView + + + + 0 + 0 + 781 + 201 + + + + + + + + + + + + Add Trackers + + + + + + + Remove Tracker + + + + + + + Changes the current active tracker for private torrents. + + + Switch Tracker + + + + + + + Update Trackers + + + + + + + Qt::Vertical + + + + 20 + 81 + + + + + + + + Restore Defaults + + + + + + + + + + diff --git a/plugins/infowidget/webseedsmodel.cpp b/plugins/infowidget/webseedsmodel.cpp new file mode 100644 index 0000000..1eab7e9 --- /dev/null +++ b/plugins/infowidget/webseedsmodel.cpp @@ -0,0 +1,176 @@ +/* + SPDX-FileCopyrightText: 2008 Joris Guisson + SPDX-FileCopyrightText: 2008 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "webseedsmodel.h" + +#include + +#include +#include + +using namespace bt; + +namespace kt +{ +WebSeedsModel::WebSeedsModel(QObject *parent) + : QAbstractTableModel(parent) +{ +} + +WebSeedsModel::~WebSeedsModel() +{ +} + +void WebSeedsModel::changeTC(bt::TorrentInterface *tc) +{ + curr_tc = tc; + beginResetModel(); + items.clear(); + if (tc) { + for (Uint32 i = 0; i < tc->getNumWebSeeds(); i++) { + const bt::WebSeedInterface *ws = tc->getWebSeed(i); + Item item; + item.status = ws->getStatus(); + item.downloaded = ws->getTotalDownloaded(); + item.speed = ws->getDownloadRate(); + items.append(item); + } + } + endResetModel(); +} + +bool WebSeedsModel::update() +{ + if (!curr_tc) + return false; + + bt::TorrentInterface *tc = curr_tc.data(); + bool ret = false; + + for (Uint32 i = 0; i < tc->getNumWebSeeds(); i++) { + const bt::WebSeedInterface *ws = tc->getWebSeed(i); + Item &item = items[i]; + bool changed = false; + if (item.status != ws->getStatus()) { + changed = true; + item.status = ws->getStatus(); + } + + if (item.downloaded != ws->getTotalDownloaded()) { + changed = true; + item.downloaded = ws->getTotalDownloaded(); + } + + if (item.speed != ws->getDownloadRate()) { + changed = true; + item.speed = ws->getDownloadRate(); + } + + if (changed) { + dataChanged(createIndex(i, 1), createIndex(i, 3)); + ret = true; + } + } + + return ret; +} + +int WebSeedsModel::rowCount(const QModelIndex &parent) const +{ + if (parent.isValid()) + return 0; + else + return curr_tc ? curr_tc.data()->getNumWebSeeds() : 0; +} + +int WebSeedsModel::columnCount(const QModelIndex &parent) const +{ + if (parent.isValid()) + return 0; + else + return 4; +} + +QVariant WebSeedsModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if (role != Qt::DisplayRole || orientation != Qt::Horizontal) + return QVariant(); + + switch (section) { + case 0: + return i18n("URL"); + case 1: + return i18n("Speed"); + case 2: + return i18n("Downloaded"); + case 3: + return i18n("Status"); + default: + return QVariant(); + } +} + +QVariant WebSeedsModel::data(const QModelIndex &index, int role) const +{ + if (!curr_tc) + return QVariant(); + + if (!index.isValid() || index.row() >= (int)curr_tc.data()->getNumWebSeeds() || index.row() < 0) + return QVariant(); + + if (role == Qt::DisplayRole) { + const bt::WebSeedInterface *ws = curr_tc.data()->getWebSeed(index.row()); + switch (index.column()) { + case 0: + return ws->getUrl().toDisplayString(); + case 1: + return bt::BytesPerSecToString(ws->getDownloadRate()); + case 2: + return bt::BytesToString(ws->getTotalDownloaded()); + case 3: + return ws->getStatus(); + } + } else if (role == Qt::CheckStateRole && index.column() == 0) { + const bt::WebSeedInterface *ws = curr_tc.data()->getWebSeed(index.row()); + return ws->isEnabled() ? Qt::Checked : Qt::Unchecked; + } else if (role == Qt::UserRole) { + const bt::WebSeedInterface *ws = curr_tc.data()->getWebSeed(index.row()); + switch (index.column()) { + case 0: + return ws->getUrl().toDisplayString(); + case 1: + return ws->getDownloadRate(); + case 2: + return ws->getTotalDownloaded(); + case 3: + return ws->getStatus(); + } + } + return QVariant(); +} + +Qt::ItemFlags WebSeedsModel::flags(const QModelIndex &index) const +{ + Qt::ItemFlags flags = QAbstractTableModel::flags(index); + if (index.column() == 0) + flags |= Qt::ItemIsUserCheckable; + + return flags; +} + +bool WebSeedsModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + if (!curr_tc || role != Qt::CheckStateRole) + return false; + + if (!index.isValid() || index.row() >= (int)curr_tc.data()->getNumWebSeeds() || index.row() < 0) + return false; + + bt::WebSeedInterface *ws = curr_tc.data()->getWebSeed(index.row()); + ws->setEnabled((Qt::CheckState)value.toInt() == Qt::Checked), Q_EMIT dataChanged(index, index); + return true; +} +} diff --git a/plugins/infowidget/webseedsmodel.h b/plugins/infowidget/webseedsmodel.h new file mode 100644 index 0000000..c070e64 --- /dev/null +++ b/plugins/infowidget/webseedsmodel.h @@ -0,0 +1,59 @@ +/* + SPDX-FileCopyrightText: 2008 Joris Guisson + SPDX-FileCopyrightText: 2008 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KTWEBSEEDSMODEL_H +#define KTWEBSEEDSMODEL_H + +#include +#include + +#include +#include + +namespace kt +{ +/** + @author +*/ +class WebSeedsModel : public QAbstractTableModel +{ + Q_OBJECT + +public: + WebSeedsModel(QObject *parent); + ~WebSeedsModel() override; + + /** + * Change the current torrent. + * @param tc + */ + void changeTC(bt::TorrentInterface *tc); + + /** + * See if we need to update the model + */ + bool update(); + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + int columnCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant headerData(int section, Qt::Orientation orientation, int role) const override; + QVariant data(const QModelIndex &index, int role) const override; + Qt::ItemFlags flags(const QModelIndex &index) const override; + bool setData(const QModelIndex &index, const QVariant &value, int role) override; + +private: + struct Item { + QString status; + bt::Uint64 downloaded; + bt::Uint32 speed; + }; + bt::TorrentInterface::WPtr curr_tc; + QVector items; +}; + +} + +#endif diff --git a/plugins/infowidget/webseedstab.cpp b/plugins/infowidget/webseedstab.cpp new file mode 100644 index 0000000..7e0e3d1 --- /dev/null +++ b/plugins/infowidget/webseedstab.cpp @@ -0,0 +1,169 @@ +/* + SPDX-FileCopyrightText: 2008 Joris Guisson + SPDX-FileCopyrightText: 2008 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "webseedstab.h" + +#include +#include + +#include "webseedsmodel.h" +#include + +using namespace bt; + +namespace kt +{ +WebSeedsTab::WebSeedsTab(QWidget *parent) + : QWidget(parent) +{ + setupUi(this); + connect(m_add, &QPushButton::clicked, this, &WebSeedsTab::addWebSeed); + connect(m_remove, &QPushButton::clicked, this, &WebSeedsTab::removeWebSeed); + connect(m_disable_all, &QPushButton::clicked, this, &WebSeedsTab::disableAll); + connect(m_enable_all, &QPushButton::clicked, this, &WebSeedsTab::enableAll); + m_add->setIcon(QIcon::fromTheme(QLatin1String("list-add"))); + m_remove->setIcon(QIcon::fromTheme(QLatin1String("list-remove"))); + m_add->setEnabled(false); + m_remove->setEnabled(false); + m_webseed_list->setEnabled(false); + model = new WebSeedsModel(this); + proxy_model = new QSortFilterProxyModel(this); + proxy_model->setSourceModel(model); + proxy_model->setSortRole(Qt::UserRole); + m_webseed_list->setModel(proxy_model); + m_webseed_list->setSortingEnabled(true); + m_webseed_list->setUniformRowHeights(true); + + connect(m_webseed_list->selectionModel(), + &QItemSelectionModel::selectionChanged, + this, + qOverload(&WebSeedsTab::selectionChanged)); + + connect(m_webseed, &QLineEdit::textChanged, this, &WebSeedsTab::onWebSeedTextChanged); +} + +WebSeedsTab::~WebSeedsTab() +{ +} + +void WebSeedsTab::changeTC(bt::TorrentInterface *tc) +{ + curr_tc = tc; + model->changeTC(tc); + m_add->setEnabled(tc != nullptr); + m_remove->setEnabled(tc != nullptr); + m_webseed_list->setEnabled(tc != nullptr); + m_webseed->setEnabled(tc != nullptr); + m_enable_all->setEnabled(tc != nullptr); + m_disable_all->setEnabled(tc != nullptr); + onWebSeedTextChanged(m_webseed->text()); + + // see if we need to enable or disable the remove button + if (curr_tc) + selectionChanged(m_webseed_list->selectionModel()->selectedRows()); +} + +void WebSeedsTab::addWebSeed() +{ + if (!curr_tc) + return; + + bt::TorrentInterface *tc = curr_tc.data(); + QUrl url(m_webseed->text()); + if (tc && url.isValid() && url.scheme() == QLatin1String("http")) { + if (tc->addWebSeed(url)) { + model->changeTC(tc); + m_webseed->clear(); + } else { + KMessageBox::error(this, i18n("Cannot add the webseed %1, it is already part of the list of webseeds.", url.toDisplayString())); + } + } +} + +void WebSeedsTab::removeWebSeed() +{ + if (!curr_tc) + return; + + bt::TorrentInterface *tc = curr_tc.data(); + const QModelIndexList idx_list = m_webseed_list->selectionModel()->selectedRows(); + for (const QModelIndex &idx : idx_list) { + const WebSeedInterface *ws = tc->getWebSeed(proxy_model->mapToSource(idx).row()); + if (ws && ws->isUserCreated()) { + if (!tc->removeWebSeed(ws->getUrl())) + KMessageBox::error(this, i18n("Cannot remove webseed %1, it is part of the torrent.", ws->getUrl().toDisplayString())); + } + } + + model->changeTC(tc); +} + +void WebSeedsTab::selectionChanged(const QModelIndexList &indexes) +{ + if (curr_tc) { + for (const QModelIndex &idx : indexes) { + const WebSeedInterface *ws = curr_tc.data()->getWebSeed(proxy_model->mapToSource(idx).row()); + if (ws && ws->isUserCreated()) { + m_remove->setEnabled(true); + return; + } + } + } + + m_remove->setEnabled(false); +} + +void WebSeedsTab::selectionChanged(const QItemSelection &selected, const QItemSelection &deselected) +{ + Q_UNUSED(deselected); + if (!curr_tc) + return; + + selectionChanged(selected.indexes()); +} + +void WebSeedsTab::onWebSeedTextChanged(const QString &ws) +{ + QUrl url(ws); + m_add->setEnabled(!curr_tc.isNull() && url.isValid() && url.scheme() == QLatin1String("http")); +} + +void WebSeedsTab::update() +{ + if (model->update()) + proxy_model->invalidate(); +} + +void WebSeedsTab::saveState(KSharedConfigPtr cfg) +{ + KConfigGroup g = cfg->group("WebSeedsTab"); + QByteArray s = m_webseed_list->header()->saveState(); + g.writeEntry("state", s.toBase64()); +} + +void WebSeedsTab::loadState(KSharedConfigPtr cfg) +{ + KConfigGroup g = cfg->group("WebSeedsTab"); + QByteArray s = QByteArray::fromBase64(g.readEntry("state", QByteArray())); + if (!s.isEmpty()) + m_webseed_list->header()->restoreState(s); +} + +void WebSeedsTab::disableAll() +{ + for (int i = 0; i < model->rowCount(); i++) { + model->setData(model->index(i, 0), Qt::Unchecked, Qt::CheckStateRole); + } +} + +void WebSeedsTab::enableAll() +{ + for (int i = 0; i < model->rowCount(); i++) { + model->setData(model->index(i, 0), Qt::Checked, Qt::CheckStateRole); + } +} + +} diff --git a/plugins/infowidget/webseedstab.h b/plugins/infowidget/webseedstab.h new file mode 100644 index 0000000..9b7ae4b --- /dev/null +++ b/plugins/infowidget/webseedstab.h @@ -0,0 +1,64 @@ +/* + SPDX-FileCopyrightText: 2008 Joris Guisson + SPDX-FileCopyrightText: 2008 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KTWEBSEEDSTAB_H +#define KTWEBSEEDSTAB_H + +#include +#include + +#include +#include + +#include "ui_webseedstab.h" +#include + +namespace kt +{ +class WebSeedsModel; + +/** + Tab which displays the list of webseeds of a torrent, and allows you to add or remove them. +*/ +class WebSeedsTab : public QWidget, public Ui_WebSeedsTab +{ + Q_OBJECT +public: + WebSeedsTab(QWidget *parent); + ~WebSeedsTab() override; + + /** + * Switch to a different torrent. + * @param tc The torrent + */ + void changeTC(bt::TorrentInterface *tc); + + /// Check to see if the GUI needs to be updated + void update(); + + void saveState(KSharedConfigPtr cfg); + void loadState(KSharedConfigPtr cfg); + +private Q_SLOTS: + void addWebSeed(); + void removeWebSeed(); + void disableAll(); + void enableAll(); + void onWebSeedTextChanged(const QString &ws); + void selectionChanged(const QItemSelection &selected, const QItemSelection &deselected); + +private: + void selectionChanged(const QModelIndexList &indexes); + +private: + bt::TorrentInterface::WPtr curr_tc; + WebSeedsModel *model; + QSortFilterProxyModel *proxy_model; +}; + +} + +#endif diff --git a/plugins/infowidget/webseedstab.ui b/plugins/infowidget/webseedstab.ui new file mode 100644 index 0000000..0fabfcb --- /dev/null +++ b/plugins/infowidget/webseedstab.ui @@ -0,0 +1,85 @@ + + + WebSeedsTab + + + + 0 + 0 + 482 + 300 + + + + + + + <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Webseed to add to the torrent.</p> +<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"></p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-weight:600;">Note: </span>Only http webseeds are supported.</p> + + + + + + + Add Webseed + + + + + + + false + + + true + + + true + + + + + + + + + Remove Webseed + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Enable All + + + + + + + Disable All + + + + + + + + + + diff --git a/plugins/ipfilter/CMakeLists.txt b/plugins/ipfilter/CMakeLists.txt new file mode 100644 index 0000000..c7a7971 --- /dev/null +++ b/plugins/ipfilter/CMakeLists.txt @@ -0,0 +1,34 @@ +add_library(ktorrent_ipfilter MODULE) + +target_sources(ktorrent_ipfilter PRIVATE + ipblocklist.cpp + ipblockingprefpage.cpp + convertthread.cpp + convertdialog.cpp + ipfilterplugin.cpp + downloadandconvertjob.cpp +) + +ki18n_wrap_ui(ktorrent_ipfilter ipblockingprefpage.ui convertdialog.ui) +kconfig_add_kcfg_files(ktorrent_ipfilter ipfilterpluginsettings.kcfgc) + +kcoreaddons_desktop_to_json(ktorrent_ipfilter ktorrent_ipfilter.desktop) + +target_link_libraries( + ktorrent_ipfilter + ktcore + KF5::Torrent + KF5::Archive + KF5::CoreAddons + KF5::I18n + KF5::KIOWidgets + KF5::Notifications + KF5::TextWidgets + KF5::WidgetsAddons +) +install(TARGETS ktorrent_ipfilter DESTINATION ${KTORRENT_PLUGIN_INSTALL_DIR} ) + +find_package(Qt5Test ${QT5_REQUIRED_VERSION}) +if (Qt5Test_DIR) + add_subdirectory(tests) +endif() diff --git a/plugins/ipfilter/convertdialog.cpp b/plugins/ipfilter/convertdialog.cpp new file mode 100644 index 0000000..c2cea8c --- /dev/null +++ b/plugins/ipfilter/convertdialog.cpp @@ -0,0 +1,108 @@ +/* + SPDX-FileCopyrightText: 2005 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include +#include + +#include + +#include +#include +#include +#include + +#include "convertdialog.h" +#include "convertthread.h" + +using namespace bt; + +namespace kt +{ +ConvertDialog::ConvertDialog(QWidget *parent) + : QDialog(parent) + , convert_thread(nullptr) +{ + setupUi(this); + setModal(false); + adjustSize(); + canceled = false; + connect(m_cancel, &QPushButton::clicked, this, &ConvertDialog::btnCancelClicked); + connect(&timer, &QTimer::timeout, this, &ConvertDialog::update); + + QTimer::singleShot(500, this, &ConvertDialog::convert); +} + +ConvertDialog::~ConvertDialog() +{ +} + +void ConvertDialog::message(const QString &msg) +{ + QMutexLocker lock(&mutex); + this->msg = msg; +} + +void ConvertDialog::progress(int val, int total) +{ + QMutexLocker lock(&mutex); + prog = val; + max = total; +} + +void ConvertDialog::update() +{ + QMutexLocker lock(&mutex); + m_msg->setText(msg); + m_progress_bar->setValue(prog); + m_progress_bar->setMaximum(max); +} + +void ConvertDialog::convert() +{ + if (convert_thread) + return; + + convert_thread = new ConvertThread(this); + connect(convert_thread, &ConvertThread::finished, this, &ConvertDialog::threadFinished, Qt::QueuedConnection); + convert_thread->start(); + timer.start(500); +} + +void ConvertDialog::threadFinished() +{ + QString failure = convert_thread->getFailureReason(); + if (failure != QString()) { + convert_thread->wait(); + convert_thread->deleteLater(); + convert_thread = nullptr; + KMessageBox::error(this, failure); + reject(); + } else { + convert_thread->wait(); + convert_thread->deleteLater(); + convert_thread = nullptr; + if (canceled) + reject(); + else + accept(); + } +} + +void ConvertDialog::closeEvent(QCloseEvent *e) +{ + if (!convert_thread) + e->accept(); + else + e->ignore(); +} + +void ConvertDialog::btnCancelClicked() +{ + canceled = true; + if (convert_thread) + convert_thread->stop(); +} + +} diff --git a/plugins/ipfilter/convertdialog.h b/plugins/ipfilter/convertdialog.h new file mode 100644 index 0000000..052bd97 --- /dev/null +++ b/plugins/ipfilter/convertdialog.h @@ -0,0 +1,59 @@ +/* + SPDX-FileCopyrightText: 2005 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef CONVERTDIALOG_H +#define CONVERTDIALOG_H + +#include +#include +#include +#include +#include + +#include "ui_convertdialog.h" + +namespace kt +{ +class ConvertThread; + +class ConvertDialog : public QDialog, public Ui_ConvertDialog +{ + Q_OBJECT +public: + ConvertDialog(QWidget *parent); + ~ConvertDialog() override; + + /** + * Set the message. + * @param msg The new message + */ + void message(const QString &msg); + + /** + * Update progress bar + * @param val The value + * @param total The max number of steps + */ + void progress(int val, int total); + +private Q_SLOTS: + void convert(); + void threadFinished(); + void btnCancelClicked(); + void update(); + +private: + void closeEvent(QCloseEvent *e) override; + +private: + ConvertThread *convert_thread; + QString msg; + int prog, max; + QMutex mutex; + QTimer timer; + bool canceled; +}; +} +#endif diff --git a/plugins/ipfilter/convertdialog.ui b/plugins/ipfilter/convertdialog.ui new file mode 100644 index 0000000..3d68b5b --- /dev/null +++ b/plugins/ipfilter/convertdialog.ui @@ -0,0 +1,106 @@ + + ConvertDialog + + + + 0 + 0 + 421 + 145 + + + + + 0 + 0 + + + + Converting... + + + true + + + + + + Converting block list to KTorrent format. This might take some time. + + + Qt::AutoText + + + false + + + Qt::AlignVCenter + + + true + + + -2 + + + + + + + + + + false + + + + + + + 24 + + + + + + + Qt::Horizontal + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 0 + 0 + + + + C&ancel + + + + + + + + + + + diff --git a/plugins/ipfilter/convertthread.cpp b/plugins/ipfilter/convertthread.cpp new file mode 100644 index 0000000..4ec46d4 --- /dev/null +++ b/plugins/ipfilter/convertthread.cpp @@ -0,0 +1,159 @@ +/* + SPDX-FileCopyrightText: 2007 Joris Guisson + SPDX-FileCopyrightText: 2007 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include +#include +#include +#include + +#include +#include +#include + +#include +#include + +#include "convertdialog.h" +#include "convertthread.h" +#include +#include +#include +#include + +using namespace bt; + +namespace kt +{ +ConvertThread::ConvertThread(ConvertDialog *dlg) + : dlg(dlg) + , abort(false) +{ + txt_file = kt::DataDir() + QStringLiteral("level1.txt"); + dat_file = kt::DataDir() + QStringLiteral("level1.dat"); + tmp_file = kt::DataDir() + QStringLiteral("level1.dat.tmp"); +} + +ConvertThread::~ConvertThread() +{ +} + +void ConvertThread::run() +{ + readInput(); + writeOutput(); +} + +void ConvertThread::readInput() +{ + /* READ INPUT FILE */ + QFile source(txt_file); + if (!source.open(QIODevice::ReadOnly)) { + Out(SYS_IPF | LOG_IMPORTANT) << "Cannot find level1.txt file" << endl; + failure_reason = i18n("Cannot open %1: %2", txt_file, QString::fromLatin1(strerror(errno))); + return; + } + + Out(SYS_IPF | LOG_NOTICE) << "Loading " << txt_file << " ..." << endl; + dlg->message(i18n("Loading txt file...")); + + ulong source_size = source.size(); + QTextStream stream(&source); + + int i = 0; + const std::regex rx("(?:[0-9]{1,3}\\.){3}[0-9]{1,3}"); + + while (!stream.atEnd() && !abort) { + std::string line = stream.readLine().toStdString(); + i += line.length() * sizeof(char); // rough estimation of string size + dlg->progress(i, source_size); + ++i; + + std::vector addresses; + for (auto it = std::sregex_iterator(line.begin(), line.end(), rx); it != std::sregex_iterator(); ++it) { + addresses.push_back(it->str()); + } + + // if we have found two addresses, create a block out of it + if (addresses.size() == 2) { + input += IPBlock(QString::fromStdString(addresses[0]), QString::fromStdString(addresses[1])); + } + } + source.close(); + Out(SYS_IPF | LOG_NOTICE) << "Loaded " << input.count() << " lines" << endl; + dlg->progress(100, 100); +} + +static bool LessThan(const IPBlock &a, const IPBlock &b) +{ + if (a.ip1 == b.ip1) + return a.ip2 < b.ip2; + else + return a.ip1 < b.ip1; +} + +void ConvertThread::sort() +{ + std::sort(input.begin(), input.end(), LessThan); +} + +void ConvertThread::merge() +{ + if (input.count() < 2) // noting to merge + return; + + QList::iterator i = input.begin(); + QList::iterator j = i; + j++; + while (j != input.end() && i != input.end()) { + IPBlock &a = *i; + IPBlock &b = *j; + if (a.ip2 < b.ip1 || b.ip2 < a.ip1) { + // separate ranges, so go to the next pair + i = j; + j++; + } else { + // merge b into a + a.ip1 = (a.ip1 < b.ip1) ? a.ip1 : b.ip1; + a.ip2 = (a.ip2 > b.ip2) ? a.ip2 : b.ip2; + + // remove b + j = input.erase(j); + } + } +} + +void ConvertThread::writeOutput() +{ + if (input.count() == 0) { + failure_reason = i18n("There are no IP addresses to convert in %1", txt_file); + return; + } + + sort(); // sort the block + merge(); // merge neighbouring blocks + + QFile target(dat_file); + if (!target.open(QIODevice::WriteOnly)) { + Out(SYS_IPF | LOG_IMPORTANT) << "Unable to open file for writing" << endl; + failure_reason = i18n("Cannot open %1: %2", dat_file, QString::fromLatin1(strerror(errno))); + return; + } + + Out(SYS_IPF | LOG_NOTICE) << "Loading finished, starting conversion..." << endl; + dlg->message(i18n("Converting...")); + + int i = 0; + int tot = input.count(); + for (const IPBlock &block : qAsConst(input)) { + dlg->progress(i, tot); + target.write((char *)&block, sizeof(IPBlock)); + if (abort) { + return; + } + i++; + } +} +} diff --git a/plugins/ipfilter/convertthread.h b/plugins/ipfilter/convertthread.h new file mode 100644 index 0000000..dbcce56 --- /dev/null +++ b/plugins/ipfilter/convertthread.h @@ -0,0 +1,58 @@ +/* + SPDX-FileCopyrightText: 2007 Joris Guisson + SPDX-FileCopyrightText: 2007 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KTCONVERTTHREAD_H +#define KTCONVERTTHREAD_H + +#include "ipblocklist.h" +#include + +namespace kt +{ +class ConvertDialog; + +/** + * Thread which does the converting of the text filter file to our own format. + * @author Joris Guisson + */ +class ConvertThread : public QThread +{ +public: + ConvertThread(ConvertDialog *dlg); + ~ConvertThread() override; + + void run() override; + + QString getFailureReason() const + { + return failure_reason; + } + + void stop() + { + abort = true; + } + +private: + void readInput(); + void writeOutput(); + void cleanUp(bool failed); + void sort(); + void merge(); + +private: + ConvertDialog *dlg; + bool abort; + QString txt_file; + QString dat_file; + QString tmp_file; + QList input; + QString failure_reason; +}; + +} + +#endif diff --git a/plugins/ipfilter/downloadandconvertjob.cpp b/plugins/ipfilter/downloadandconvertjob.cpp new file mode 100644 index 0000000..4ed3cba --- /dev/null +++ b/plugins/ipfilter/downloadandconvertjob.cpp @@ -0,0 +1,291 @@ +/* + SPDX-FileCopyrightText: 2008 Joris Guisson + SPDX-FileCopyrightText: 2008 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include +#include + +#include +#include +#include +#include + +#include "convertdialog.h" +#include "downloadandconvertjob.h" +#include +#include +#include +#include +#include +#include +#include + +using namespace bt; + +namespace kt +{ +DownloadAndConvertJob::DownloadAndConvertJob(const QUrl &url, Mode mode) + : url(url) + , unzip(false) + , convert_dlg(nullptr) + , mode(mode) +{ +} + +DownloadAndConvertJob::~DownloadAndConvertJob() +{ +} + +void DownloadAndConvertJob::start() +{ + QString temp = kt::DataDir() + QStringLiteral("tmp-") + url.fileName(); + if (bt::Exists(temp)) + bt::Delete(temp, true); + + active_job = KIO::file_copy(url, QUrl::fromLocalFile(temp), -1, KIO::Overwrite); + connect(active_job, &KJob::result, this, &DownloadAndConvertJob::downloadFileFinished); +} + +void DownloadAndConvertJob::kill(KJob::KillVerbosity) +{ + if (active_job) + active_job->kill(KJob::EmitResult); + else if (convert_dlg) + convert_dlg->reject(); +} + +void DownloadAndConvertJob::convert(KJob *j) +{ + active_job = nullptr; + if (j->error()) { + Out(SYS_IPF | LOG_NOTICE) << "IP filter update failed: " << j->errorString() << endl; + if (mode == Verbose) { + j->uiDelegate()->showErrorMessage(); + } else { + QString msg = i18n("Automatic update of IP filter failed: %1", j->errorString()); + notification(msg); + } + setError(unzip ? UNZIP_FAILED : MOVE_FAILED); + emitResult(); + } else + convert(); +} + +static bool isBinaryData(const QString &fileName) +{ + QFile file(fileName); + if (!file.open(QIODevice::ReadOnly)) { + return false; // err, whatever + } + // Check the first 32 bytes (see shared-mime spec) + const QByteArray data = file.read(32); + const char *p = data.data(); + for (int i = 0; i < data.size(); ++i) { + if ((unsigned char)(p[i]) < 32 && p[i] != 9 && p[i] != 10 && p[i] != 13) { // ASCII control character + return true; + } + } + return false; +} + +void DownloadAndConvertJob::downloadFileFinished(KJob *j) +{ + active_job = nullptr; + if (j->error()) { + Out(SYS_IPF | LOG_NOTICE) << "IP filter update failed: " << j->errorString() << endl; + if (mode == Verbose) { + j->uiDelegate()->showErrorMessage(); + } else { + QString msg = i18n("Automatic update of IP filter failed: %1", j->errorString()); + notification(msg); + } + + setError(DOWNLOAD_FAILED); + emitResult(); + return; + } + + QString temp = kt::DataDir() + QStringLiteral("tmp-") + url.fileName(); + + // now determine if it's ZIP or TXT file + QMimeDatabase db; + QMimeType ptr = db.mimeTypeForFile(temp, QMimeDatabase::MatchContent); + Out(SYS_IPF | LOG_NOTICE) << "Mimetype: " << ptr.name() << endl; + if (ptr.name() == QStringLiteral("application/zip")) { + active_job = KIO::file_move(QUrl::fromLocalFile(temp), + QUrl::fromLocalFile(QString(kt::DataDir() + QLatin1String("level1.zip"))), + -1, + KIO::HideProgressInfo | KIO::Overwrite); + connect(active_job, &KJob::result, this, &DownloadAndConvertJob::extract); + } else if (ptr.name() == QStringLiteral("application/x-7z-compressed")) { + QString msg = i18n("7z files are not supported"); + if (mode == Verbose) + KMessageBox::error(nullptr, msg); + else + notification(msg); + + setError(UNZIP_FAILED); + emitResult(); + } else if (ptr.name() == QStringLiteral("application/gzip") || ptr.name() == QStringLiteral("application/x-bzip")) { + active_job = new bt::DecompressFileJob(temp, kt::DataDir() + QStringLiteral("level1.txt")); + connect(active_job, &KJob::result, this, qOverload(&DownloadAndConvertJob::convert)); + active_job->start(); + } else if (!isBinaryData(temp) || ptr.name() == QStringLiteral("text/plain")) { + active_job = KIO::file_move(QUrl::fromLocalFile(temp), + QUrl::fromLocalFile(kt::DataDir() + QStringLiteral("level1.txt")), + -1, + KIO::HideProgressInfo | KIO::Overwrite); + connect(active_job, &KJob::result, this, qOverload(&DownloadAndConvertJob::convert)); + } else { + QString msg = i18n("Cannot determine file type of %1", url.toDisplayString()); + if (mode == Verbose) + KMessageBox::error(nullptr, msg); + else + notification(msg); + + setError(UNZIP_FAILED); + emitResult(); + } +} + +void DownloadAndConvertJob::extract(KJob *j) +{ + active_job = nullptr; + if (j->error()) { + Out(SYS_IPF | LOG_NOTICE) << "IP filter update failed: " << j->errorString() << endl; + if (mode == Verbose) { + j->uiDelegate()->showErrorMessage(); + } else { + QString msg = i18n("Automatic update of IP filter failed: %1", j->errorString()); + notification(msg); + } + setError(MOVE_FAILED); + emitResult(); + return; + } + + QString zipfile = kt::DataDir() + QStringLiteral("level1.zip"); + KZip *zip = new KZip(zipfile); + if (!zip->open(QIODevice::ReadOnly) || !zip->directory()) { + Out(SYS_IPF | LOG_NOTICE) << "IP filter update failed: cannot open zip file " << zipfile << endl; + if (mode == Verbose) { + KMessageBox::error(nullptr, i18n("Cannot open zip file %1.", zipfile)); + } else { + QString msg = i18n("Automatic update of IP filter failed: cannot open zip file %1", zipfile); + notification(msg); + } + + setError(UNZIP_FAILED); + emitResult(); + delete zip; + return; + } + + QString destination = kt::DataDir() + QStringLiteral("level1.txt"); + QStringList entries = zip->directory()->entries(); + if (entries.count() >= 1) { + active_job = new bt::ExtractFileJob(zip, entries.front(), destination); + connect(active_job, &KJob::result, this, qOverload(&DownloadAndConvertJob::convert)); + unzip = true; + active_job->start(); + } else { + Out(SYS_IPF | LOG_NOTICE) << "IP filter update failed: no blocklist found in zipfile " << zipfile << endl; + if (mode == Verbose) { + KMessageBox::error(nullptr, i18n("Cannot find blocklist in zip file %1.", zipfile)); + } else { + QString msg = i18n("Automatic update of IP filter failed: cannot find blocklist in zip file %1", zipfile); + notification(msg); + } + + setError(UNZIP_FAILED); + emitResult(); + delete zip; + } +} + +void DownloadAndConvertJob::revertBackupFinished(KJob *) +{ + active_job = nullptr; + cleanUpFiles(); + setError(CANCELED); + emitResult(); +} + +void DownloadAndConvertJob::makeBackupFinished(KJob *j) +{ + if (j && j->error()) { + Out(SYS_IPF | LOG_NOTICE) << "IP filter update failed: " << j->errorString() << endl; + if (mode == Verbose) { + j->uiDelegate()->showErrorMessage(); + } else { + QString msg = i18n("Automatic update of IP filter failed: %1", j->errorString()); + notification(msg); + } + setError(BACKUP_FAILED); + emitResult(); + } else { + convert_dlg = new ConvertDialog(nullptr); + if (mode == Verbose) + convert_dlg->show(); + connect(convert_dlg, &ConvertDialog::accepted, this, &DownloadAndConvertJob::convertAccepted); + connect(convert_dlg, &ConvertDialog::rejected, this, &DownloadAndConvertJob::convertRejected); + } +} + +void DownloadAndConvertJob::convertAccepted() +{ + convert_dlg->deleteLater(); + convert_dlg = nullptr; + cleanUpFiles(); + setError(0); + emitResult(); +} + +void DownloadAndConvertJob::convertRejected() +{ + convert_dlg->deleteLater(); + convert_dlg = nullptr; + // shit happened move back backup stuff + QString dat_file = kt::DataDir() + QStringLiteral("level1.dat"); + QString tmp_file = kt::DataDir() + QStringLiteral("level1.dat.tmp"); + + if (bt::Exists(tmp_file)) { + active_job = KIO::file_copy(QUrl::fromLocalFile(tmp_file), QUrl::fromLocalFile(dat_file), -1, KIO::HideProgressInfo | KIO::Overwrite); + connect(active_job, &KJob::result, this, &DownloadAndConvertJob::revertBackupFinished); + } else { + cleanUpFiles(); + setError(CANCELED); + emitResult(); + } +} + +void DownloadAndConvertJob::convert() +{ + if (bt::Exists(kt::DataDir() + QStringLiteral("level1.dat"))) { + // make backup of data file, if stuff fails we can always go back + QString dat_file = kt::DataDir() + QStringLiteral("level1.dat"); + QString tmp_file = kt::DataDir() + QStringLiteral("level1.dat.tmp"); + + KIO::Job *job = KIO::file_copy(QUrl::fromLocalFile(dat_file), QUrl::fromLocalFile(tmp_file), -1, KIO::HideProgressInfo | KIO::Overwrite); + connect(job, &KIO::Job::result, this, &DownloadAndConvertJob::makeBackupFinished); + } else + makeBackupFinished(nullptr); +} + +void DownloadAndConvertJob::cleanUpFiles() +{ + // cleanup temp files + cleanUp(kt::DataDir() + QStringLiteral("level1.zip")); + cleanUp(kt::DataDir() + QStringLiteral("level1.txt")); + cleanUp(kt::DataDir() + QStringLiteral("level1.tmp")); + cleanUp(kt::DataDir() + QStringLiteral("level1.dat.tmp")); +} + +void DownloadAndConvertJob::cleanUp(const QString &path) +{ + if (bt::Exists(path)) + bt::Delete(path, true); +} +} diff --git a/plugins/ipfilter/downloadandconvertjob.h b/plugins/ipfilter/downloadandconvertjob.h new file mode 100644 index 0000000..61d443d --- /dev/null +++ b/plugins/ipfilter/downloadandconvertjob.h @@ -0,0 +1,74 @@ +/* + SPDX-FileCopyrightText: 2008 Joris Guisson + SPDX-FileCopyrightText: 2008 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KTDOWNLOADANDCONVERTJOB_H +#define KTDOWNLOADANDCONVERTJOB_H + +#include + +namespace kt +{ +class ConvertDialog; + +/** + Job to download and convert a filter file +*/ +class DownloadAndConvertJob : public KIO::Job +{ + Q_OBJECT +public: + enum Mode { + Verbose, + Quietly, + }; + DownloadAndConvertJob(const QUrl &url, Mode mode); + ~DownloadAndConvertJob() override; + + enum ErrorCode { + CANCELED = 100, + DOWNLOAD_FAILED, + UNZIP_FAILED, + MOVE_FAILED, + BACKUP_FAILED, + }; + + void kill(KJob::KillVerbosity v); + void start() override; + + bool isAutoUpdate() const + { + return mode == Quietly; + } + +Q_SIGNALS: + /// Emitted when the job needs to show a notification + void notification(const QString &msg); + +private Q_SLOTS: + void downloadFileFinished(KJob *); + void convert(KJob *); + void extract(KJob *); + void makeBackupFinished(KJob *); + void revertBackupFinished(KJob *); + void convertAccepted(); + void convertRejected(); + +private: + void convert(); + void cleanUp(const QString &path); + void cleanUpFiles(); + +private: + QUrl url; + KJob *active_job; + bool unzip; + ConvertDialog *convert_dlg; + Mode mode; +}; + +} + +#endif diff --git a/plugins/ipfilter/ipblockingprefpage.cpp b/plugins/ipfilter/ipblockingprefpage.cpp new file mode 100644 index 0000000..bde4244 --- /dev/null +++ b/plugins/ipfilter/ipblockingprefpage.cpp @@ -0,0 +1,206 @@ +/* + SPDX-FileCopyrightText: 2005 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include + +#include "downloadandconvertjob.h" +#include "ipblockingprefpage.h" +#include "ipfilterplugin.h" +#include "ipfilterpluginsettings.h" +#include + +using namespace bt; + +namespace kt +{ +IPBlockingPrefPage::IPBlockingPrefPage(IPFilterPlugin *p) + : PrefPageInterface(IPBlockingPluginSettings::self(), i18n("IP Filter"), QStringLiteral("view-filter"), nullptr) + , m_plugin(p) +{ + setupUi(this); + connect(kcfg_useLevel1, &QCheckBox::toggled, this, &IPBlockingPrefPage::checkUseLevel1Toggled); + connect(m_download, &QPushButton::clicked, this, &IPBlockingPrefPage::downloadClicked); + connect(kcfg_autoUpdate, &QCheckBox::toggled, this, &IPBlockingPrefPage::autoUpdateToggled); + connect(kcfg_autoUpdateInterval, qOverload(&KPluralHandlingSpinBox::valueChanged), this, &IPBlockingPrefPage::autoUpdateIntervalChanged); + kcfg_autoUpdateInterval->setSuffix(ki18np(" day", " days")); + m_job = nullptr; + m_verbose = true; +} + +IPBlockingPrefPage::~IPBlockingPrefPage() +{ +} + +void IPBlockingPrefPage::checkUseLevel1Toggled(bool check) +{ + if (check) { + kcfg_filterURL->setEnabled(true); + m_download->setEnabled(true); + m_plugin->loadAntiP2P(); + } else { + m_status->setText(QString()); + kcfg_filterURL->setEnabled(false); + m_download->setEnabled(false); + m_plugin->unloadAntiP2P(); + } + + if (m_plugin->loadedAndRunning() && check) + m_status->setText(i18n("Status: Loaded and running.")); + else + m_status->setText(i18n("Status: Not loaded.")); + + updateAutoUpdate(); +} + +void IPBlockingPrefPage::loadDefaults() +{ + loadSettings(); +} + +void IPBlockingPrefPage::updateSettings() +{ + m_plugin->checkAutoUpdate(); +} + +void IPBlockingPrefPage::loadSettings() +{ + if (IPBlockingPluginSettings::useLevel1()) { + if (m_plugin->loadedAndRunning()) + m_status->setText(i18n("Status: Loaded and running.")); + else + m_status->setText(i18n("Status: Not loaded.")); + + kcfg_filterURL->setEnabled(true); + m_download->setEnabled(true); + m_last_updated->clear(); + m_next_update->clear(); + kcfg_autoUpdateInterval->setEnabled(IPBlockingPluginSettings::autoUpdate()); + m_auto_update_group_box->setEnabled(true); + } else { + m_status->setText(i18n("Status: Not loaded.")); + kcfg_filterURL->setEnabled(false); + m_download->setEnabled(false); + m_last_updated->clear(); + m_next_update->clear(); + kcfg_autoUpdateInterval->setEnabled(IPBlockingPluginSettings::autoUpdate()); + m_auto_update_group_box->setEnabled(false); + } + + updateAutoUpdate(); +} + +void IPBlockingPrefPage::downloadClicked() +{ + QUrl url = kcfg_filterURL->url(); + + // block GUI so you cannot do stuff during conversion + m_download->setEnabled(false); + m_status->setText(i18n("Status: Downloading and converting new block list...")); + kcfg_useLevel1->setEnabled(false); + kcfg_filterURL->setEnabled(false); + + m_plugin->unloadAntiP2P(); + m_job = new DownloadAndConvertJob(url, m_verbose ? DownloadAndConvertJob::Verbose : DownloadAndConvertJob::Quietly); + connect(m_job, &DownloadAndConvertJob::result, this, &IPBlockingPrefPage::downloadAndConvertFinished); + connect(m_job, &DownloadAndConvertJob::notification, m_plugin, &IPFilterPlugin::notification); + m_job->start(); +} + +bool IPBlockingPrefPage::doAutoUpdate() +{ + if (m_job) { + if (m_job->isAutoUpdate()) + return true; // if we are already auto updating, lets not start it again + else + return false; + } + + m_verbose = false; + Out(SYS_IPF | LOG_NOTICE) << "Doing ipfilter auto update !" << endl; + downloadClicked(); + m_verbose = true; + return true; +} + +void IPBlockingPrefPage::restoreGUI() +{ + m_download->setEnabled(true); + kcfg_useLevel1->setEnabled(true); + kcfg_filterURL->setEnabled(true); + + if (m_plugin->loadedAndRunning()) + m_status->setText(i18n("Status: Loaded and running.")); + else + m_status->setText(i18n("Status: Not loaded.")); +} + +void IPBlockingPrefPage::downloadAndConvertFinished(KJob *j) +{ + if (j != m_job) + return; + + KConfigGroup g = KSharedConfig::openConfig()->group("IPFilterAutoUpdate"); + if (!j->error()) { + g.writeEntry("last_updated", QDateTime::currentDateTime()); + g.writeEntry("last_update_ok", true); + } else { + g.writeEntry("last_update_attempt", QDateTime::currentDateTime()); + g.writeEntry("last_update_ok", false); + } + + g.sync(); + + m_job = nullptr; + m_plugin->loadAntiP2P(); + restoreGUI(); + updateAutoUpdate(); + updateFinished(); +} + +void IPBlockingPrefPage::updateAutoUpdate() +{ + if (!kcfg_useLevel1->isChecked()) { + m_next_update->clear(); + m_last_updated->clear(); + return; + } + + KConfigGroup g = KSharedConfig::openConfig()->group("IPFilterAutoUpdate"); + bool ok = g.readEntry("last_update_ok", true); + QDate last_updated = g.readEntry("last_updated", QDate()); + + if (last_updated.isNull()) + m_last_updated->setText(i18n("No update done yet.")); + else if (ok) + m_last_updated->setText(last_updated.toString()); + else + m_last_updated->setText(i18n("%1 (Last update attempt failed.)", last_updated.toString())); + + if (kcfg_autoUpdate->isChecked()) { + QDate next_update; + if (last_updated.isNull()) + next_update = QDate::currentDate().addDays(kcfg_autoUpdateInterval->value()); + else + next_update = last_updated.addDays(kcfg_autoUpdateInterval->value()); + + m_next_update->setText(next_update.toString()); + } else { + m_next_update->setText(i18n("Never")); + } +} + +void IPBlockingPrefPage::autoUpdateToggled(bool on) +{ + Q_UNUSED(on); + updateAutoUpdate(); +} + +void IPBlockingPrefPage::autoUpdateIntervalChanged(int val) +{ + Q_UNUSED(val); + updateAutoUpdate(); +} + +} diff --git a/plugins/ipfilter/ipblockingprefpage.h b/plugins/ipfilter/ipblockingprefpage.h new file mode 100644 index 0000000..2c02a10 --- /dev/null +++ b/plugins/ipfilter/ipblockingprefpage.h @@ -0,0 +1,61 @@ +/* + SPDX-FileCopyrightText: 2005 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef IPBLOCKINGPREFPAGE_H +#define IPBLOCKINGPREFPAGE_H + +#include "ipfilterplugin.h" +#include "ui_ipblockingprefpage.h" +#include +#include +#include + +class KJob; + +namespace kt +{ +class IPFilterPlugin; +class DownloadAndConvertJob; + +/** + * @author Ivan Vasic + * @brief IPBlocking plugin interface page + **/ +class IPBlockingPrefPage : public PrefPageInterface, public Ui_IPBlockingPrefPage +{ + Q_OBJECT +public: + IPBlockingPrefPage(IPFilterPlugin *p); + ~IPBlockingPrefPage() override; + + void loadSettings() override; + void loadDefaults() override; + void updateSettings() override; + + /// Do an auto update, return false if this is not possible + bool doAutoUpdate(); + +private Q_SLOTS: + void downloadClicked(); + void checkUseLevel1Toggled(bool); + void restoreGUI(); + void downloadAndConvertFinished(KJob *j); + void autoUpdateToggled(bool on); + void autoUpdateIntervalChanged(int val); + +private: + void updateAutoUpdate(); + +Q_SIGNALS: + void updateFinished(); + +private: + CoreInterface *m_core; + IPFilterPlugin *m_plugin; + DownloadAndConvertJob *m_job; + bool m_verbose; +}; +} +#endif diff --git a/plugins/ipfilter/ipblockingprefpage.ui b/plugins/ipfilter/ipblockingprefpage.ui new file mode 100644 index 0000000..49bd422 --- /dev/null +++ b/plugins/ipfilter/ipblockingprefpage.ui @@ -0,0 +1,278 @@ + + + IPBlockingPrefPage + + + + 0 + 0 + 564 + 444 + + + + IPBlocking Preferences + + + + + + true + + + PeerGuardian Filter File + + + + + + Enable this if you want the IP filter plugin to work. + + + Use PeerGuardian filter + + + + + + + + + + + + IP filter file: + + + false + + + + + + + Filter file to use, this can be a local file or a remote file. + + + + + + + + + + + Qt::Horizontal + + + QSizePolicy::Expanding + + + + 361 + 20 + + + + + + + + Download and convert the IP filter file. + + + Dow&nload/Convert + + + + + + + + + Download PeerGuardian filter from bluetack.co.uk or iblocklist.org. +NOTE: archive files like zip and tar.gz or tar.bz2 are supported. + + + false + + + + + + + + + + false + + + + + + + + + + Automatic Update + + + + + + + + Enable this if you want to automatically update the filter file. + + + Update file every: + + + + + + + + 0 + 0 + + + + + 12 + 0 + + + + Update interval in days. + + + 1 + + + 100000 + + + 5 + + + + + + + + + + + Last updated: + + + + + + + + 0 + 0 + + + + TextLabel + + + + + + + Next update: + + + + + + + + 0 + 0 + + + + TextLabel + + + + + + + + + + + + Qt::Vertical + + + QSizePolicy::Expanding + + + + 20 + 20 + + + + + + + + + + KUrlRequester + QFrame +
kurlrequester.h
+
+ + KPluralHandlingSpinBox + QSpinBox +
kpluralhandlingspinbox.h
+
+
+ + kurlrequester.h + + + + + kcfg_autoUpdate + toggled(bool) + kcfg_autoUpdateInterval + setEnabled(bool) + + + 90 + 244 + + + 360 + 244 + + + + + kcfg_useLevel1 + toggled(bool) + m_auto_update_group_box + setEnabled(bool) + + + 58 + 52 + + + 114 + 218 + + + + +
diff --git a/plugins/ipfilter/ipblocklist.cpp b/plugins/ipfilter/ipblocklist.cpp new file mode 100644 index 0000000..979ebd2 --- /dev/null +++ b/plugins/ipfilter/ipblocklist.cpp @@ -0,0 +1,110 @@ +/* + SPDX-FileCopyrightText: 2005 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "ipblocklist.h" +#include +#include +#include +#include + +using namespace bt; + +namespace kt +{ +static Uint32 StringToUint32(const QString &ip) +{ + bool test; + Uint32 ret = ip.section(QLatin1Char('.'), 0, 0).toULongLong(&test); + ret <<= 8; + ret |= ip.section(QLatin1Char('.'), 1, 1).toULong(&test); + ret <<= 8; + ret |= ip.section(QLatin1Char('.'), 2, 2).toULong(&test); + ret <<= 8; + ret |= ip.section(QLatin1Char('.'), 3, 3).toULong(&test); + + return ret; +} + +IPBlock::IPBlock() + : ip1(0) + , ip2(0) +{ +} + +IPBlock::IPBlock(const IPBlock &block) + : ip1(block.ip1) + , ip2(block.ip2) +{ +} + +IPBlock::IPBlock(const QString &start, const QString &end) +{ + ip1 = StringToUint32(start); + ip2 = StringToUint32(end); +} + +IPBlockList::IPBlockList() +{ +} + +IPBlockList::~IPBlockList() +{ +} + +bool IPBlockList::blocked(const net::Address &addr) const +{ + if (addr.protocol() == QAbstractSocket::IPv6Protocol || blocks.empty()) + return false; + + // Binary search the list of blocks which are sorted + quint32 ip = addr.toIPv4Address(); + int begin = 0; + int end = blocks.size() - 1; + while (true) { + if (begin == end) + return blocks[begin].contains(ip); + else if (begin == end - 1) + return blocks[begin].contains(ip) || blocks[end].contains(ip); + + int pivot = begin + (end - begin) / 2; + if (blocks[pivot].contains(ip)) + return true; + else if (ip < blocks[pivot].ip1) + end = pivot - 1; // continue in the range [begin, pivot - 1] + else // ip > blocks[pivot].ip2 + begin = pivot + 1; // continue in the range [pivot + 1, end] + } + return false; +} + +bool IPBlockList::load(const QString &path) +{ + QFile file(path); + if (!file.open(QIODevice::ReadOnly)) { + Out(SYS_IPF | LOG_NOTICE) << "Cannot open " << path << ": " << file.errorString() << endl; + return false; + } + + // Note: the conversion process has sorted the blocks ! + int num_blocks = file.size() / sizeof(IPBlock); + blocks.reserve(num_blocks); + while (!file.atEnd() && blocks.size() < num_blocks) { + IPBlock block; + if (file.read((char *)&block, sizeof(IPBlock)) == sizeof(IPBlock)) + addBlock(block); + else + break; + } + + Out(SYS_IPF | LOG_NOTICE) << "Loaded " << blocks.size() << " blocked IP ranges" << endl; + return true; +} + +void IPBlockList::addBlock(const IPBlock &block) +{ + blocks.append(block); +} + +} diff --git a/plugins/ipfilter/ipblocklist.h b/plugins/ipfilter/ipblocklist.h new file mode 100644 index 0000000..b4972c1 --- /dev/null +++ b/plugins/ipfilter/ipblocklist.h @@ -0,0 +1,64 @@ +/* + SPDX-FileCopyrightText: 2005 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef ANTIP2P_H +#define ANTIP2P_H + +#include + +#include +#include + +namespace kt +{ +struct IPBlock { + bt::Uint32 ip1; + bt::Uint32 ip2; + + IPBlock(); + IPBlock(const IPBlock &block); + IPBlock(const QString &start, const QString &end); + + bool contains(bt::Uint32 ip) const + { + return ip1 <= ip && ip <= ip2; + } +}; + +/** + * @author Ivan Vasic + * @brief This class is used to manage anti-p2p filter list, so called level1. + */ +class IPBlockList : public bt::BlockListInterface +{ +public: + IPBlockList(); + ~IPBlockList() override; + + bool blocked(const net::Address &addr) const override; + + /** + * Overloaded function. Uses Uint32 IP to be checked + **/ + bool isBlockedIP(bt::Uint32 ip); + + /** + * Loads filter file + * @param path The file to load + * @return true upon success, false otherwise + */ + bool load(const QString &path); + + /** + * Add a single block + * @param block + */ + void addBlock(const IPBlock &block); + +private: + QVector blocks; +}; +} +#endif diff --git a/plugins/ipfilter/ipfilterplugin.cpp b/plugins/ipfilter/ipfilterplugin.cpp new file mode 100644 index 0000000..3ff1f95 --- /dev/null +++ b/plugins/ipfilter/ipfilterplugin.cpp @@ -0,0 +1,142 @@ +/* + SPDX-FileCopyrightText: 2005 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "ipfilterplugin.h" + +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "ipblocklist.h" +#include "ipfilterpluginsettings.h" + +using namespace bt; + +K_PLUGIN_FACTORY_WITH_JSON(ktorrent_ipfilter, "ktorrent_ipfilter.json", registerPlugin();) + +namespace kt +{ +IPFilterPlugin::IPFilterPlugin(QObject *parent, const QVariantList &args) + : Plugin(parent) +{ + Q_UNUSED(args); + connect(&auto_update_timer, &QTimer::timeout, this, &IPFilterPlugin::checkAutoUpdate); + auto_update_timer.setSingleShot(true); +} + +IPFilterPlugin::~IPFilterPlugin() +{ +} + +void IPFilterPlugin::load() +{ + LogSystemManager::instance().registerSystem(i18n("IP Filter"), SYS_IPF); + pref = new IPBlockingPrefPage(this); + connect(pref, &IPBlockingPrefPage::updateFinished, this, &IPFilterPlugin::checkAutoUpdate); + getGUI()->addPrefPage(pref); + + if (IPBlockingPluginSettings::useLevel1()) + loadAntiP2P(); + + checkAutoUpdate(); +} + +void IPFilterPlugin::unload() +{ + LogSystemManager::instance().unregisterSystem(i18n("IP Filter")); + getGUI()->removePrefPage(pref); + delete pref; + pref = nullptr; + if (ip_filter) { + AccessManager::instance().removeBlockList(ip_filter.data()); + ip_filter.reset(); + } +} + +bool IPFilterPlugin::loadAntiP2P() +{ + if (ip_filter) + return true; + + ip_filter.reset(new IPBlockList()); + if (!ip_filter->load(kt::DataDir() + QStringLiteral("level1.dat"))) { + ip_filter.reset(); + return false; + } + AccessManager::instance().addBlockList(ip_filter.data()); + return true; +} + +bool IPFilterPlugin::unloadAntiP2P() +{ + if (ip_filter) { + AccessManager::instance().removeBlockList(ip_filter.data()); + ip_filter.reset(); + return true; + } else + return true; +} + +bool IPFilterPlugin::loadedAndRunning() +{ + return ip_filter; +} + +bool IPFilterPlugin::versionCheck(const QString &version) const +{ + return version == QStringLiteral(VERSION); +} + +void IPFilterPlugin::checkAutoUpdate() +{ + auto_update_timer.stop(); + if (!loadedAndRunning() || !IPBlockingPluginSettings::autoUpdate()) + return; + + KConfigGroup g = KSharedConfig::openConfig()->group("IPFilterAutoUpdate"); + bool ok = g.readEntry("last_update_ok", false); + QDateTime now = QDateTime::currentDateTime(); + if (!ok) { + QDateTime last_update_attempt = g.readEntry("last_update_attempt", now); + // if we cannot do it now, or the last attempt was less then 15 minute ago, try again in 15 minutes + if (last_update_attempt.secsTo(now) < AUTO_UPDATE_RETRY_INTERVAL || !pref->doAutoUpdate()) + auto_update_timer.start(AUTO_UPDATE_RETRY_INTERVAL * 1000); + } else { + QDateTime last_updated = g.readEntry("last_updated", QDateTime()); + QDateTime next_update; + if (last_updated.isNull()) + next_update = now.addDays(IPBlockingPluginSettings::autoUpdateInterval()); + else + next_update = QDateTime(last_updated).addDays(IPBlockingPluginSettings::autoUpdateInterval()); + + if (now >= next_update) { + if (!pref->doAutoUpdate()) // if we cannot do it now, try again in 15 minutes + auto_update_timer.start(AUTO_UPDATE_RETRY_INTERVAL * 1000); + } else { + // schedule an auto update + auto_update_timer.start(1000 * (now.secsTo(next_update) + 5)); + Out(SYS_IPF | LOG_NOTICE) << "Scheduling ipfilter auto update on " << next_update.toString() << endl; + } + } +} + +void IPFilterPlugin::notification(const QString &msg) +{ + KNotification::event(QStringLiteral("PluginEvent"), msg, QPixmap(), getGUI()->getMainWindow()); +} + +} + +#include diff --git a/plugins/ipfilter/ipfilterplugin.h b/plugins/ipfilter/ipfilterplugin.h new file mode 100644 index 0000000..2101d5c --- /dev/null +++ b/plugins/ipfilter/ipfilterplugin.h @@ -0,0 +1,63 @@ +/* + SPDX-FileCopyrightText: 2005 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KTIPFILTERPLUGIN_H +#define KTIPFILTERPLUGIN_H + +#include "ipblockingprefpage.h" +#include "ipblocklist.h" +#include +#include + +class QString; + +namespace kt +{ +class IPBlockingPrefPage; + +const int AUTO_UPDATE_RETRY_INTERVAL = 15 * 60; // seconds + +/** + * @author Ivan Vasic + * @brief IP filter plugin + * + * This plugin will load IP ranges from specific files into KT IPBlocklist. + */ +class IPFilterPlugin : public Plugin +{ + Q_OBJECT +public: + IPFilterPlugin(QObject *parent, const QVariantList &args); + ~IPFilterPlugin() override; + + void load() override; + void unload() override; + bool versionCheck(const QString &version) const override; + + /// Loads the KT format list filter + void loadFilters(); + + /// Loads the anti-p2p filter list + bool loadAntiP2P(); + + /// Unloads the anti-p2p filter list + bool unloadAntiP2P(); + + /// Whether or not the IP filter is loaded and running + bool loadedAndRunning(); + +public Q_SLOTS: + void checkAutoUpdate(); + void notification(const QString &msg); + +private: + IPBlockingPrefPage *pref; + QScopedPointer ip_filter; + QTimer auto_update_timer; +}; + +} + +#endif diff --git a/plugins/ipfilter/ipfilterpluginsettings.kcfgc b/plugins/ipfilter/ipfilterpluginsettings.kcfgc new file mode 100644 index 0000000..b9aeaeb --- /dev/null +++ b/plugins/ipfilter/ipfilterpluginsettings.kcfgc @@ -0,0 +1,7 @@ +# Code generation options for kconfig_compiler +File=ktipfilterplugin.kcfg +ClassName=IPBlockingPluginSettings +Namespace=kt +Singleton=true +Mutators=true +# will create the necessary code for setting those variables \ No newline at end of file diff --git a/plugins/ipfilter/ktipfilterplugin.kcfg b/plugins/ipfilter/ktipfilterplugin.kcfg new file mode 100644 index 0000000..4052f58 --- /dev/null +++ b/plugins/ipfilter/ktipfilterplugin.kcfg @@ -0,0 +1,26 @@ + + + + + + + + QUrl(QStringLiteral("http://list.iblocklist.com/?list=bt_level1&fileformat=p2p&archiveformat=gz")) + + + + false + + + false + + + 1 + 1000 + 7 + + + diff --git a/plugins/ipfilter/ktorrent_ipfilter.desktop b/plugins/ipfilter/ktorrent_ipfilter.desktop new file mode 100644 index 0000000..83e7083 --- /dev/null +++ b/plugins/ipfilter/ktorrent_ipfilter.desktop @@ -0,0 +1,111 @@ +[Desktop Entry] +Name=IP Filter +Name[ast]=Peñera d'IPs +Name[bg]=Филтър по IP +Name[bs]=IP filter +Name[ca]=Filtre d'IP +Name[ca@valencia]=Filtre d'IP +Name[cs]=Filtr IP +Name[da]=IP-filter +Name[de]=IP-Filter +Name[el]=Φίλτρο IP +Name[en_GB]=IP Filter +Name[eo]=IPFiltrilo +Name[es]=Filtro de IP +Name[et]=IP filter +Name[fi]=IP-suodatin +Name[fr]=Filtre d'adresses IP +Name[ga]=Scagaire IP +Name[gl]=Filtro de IP +Name[hr]=IP filtar +Name[hu]=IP-szűrő +Name[ia]=Filtro IP +Name[is]=IP síun +Name[it]=Filtro IP +Name[ja]=IP フィルタ +Name[kk]=IP сүзгісі +Name[km]=តម្រង IP +Name[ko]=IP 필터 +Name[lt]=IP filtras +Name[lv]=IP filtrs +Name[nb]=IP-filter +Name[nds]=IP-Filter +Name[nl]=IP-filter +Name[nn]=IP-filter +Name[pl]=Filtr IP +Name[pt]=Filtro de IPs +Name[pt_BR]=Filtro de IP +Name[ro]=Filtru IP +Name[ru]=IP-фильтр +Name[si]=IP පෙරහන +Name[sk]=IP Filter +Name[sl]=Filter IP-jev +Name[sq]=IP Filtër +Name[sr]=ИП филтер +Name[sr@ijekavian]=ИП филтер +Name[sr@ijekavianlatin]=IP filter +Name[sr@latin]=IP filter +Name[sv]=IP-filter +Name[tr]=IP Süzgeci +Name[ug]=IP سۈزگۈچ +Name[uk]=Фільтр IP +Name[x-test]=xxIP Filterxx +Name[zh_CN]=IP 过滤 +Name[zh_TW]=IP 過濾 +Comment=Filter IP addresses through a blocklist +Comment[bg]=Филтриране на IP-адресите с черен списък +Comment[bs]=Filtrirajte IP adrese kroz listu blokiranja +Comment[ca]=Filtra les adreces IP a través d'una llista de bloqueig +Comment[ca@valencia]=Filtra les adreces IP a través d'una llista de bloqueig +Comment[cs]=Filtrovat IP adresy podle seznamu blokovaných +Comment[da]=Filtrér IP-adresser gennem en blokeringsliste +Comment[de]=IP-Adressen mit einer Blockliste filtern +Comment[el]=Φίλτρο διευθύνσεων IP μέσω μιας λίστας αποκλεισμού +Comment[en_GB]=Filter IP addresses through a blocklist +Comment[es]=Filtre direcciones IP por medio de una lista de bloqueo +Comment[et]=IP-aadresside filtreerimine musta nimekirja alusel +Comment[fi]=Suodattaa IP-osoitteita estoluettelon avulla +Comment[fr]=Filtre les adresses IP grâce une liste de blocage +Comment[ga]=Scag seoltaí IP trí liosta dubh +Comment[gl]=Filtrar os enderezos IP mediante unha lista de bloqueos. +Comment[hu]=IP-címek szűrése tiltólista alapján +Comment[is]=Sía IP-vistföng miðað við svartan lista +Comment[it]=Filtra indirizzi IP attraverso una lista di blocco +Comment[ja]=ブロックリストによって IP アドレスをフィルタします +Comment[kk]=Бұғаттау тізімі бойынша IP-адрестерді сүзу +Comment[km]=តម្រង​អាសយដ្ឋាន IP តាម​បញ្ជី​ប្លុក +Comment[ko]=차단 목록으로 IP 주소 필터링 +Comment[lt]=Filtruoti IP adresus per blocklist'ą +Comment[lv]=Filtrē IP adreses ar bloķēšanas saraksta palīdzību +Comment[nb]=Filtrer IP-adresser gjennom en svarteliste +Comment[nds]=IP-Adressen över en Blockeerlist filtern +Comment[nl]=Filter IP-adressen via een blokkeerlijst +Comment[nn]=Filtrer IP-adresser gjennom ei blokkeringsliste +Comment[pl]=Filtrowanie adresów IP z wykorzystaniem spisu blokowanych adresów +Comment[pt]=Filtra os endereços IP através de uma lista de bloqueios +Comment[pt_BR]=Filtra os endereços IP através de uma lista de bloqueio +Comment[ro]=Filtrează adresele IP printr-o listă de blocare +Comment[ru]=Позволяет блокировать определённые IP-адреса +Comment[si]=වැලකුම් ලැයිස්තුවක් හරහා IP ලිපින පෙරයි +Comment[sk]=Filtrovať IP adresy cez blocklist +Comment[sl]=Filtriranje naslovov IP s seznamom blokiranih +Comment[sr]=Филтрирајте ИП адресе кроз листу блокирања +Comment[sr@ijekavian]=Филтрирајте ИП адресе кроз листу блокирања +Comment[sr@ijekavianlatin]=Filtrirajte IP adrese kroz listu blokiranja +Comment[sr@latin]=Filtrirajte IP adrese kroz listu blokiranja +Comment[sv]=Filtrera IP-adresser med en blockeringslista +Comment[tr]=Bir engel listesiyle IP adreslerini süzgeçten geçirir +Comment[uk]=Фільтрувати IP-адреси за списком блокування +Comment[x-test]=xxFilter IP addresses through a blocklistxx +Comment[zh_CN]=通过屏蔽列表过滤 IP 地址 +Comment[zh_TW]=透過黑名單過濾 IP 位址 +Type=Service +X-KDE-Library=ktipfilterplugin +X-KDE-PluginInfo-Author=Joris Guisson, Ivan Vasic +X-KDE-PluginInfo-Email=joris.guisson@gmail.com, ivasic@gmail.com +X-KDE-PluginInfo-Name=IPFilterPlugin +X-KDE-PluginInfo-Version=0.1 +X-KDE-PluginInfo-Website=http://kde.org/applications/internet/ktorrent/ +X-KDE-PluginInfo-License=GPL +X-KDE-PluginInfo-EnabledByDefault=false +Icon=view-filter diff --git a/plugins/ipfilter/tests/CMakeLists.txt b/plugins/ipfilter/tests/CMakeLists.txt new file mode 100644 index 0000000..6c34b2d --- /dev/null +++ b/plugins/ipfilter/tests/CMakeLists.txt @@ -0,0 +1,2 @@ +include(ECMAddTests) +ecm_add_test(ipblocklisttest.cpp ../ipblocklist.cpp TEST_NAME ipblocklisttest LINK_LIBRARIES ktcore Qt5::Core Qt5::Network KF5::Torrent Qt5::Test) diff --git a/plugins/ipfilter/tests/ipblocklisttest.cpp b/plugins/ipfilter/tests/ipblocklisttest.cpp new file mode 100644 index 0000000..6afc5df --- /dev/null +++ b/plugins/ipfilter/tests/ipblocklisttest.cpp @@ -0,0 +1,49 @@ +/* + SPDX-FileCopyrightText: 2012 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "../ipblocklist.h" +#include +#include +#include + +class IPBlockListTest : public QObject +{ + Q_OBJECT +private: +private Q_SLOTS: + void initTestCase() + { + bt::InitLog(QStringLiteral("ipblocklisttest.log"), false, true); + } + + void cleanupTestCase() + { + } + + void testBlockList() + { + kt::IPBlockList bl; + bl.addBlock(kt::IPBlock(QStringLiteral("1.0.0.0"), QStringLiteral("50.255.255.255"))); + bl.addBlock(kt::IPBlock(QStringLiteral("127.0.0.0"), QStringLiteral("127.255.255.255"))); + bl.addBlock(kt::IPBlock(QStringLiteral("129.0.0.0"), QStringLiteral("129.255.255.255"))); + bl.addBlock(kt::IPBlock(QStringLiteral("131.0.0.0"), QStringLiteral("137.255.255.255"))); + bl.addBlock(kt::IPBlock(QStringLiteral("140.0.0.0"), QStringLiteral("200.255.255.255"))); + + QVERIFY(bl.blocked(net::Address(QStringLiteral("25.25.25.25"), 0))); + QVERIFY(!bl.blocked(net::Address(QStringLiteral("75.25.25.25"), 0))); + QVERIFY(!bl.blocked(net::Address(QStringLiteral("126.255.255.255"), 0))); + QVERIFY(bl.blocked(net::Address(QStringLiteral("127.25.25.25"), 0))); + QVERIFY(!bl.blocked(net::Address(QStringLiteral("130.255.255.255"), 0))); + QVERIFY(bl.blocked(net::Address(QStringLiteral("135.25.25.25"), 0))); + QVERIFY(!bl.blocked(net::Address(QStringLiteral("138.255.255.255"), 0))); + QVERIFY(bl.blocked(net::Address(QStringLiteral("197.25.25.25"), 0))); + } + +private: +}; + +QTEST_MAIN(IPBlockListTest) + +#include "ipblocklisttest.moc" diff --git a/plugins/logviewer/CMakeLists.txt b/plugins/logviewer/CMakeLists.txt new file mode 100644 index 0000000..3ccfbc6 --- /dev/null +++ b/plugins/logviewer/CMakeLists.txt @@ -0,0 +1,25 @@ +add_library(ktorrent_logviewer MODULE) + +target_sources(ktorrent_logviewer PRIVATE + logviewerplugin.cpp + logflags.cpp + logviewer.cpp + logprefpage.cpp + logflagsdelegate.cpp +) + +ki18n_wrap_ui(ktorrent_logviewer logprefwidget.ui) +kconfig_add_kcfg_files(ktorrent_logviewer logviewerpluginsettings.kcfgc) + +kcoreaddons_desktop_to_json(ktorrent_logviewer ktorrent_logviewer.desktop) + +target_link_libraries( + ktorrent_logviewer + ktcore + KF5::Torrent + KF5::ConfigCore + KF5::I18n + KF5::XmlGui +) +install(TARGETS ktorrent_logviewer DESTINATION ${KTORRENT_PLUGIN_INSTALL_DIR} ) + diff --git a/plugins/logviewer/ktlogviewerplugin.kcfg b/plugins/logviewer/ktlogviewerplugin.kcfg new file mode 100644 index 0000000..6cfc9e8 --- /dev/null +++ b/plugins/logviewer/ktlogviewerplugin.kcfg @@ -0,0 +1,26 @@ + + + + + + + + + true + + + + 0 + 0 + 2 + + + 200 + 50 + 500000 + + + diff --git a/plugins/logviewer/ktorrent_logviewer.desktop b/plugins/logviewer/ktorrent_logviewer.desktop new file mode 100644 index 0000000..ee30dac --- /dev/null +++ b/plugins/logviewer/ktorrent_logviewer.desktop @@ -0,0 +1,113 @@ +[Desktop Entry] +Name=Log Viewer +Name[ar]=عارض السّجلات +Name[bg]=Преглед на дневника +Name[bs]=Prikazivač dnevnika +Name[ca]=Visor del registre +Name[ca@valencia]=Visor del registre +Name[cs]=Prohlížeč záznamů +Name[da]=Logfremviser +Name[de]=Protokollbetrachter +Name[el]=Προβολή καταγραφής +Name[en_GB]=Log Viewer +Name[eo]=Protokolorigardilo +Name[es]=Visor del registro +Name[et]=Loginäitaja +Name[fi]=Lokikatselin +Name[fr]=Afficheur de journaux +Name[ga]=Amharcán Logchomhad +Name[gl]=Visor do rexistro +Name[hr]=Preglednik zapisa +Name[hu]=Naplómegjelenítő +Name[ia]=Visor de registro +Name[is]=Annálaskoðari +Name[it]=Visore registro +Name[ja]=ログビューア +Name[kk]=Журналын қарау +Name[km]=កម្មវិធី​មើល​កំណត់ហេតុ +Name[ko]=로그 뷰어 +Name[lt]=Žurnalo žiūryklė +Name[lv]=Žurnāla skatītājs +Name[mr]=लॉग प्रदर्शक +Name[nb]=Loggviser +Name[nds]=Logbookkieker +Name[nl]=Log-weergave +Name[nn]=Loggvisar +Name[pl]=Przeglądarka dziennika +Name[pt]=Visualizador do Registo +Name[pt_BR]=Visualizador de registros (logs) +Name[ro]=Vizualizator jurnal +Name[ru]=Просмотр журналов +Name[si]=වාර්තා දසුන +Name[sk]=Prehliadač záznamov +Name[sl]=Pregledovalnik dnevnika +Name[sq]=Shikuesi i log +Name[sr]=Приказивач дневника +Name[sr@ijekavian]=Приказивач дневника +Name[sr@ijekavianlatin]=Prikazivač dnevnika +Name[sr@latin]=Prikazivač dnevnika +Name[sv]=Loggvisning +Name[tr]=Günlük İzleyicisi +Name[ug]=خاتىرە كۆرگۈچ +Name[uk]=Переглядач журналу +Name[x-test]=xxLog Viewerxx +Name[zh_CN]=日志查看器 +Name[zh_TW]=紀錄檢視器 +Comment=Displays the logging output +Comment[bg]=Показване на изхода от дневника +Comment[bs]=Prikazuje upise dnevnika +Comment[ca]=Mostra la sortida del registre +Comment[ca@valencia]=Mostra l'eixida del registre +Comment[cs]=Zobrazí výstup záznamu +Comment[da]=Viser log-outputtet +Comment[de]=Zeigt die Protokollausgaben +Comment[el]=Εμφάνιση εξόδου της καταγραφής +Comment[en_GB]=Displays the logging output +Comment[es]=Muestra la salida del registro +Comment[et]=Logiväljundi näitamine +Comment[fi]=Näyttää lokitulosteen +Comment[fr]=Affiche la sortie de journalisation +Comment[ga]=Taispeáin an t-aschur logála +Comment[gl]=Mostra o rexistro da saída. +Comment[hu]=A naplózás kimenetének megjelenítése +Comment[ia]=Monstra le exito de registrar +Comment[is]=Birtir úttak annáls +Comment[it]=Mostra l'output del registro +Comment[ja]=ログ出力を表示します +Comment[kk]=Журнал жазуын көрсету +Comment[km]=បង្ហាញ​លទ្ធផល​នៃ​ការ​ចុះ​កំណត់ហេតុ +Comment[ko]=로그 출력 표시 +Comment[lt]=Rodo žurnalo išvestį +Comment[lv]=Rāda žurnāla izvadu +Comment[nb]=Viser loggen +Comment[nds]=Wiest de Logbook-Utgaven +Comment[nl]=Toon de log-uitvoer +Comment[nn]=Viser loggen +Comment[pl]=Wyświetla dziennik powiadomień programu +Comment[pt]=Mostra o resultado de registo +Comment[pt_BR]=Exibe o log de saída +Comment[ro]=Afișează înregistrările din jurnal +Comment[ru]=Отображение журналов работы KTorrent +Comment[si]=වාර්තා ප්‍රතිදානය දර්ශනය කරයි +Comment[sk]=Zobrazí protokolovací výstup +Comment[sl]=Prikazuje izhod beleženja v dnevnik +Comment[sr]=Приказује уписе дневника +Comment[sr@ijekavian]=Приказује уписе дневника +Comment[sr@ijekavianlatin]=Prikazuje upise dnevnika +Comment[sr@latin]=Prikazuje upise dnevnika +Comment[sv]=Visar loggutmatning +Comment[tr]=Günlük kaydı çıktısını görüntüler +Comment[uk]=Показати відомості з журналу +Comment[x-test]=xxDisplays the logging outputxx +Comment[zh_CN]=显示日志输出 +Comment[zh_TW]=顯示紀錄輸出 +Type=Service +X-KDE-Library=ktlogviewerplugin +X-KDE-PluginInfo-Author=Joris Guisson, Ivan Vasic +X-KDE-PluginInfo-Email=joris.guisson@gmail.com, ivasic@gmail.com +X-KDE-PluginInfo-Name=LogViewerPlugin +X-KDE-PluginInfo-Version=0.1 +X-KDE-PluginInfo-Website=http://kde.org/applications/internet/ktorrent/ +X-KDE-PluginInfo-License=GPL +X-KDE-PluginInfo-EnabledByDefault=false +Icon=utilities-log-viewer diff --git a/plugins/logviewer/logflags.cpp b/plugins/logviewer/logflags.cpp new file mode 100644 index 0000000..a649285 --- /dev/null +++ b/plugins/logviewer/logflags.cpp @@ -0,0 +1,228 @@ +/* + SPDX-FileCopyrightText: 2006 Ivan Vasić + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "logflags.h" + +#include + +#include +#include + +#include +#include +#include + +#include "logviewer.h" +#include "logviewerpluginsettings.h" + +using namespace bt; + +namespace kt +{ +LogFlags::LogFlags() +{ + updateFlags(); + LogSystemManager &lsman = LogSystemManager::instance(); + connect(&lsman, &bt::LogSystemManager::registered, this, &LogFlags::registered); + connect(&lsman, &bt::LogSystemManager::unregisted, this, &LogFlags::unregistered); +} + +LogFlags::~LogFlags() +{ +} + +bool LogFlags::checkFlags(unsigned int arg) +{ + QList::iterator i = log_flags.begin(); + while (i != log_flags.end()) { + const LogFlag &f = *i; + if (f.id & arg) + return f.flag & arg; + i++; + } + + return false; +} + +void LogFlags::updateFlags() +{ + KConfigGroup cfg = KSharedConfig::openConfig()->group("LogFlags"); + log_flags.clear(); + const LogSystemManager &lsman = LogSystemManager::instance(); + for (LogSystemManager::const_iterator i = lsman.begin(); i != lsman.end(); i++) { + LogFlag f; + f.name = i.key(); + f.id = i.value(); + f.flag = cfg.readEntry(QStringLiteral("sys_%1").arg(i.value()), LOG_ALL); + log_flags.append(f); + } +} + +QString LogFlags::getFormattedMessage(unsigned int arg, const QString &line) +{ + if ((arg & LOG_ALL) == LOG_ALL) + return line; + + if (arg & 0x04) { // Debug + return QStringLiteral("%1").arg(line); + } + + if (arg & 0x02) // Notice + return line; + + if (arg & 0x01) { // Important + return QStringLiteral("%1").arg(line); + } + + return line; +} + +int LogFlags::rowCount(const QModelIndex &parent) const +{ + if (!parent.isValid()) + return log_flags.count(); + else + return 0; +} + +int LogFlags::columnCount(const QModelIndex &parent) const +{ + if (!parent.isValid()) + return 2; + else + return 0; +} + +QVariant LogFlags::headerData(int section, Qt::Orientation orientation, int role) const +{ + if (role != Qt::DisplayRole || orientation != Qt::Horizontal) + return QVariant(); + + switch (section) { + case 0: + return i18n("System"); + case 1: + return i18n("Log Level"); + default: + return QVariant(); + } +} + +QVariant LogFlags::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) + return QVariant(); + + if (role == Qt::DisplayRole) { + const LogFlag &f = log_flags.at(index.row()); + switch (index.column()) { + case 0: + return f.name; + case 1: + return flagToString(f.flag); + default: + return QVariant(); + } + } else if (role == Qt::EditRole && index.column() == 1) { + const LogFlag &f = log_flags.at(index.row()); + return f.flag; + } + + return QVariant(); +} + +bool LogFlags::setData(const QModelIndex &index, const QVariant &value, int role) +{ + if (!index.isValid() || role != Qt::EditRole || index.column() != 1) + return false; + + bt::Uint32 flag = value.toUInt(); + if (flag != LOG_ALL && flag != LOG_NONE && flag != LOG_DEBUG && flag != LOG_NOTICE && flag != LOG_IMPORTANT) + return false; + + LogFlag &f = log_flags[index.row()]; + f.flag = flag; + + KConfigGroup cfg = KSharedConfig::openConfig()->group("LogFlags"); + cfg.writeEntry(QStringLiteral("sys_%1").arg(f.id), flag); + cfg.sync(); + + Q_EMIT dataChanged(index, index); + return true; +} + +Qt::ItemFlags LogFlags::flags(const QModelIndex &index) const +{ + if (!index.isValid()) + return Qt::ItemIsEnabled; + + if (index.column() == 1) + return QAbstractItemModel::flags(index) | Qt::ItemIsEditable; + else + return QAbstractItemModel::flags(index); +} + +bool LogFlags::removeRows(int row, int count, const QModelIndex &parent) +{ + Q_UNUSED(parent); + + beginRemoveRows(QModelIndex(), row, row + count - 1); + endRemoveRows(); + return true; +} + +bool LogFlags::insertRows(int row, int count, const QModelIndex &parent) +{ + Q_UNUSED(parent); + + beginInsertRows(QModelIndex(), row, row + count - 1); + endInsertRows(); + return true; +} + +QString LogFlags::flagToString(bt::Uint32 flag) const +{ + switch (flag) { + case LOG_DEBUG: + return i18n("Debug"); + case LOG_NOTICE: + return i18n("Notice"); + case LOG_IMPORTANT: + return i18n("Important"); + case LOG_ALL: + return i18n("All"); + case LOG_NONE: + return i18n("None"); + default: + return QString(); + } +} + +void LogFlags::registered(const QString &sys) +{ + KConfigGroup cfg = KSharedConfig::openConfig()->group("LogFlags"); + + LogSystemManager &lsman = LogSystemManager::instance(); + LogFlag f; + f.id = lsman.systemID(sys); + f.flag = cfg.readEntry(QStringLiteral("sys_%1").arg(f.id), LOG_ALL); + ; + f.name = sys; + log_flags.append(f); + insertRow(log_flags.count() - 1); +} + +void LogFlags::unregistered(const QString &sys) +{ + int idx = 0; + for (const LogFlag &f : qAsConst(log_flags)) { + if (sys == f.name) { + removeRow(idx); + log_flags.removeAt(idx); + break; + } + idx++; + } +} +} diff --git a/plugins/logviewer/logflags.h b/plugins/logviewer/logflags.h new file mode 100644 index 0000000..88b48fe --- /dev/null +++ b/plugins/logviewer/logflags.h @@ -0,0 +1,67 @@ +/* + SPDX-FileCopyrightText: 2006 Ivan Vasić + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KTLOGFLAGS_H +#define KTLOGFLAGS_H + +#include +#include +#include + +class QString; + +namespace kt +{ +/** + * Class to read/save logging messages flags. + * @author Ivan Vasic + */ +class LogFlags : public QAbstractTableModel +{ + Q_OBJECT + +public: + LogFlags(); + ~LogFlags() override; + + static LogFlags &instance(); + + /// Checks current flags with arg. Return true if message should be shown + bool checkFlags(unsigned int arg); + + /// Updates flags from Settings:: + void updateFlags(); + + /// Makes line rich text according to arg level. + QString getFormattedMessage(unsigned int arg, const QString &line); + + int rowCount(const QModelIndex &parent) const override; + int columnCount(const QModelIndex &parent) const override; + QVariant headerData(int section, Qt::Orientation orientation, int role) const override; + QVariant data(const QModelIndex &index, int role) const override; + bool setData(const QModelIndex &index, const QVariant &value, int role) override; + Qt::ItemFlags flags(const QModelIndex &index) const override; + bool removeRows(int row, int count, const QModelIndex &parent) override; + bool insertRows(int row, int count, const QModelIndex &parent) override; + +private Q_SLOTS: + void registered(const QString &sys); + void unregistered(const QString &sys); + +private: + QString flagToString(bt::Uint32 flag) const; + +private: + struct LogFlag { + QString name; + bt::Uint32 id; + bt::Uint32 flag; + }; + QList log_flags; +}; + +} + +#endif diff --git a/plugins/logviewer/logflagsdelegate.cpp b/plugins/logviewer/logflagsdelegate.cpp new file mode 100644 index 0000000..c32bcf0 --- /dev/null +++ b/plugins/logviewer/logflagsdelegate.cpp @@ -0,0 +1,89 @@ +/* + SPDX-FileCopyrightText: 2008 Joris Guisson + SPDX-FileCopyrightText: 2008 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include +#include + +#include "logflagsdelegate.h" +#include + +namespace kt +{ +LogFlagsDelegate::LogFlagsDelegate(QObject *parent) + : QItemDelegate(parent) +{ +} + +LogFlagsDelegate::~LogFlagsDelegate() +{ +} + +QWidget *LogFlagsDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &, const QModelIndex &index) const +{ + Q_UNUSED(index); + QComboBox *editor = new QComboBox(parent); + editor->addItem(i18n("All")); + editor->addItem(i18n("Important")); + editor->addItem(i18n("Notice")); + editor->addItem(i18n("Debug")); + editor->addItem(i18n("None")); + return editor; +} + +void LogFlagsDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const +{ + bt::Uint32 value = index.model()->data(index, Qt::EditRole).toUInt(); + QComboBox *cb = static_cast(editor); + switch (value) { + case LOG_DEBUG: + cb->setCurrentIndex(3); + break; + case LOG_NOTICE: + cb->setCurrentIndex(2); + break; + case LOG_IMPORTANT: + cb->setCurrentIndex(1); + break; + case LOG_ALL: + cb->setCurrentIndex(0); + break; + case LOG_NONE: + cb->setCurrentIndex(4); + break; + } +} + +void LogFlagsDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const +{ + QComboBox *cb = static_cast(editor); + switch (cb->currentIndex()) { + case 0: + model->setData(index, LOG_ALL, Qt::EditRole); + break; + case 1: + model->setData(index, LOG_IMPORTANT, Qt::EditRole); + break; + case 2: + model->setData(index, LOG_NOTICE, Qt::EditRole); + break; + case 3: + model->setData(index, LOG_DEBUG, Qt::EditRole); + break; + case 4: + model->setData(index, LOG_NONE, Qt::EditRole); + break; + } +} + +QSize LogFlagsDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const +{ + Q_UNUSED(option); + Q_UNUSED(index); + + QComboBox tmp; + return QSize(100, tmp.sizeHint().height()); +} +} diff --git a/plugins/logviewer/logflagsdelegate.h b/plugins/logviewer/logflagsdelegate.h new file mode 100644 index 0000000..af76142 --- /dev/null +++ b/plugins/logviewer/logflagsdelegate.h @@ -0,0 +1,32 @@ +/* + SPDX-FileCopyrightText: 2008 Joris Guisson + SPDX-FileCopyrightText: 2008 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KTLOGFLAGSDELEGATE_H +#define KTLOGFLAGSDELEGATE_H + +#include + +namespace kt +{ +/** + @author +*/ +class LogFlagsDelegate : public QItemDelegate +{ + Q_OBJECT +public: + LogFlagsDelegate(QObject *parent); + ~LogFlagsDelegate() override; + + QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const override; + void setEditorData(QWidget *editor, const QModelIndex &index) const override; + void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const override; + QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override; +}; + +} + +#endif diff --git a/plugins/logviewer/logprefpage.cpp b/plugins/logviewer/logprefpage.cpp new file mode 100644 index 0000000..c2bd87b --- /dev/null +++ b/plugins/logviewer/logprefpage.cpp @@ -0,0 +1,65 @@ +/* + SPDX-FileCopyrightText: 2007 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "logprefpage.h" + +#include +#include + +#include "logflags.h" +#include "logflagsdelegate.h" +#include "logviewerpluginsettings.h" + +namespace kt +{ +LogPrefPage::LogPrefPage(LogFlags *flags, QWidget *parent) + : PrefPageInterface(LogViewerPluginSettings::self(), i18n("Log Viewer"), QStringLiteral("utilities-log-viewer"), parent) +{ + setupUi(this); + m_logging_flags->setModel(flags); + m_logging_flags->setItemDelegate(new LogFlagsDelegate(this)); + state_loaded = false; +} + +LogPrefPage::~LogPrefPage() +{ +} + +void LogPrefPage::saveState() +{ + KConfigGroup g = KSharedConfig::openConfig()->group("LogFlags"); + QByteArray s = m_logging_flags->header()->saveState(); + g.writeEntry("logging_flags_view_state", s.toBase64()); + g.sync(); +} + +void LogPrefPage::loadState() +{ + KConfigGroup g = KSharedConfig::openConfig()->group("LogFlags"); + QByteArray s = QByteArray::fromBase64(g.readEntry("logging_flags_view_state", QByteArray())); + if (!s.isEmpty()) + m_logging_flags->header()->restoreState(s); +} + +void LogPrefPage::loadDefaults() +{ + if (!state_loaded) { + loadState(); + state_loaded = true; + } +} + +void LogPrefPage::loadSettings() +{ + if (!state_loaded) { + loadState(); + state_loaded = true; + } +} + +void LogPrefPage::updateSettings() +{ +} +} diff --git a/plugins/logviewer/logprefpage.h b/plugins/logviewer/logprefpage.h new file mode 100644 index 0000000..5edd6b0 --- /dev/null +++ b/plugins/logviewer/logprefpage.h @@ -0,0 +1,35 @@ +/* + SPDX-FileCopyrightText: 2007 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KTLOGPREFPAGE_H +#define KTLOGPREFPAGE_H + +#include "ui_logprefwidget.h" +#include + +namespace kt +{ +class LogFlags; + +class LogPrefPage : public PrefPageInterface, public Ui_LogPrefWidget +{ + Q_OBJECT +public: + LogPrefPage(LogFlags *flags, QWidget *parent); + ~LogPrefPage() override; + + void loadDefaults() override; + void loadSettings() override; + void updateSettings() override; + + void saveState(); + void loadState(); + +private: + bool state_loaded; +}; +} + +#endif diff --git a/plugins/logviewer/logprefwidget.ui b/plugins/logviewer/logprefwidget.ui new file mode 100644 index 0000000..5ae2edb --- /dev/null +++ b/plugins/logviewer/logprefwidget.ui @@ -0,0 +1,132 @@ + + + LogPrefWidget + + + + 0 + 0 + 456 + 421 + + + + + + + Double click on the log level to alter it. The possible levels are : +<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"></p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-weight:600;">All: </span>All messages are shown</p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-weight:600;">Important: </span>Only important messages are shown</p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-weight:600;">Notice: </span>Only notice and important messages are shown</p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-weight:600;">Debug: </span>Debug, notice and important messages are shown</p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-weight:600;">None:</span> No messages are shown</p> + + + true + + + false + + + + + + + Options + + + + + + Whether or not to use rich text or just plain text, for the logging output shown in the logviewer. + + + Use rich text for logging output + + + + + + + + + Log widget position: + + + + + + + + 0 + 0 + + + + + Separate activity + + + + + Dockable widget + + + + + Torrent activity + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + Maximum number of visible lines: + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + + diff --git a/plugins/logviewer/logviewer.cpp b/plugins/logviewer/logviewer.cpp new file mode 100644 index 0000000..1a93a94 --- /dev/null +++ b/plugins/logviewer/logviewer.cpp @@ -0,0 +1,127 @@ +/* + SPDX-FileCopyrightText: 2005 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "logviewer.h" + +#include +#include +#include +#include + +#include +#include + +#include "logflags.h" +#include "logviewerpluginsettings.h" + +namespace kt +{ +LogViewer::LogViewer(LogFlags *flags, QWidget *parent) + : Activity(i18n("Log"), QStringLiteral("utilities-log-viewer"), 100, parent) + , use_rich_text(true) + , flags(flags) + , suspended(false) + , menu(nullptr) + , max_block_count(200) +{ + setToolTip(i18n("View the logging output generated by KTorrent")); + QVBoxLayout *layout = new QVBoxLayout(this); + output = new QTextBrowser(this); + layout->setMargin(0); + layout->setSpacing(0); + layout->addWidget(output); + output->document()->setMaximumBlockCount(max_block_count); + output->setContextMenuPolicy(Qt::CustomContextMenu); + connect(output, &QTextBrowser::customContextMenuRequested, this, &LogViewer::showMenu); + + suspend_action = new QAction(QIcon::fromTheme(QStringLiteral("media-playback-pause")), i18n("Suspend Output"), this); + suspend_action->setCheckable(true); + connect(suspend_action, &QAction::toggled, this, &LogViewer::suspend); +} + +LogViewer::~LogViewer() +{ +} + +void LogViewer::message(const QString &line, unsigned int arg) +{ + if (suspended) + return; + + /* + IMPORTANT: because QTextBrowser is not thread safe, we must use the Qt event mechanism + to add strings to it, this will ensure that strings will only be added in the main application + thread. + */ + if (arg == 0x00 || flags->checkFlags(arg)) { + if (!mutex.tryLock()) // Drop the message if we cannot acquire the lock, no deadlocks + return; + + if (use_rich_text) { + pending.append(flags->getFormattedMessage(arg, line)); + } else { + pending.append(line); + } + + while (pending.size() > max_block_count) + pending.pop_front(); + mutex.unlock(); + } +} + +void LogViewer::processPending() +{ + // Copy to tmp list so that we do not get a deadlock when Qt tries to print something when we add lines to the output + QStringList tmp; + { + if (!mutex.tryLock()) // No deadlocks + return; + + tmp = pending; + pending.clear(); + mutex.unlock(); + } + + for (const QString &line : qAsConst(tmp)) { + QTextCharFormat fm = output->currentCharFormat(); + output->append(line); + output->setCurrentCharFormat(fm); + } +} + +void LogViewer::setRichText(bool val) +{ + use_rich_text = val; +} + +void LogViewer::setMaxBlockCount(int max) +{ + max_block_count = max; + output->document()->setMaximumBlockCount(max); +} + +void LogViewer::showMenu(const QPoint &pos) +{ + if (!menu) { + menu = output->createStandardContextMenu(); + QAction *first = menu->actions().at(0); + QAction *sep = menu->insertSeparator(first); + menu->insertAction(sep, suspend_action); + } + menu->popup(output->viewport()->mapToGlobal(pos)); +} + +void LogViewer::suspend(bool on) +{ + suspended = on; + QTextCharFormat fm = output->currentCharFormat(); + if (on) + output->append(i18n("Logging output suspended")); + else + output->append(i18n("Logging output resumed")); + output->setCurrentCharFormat(fm); +} + +} diff --git a/plugins/logviewer/logviewer.h b/plugins/logviewer/logviewer.h new file mode 100644 index 0000000..2db119a --- /dev/null +++ b/plugins/logviewer/logviewer.h @@ -0,0 +1,52 @@ +/* + SPDX-FileCopyrightText: 2005 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#ifndef KTLOGVIEWER_H +#define KTLOGVIEWER_H + +#include +#include + +#include "logflags.h" +#include +#include + +namespace kt +{ +/** + * @author Joris Guisson + */ +class LogViewer : public Activity, public bt::LogMonitorInterface +{ + Q_OBJECT +public: + LogViewer(LogFlags *flags, QWidget *parent = nullptr); + ~LogViewer() override; + + void message(const QString &line, unsigned int arg) override; + + void setRichText(bool val); + void setMaxBlockCount(int max); + void processPending(); + +public Q_SLOTS: + void showMenu(const QPoint &pos); + void suspend(bool on); + +private: + bool use_rich_text; + LogFlags *flags; + QTextBrowser *output; + bool suspended; + QMenu *menu; + QAction *suspend_action; + int max_block_count; + + QMutex mutex; + QStringList pending; +}; + +} + +#endif diff --git a/plugins/logviewer/logviewerplugin.cpp b/plugins/logviewer/logviewerplugin.cpp new file mode 100644 index 0000000..27eb87e --- /dev/null +++ b/plugins/logviewer/logviewerplugin.cpp @@ -0,0 +1,137 @@ +/* + SPDX-FileCopyrightText: 2005-2007 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include + +#include +#include +#include + +#include "logflags.h" +#include "logprefpage.h" +#include "logviewer.h" +#include "logviewerplugin.h" +#include "logviewerpluginsettings.h" +#include +#include +#include +#include +#include + +using namespace bt; + +K_PLUGIN_FACTORY_WITH_JSON(ktorrent_logviewer, "ktorrent_logviewer.json", registerPlugin();) + +namespace kt +{ +LogViewerPlugin::LogViewerPlugin(QObject *parent, const QVariantList &) + : Plugin(parent) + , lv(nullptr) + , pref(nullptr) + , flags(nullptr) + , dock(nullptr) + , pos(SEPARATE_ACTIVITY) +{ +} + +LogViewerPlugin::~LogViewerPlugin() +{ +} + +void LogViewerPlugin::load() +{ + connect(getCore(), &CoreInterface::settingsChanged, this, &LogViewerPlugin::applySettings); + flags = new LogFlags(); + lv = new LogViewer(flags); + pref = new LogPrefPage(flags, nullptr); + + pos = (LogViewerPosition)LogViewerPluginSettings::logWidgetPosition(); + addLogViewerToGUI(); + getGUI()->addPrefPage(pref); + AddLogMonitor(lv); + applySettings(); +} + +void LogViewerPlugin::unload() +{ + pref->saveState(); + disconnect(getCore(), &CoreInterface::settingsChanged, this, &LogViewerPlugin::applySettings); + getGUI()->removePrefPage(pref); + removeLogViewerFromGUI(); + RemoveLogMonitor(lv); + delete lv; + lv = nullptr; + delete pref; + pref = nullptr; + delete flags; + flags = nullptr; +} + +void LogViewerPlugin::applySettings() +{ + lv->setRichText(LogViewerPluginSettings::useRichText()); + lv->setMaxBlockCount(LogViewerPluginSettings::maxBlockCount()); + LogViewerPosition p = (LogViewerPosition)LogViewerPluginSettings::logWidgetPosition(); + if (pos != p) { + removeLogViewerFromGUI(); + pos = p; + addLogViewerToGUI(); + } +} + +void LogViewerPlugin::addLogViewerToGUI() +{ + switch (pos) { + case SEPARATE_ACTIVITY: + getGUI()->addActivity(lv); + break; + case DOCKABLE_WIDGET: { + KMainWindow *mwnd = getGUI()->getMainWindow(); + dock = new QDockWidget(mwnd); + dock->setWidget(lv); + dock->setObjectName(QStringLiteral("LogViewerDockWidget")); + mwnd->addDockWidget(Qt::BottomDockWidgetArea, dock); + break; + } + case TORRENT_ACTIVITY: + getGUI()->getTorrentActivity()->addToolWidget(lv, lv->name(), lv->icon(), lv->toolTip()); + break; + } +} + +void LogViewerPlugin::removeLogViewerFromGUI() +{ + switch (pos) { + case SEPARATE_ACTIVITY: + getGUI()->removeActivity(lv); + break; + case TORRENT_ACTIVITY: + getGUI()->getTorrentActivity()->removeToolWidget(lv); + break; + case DOCKABLE_WIDGET: { + KMainWindow *mwnd = getGUI()->getMainWindow(); + mwnd->removeDockWidget(dock); + dock->setWidget(nullptr); + lv->setParent(nullptr); + delete dock; + dock = nullptr; + break; + } + } +} + +void LogViewerPlugin::guiUpdate() +{ + if (lv) + lv->processPending(); +} + +bool LogViewerPlugin::versionCheck(const QString &version) const +{ + return version == QStringLiteral(VERSION); +} + +} +#include "logviewerplugin.moc" diff --git a/plugins/logviewer/logviewerplugin.h b/plugins/logviewer/logviewerplugin.h new file mode 100644 index 0000000..03b03ab --- /dev/null +++ b/plugins/logviewer/logviewerplugin.h @@ -0,0 +1,58 @@ +/* + SPDX-FileCopyrightText: 2005 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KTLOGVIEWERPLUGIN_H +#define KTLOGVIEWERPLUGIN_H + +#include +#include + +class QDockWidget; + +namespace kt +{ +class LogViewer; +class LogPrefPage; +class LogFlags; + +enum LogViewerPosition { + SEPARATE_ACTIVITY = 0, + DOCKABLE_WIDGET = 1, + TORRENT_ACTIVITY = 2, +}; + +/** + * @author Joris Guisson + */ +class LogViewerPlugin : public Plugin +{ + Q_OBJECT +public: + LogViewerPlugin(QObject *parent, const QVariantList &args); + ~LogViewerPlugin() override; + + void load() override; + void unload() override; + bool versionCheck(const QString &version) const override; + void guiUpdate() override; + +private Q_SLOTS: + void applySettings(); + +private: + void addLogViewerToGUI(); + void removeLogViewerFromGUI(); + +private: + LogViewer *lv; + LogPrefPage *pref; + LogFlags *flags; + QDockWidget *dock; + LogViewerPosition pos; +}; + +} + +#endif diff --git a/plugins/logviewer/logviewerpluginsettings.kcfgc b/plugins/logviewer/logviewerpluginsettings.kcfgc new file mode 100644 index 0000000..17e71a7 --- /dev/null +++ b/plugins/logviewer/logviewerpluginsettings.kcfgc @@ -0,0 +1,7 @@ +# Code generation options for kconfig_compiler +File=ktlogviewerplugin.kcfg +ClassName=LogViewerPluginSettings +Namespace=kt +Singleton=true +Mutators=true +# will create the necessary code for setting those variables diff --git a/plugins/magnetgenerator/CMakeLists.txt b/plugins/magnetgenerator/CMakeLists.txt new file mode 100644 index 0000000..cf6ac29 --- /dev/null +++ b/plugins/magnetgenerator/CMakeLists.txt @@ -0,0 +1,25 @@ +add_library(ktorrent_magnetgenerator MODULE) + +target_sources(ktorrent_magnetgenerator PRIVATE + magnetgeneratorprefwidget.cpp + magnetgeneratorplugin.cpp) + +ki18n_wrap_ui(ktorrent_magnetgenerator magnetgeneratorprefwidget.ui) +kconfig_add_kcfg_files(ktorrent_magnetgenerator magnetgeneratorpluginsettings.kcfgc) + +kcoreaddons_desktop_to_json(ktorrent_magnetgenerator ktorrent_magnetgenerator.desktop) + +target_link_libraries( + ktorrent_magnetgenerator + ktcore + KF5::Torrent + KF5::CoreAddons + KF5::I18n + KF5::Notifications + KF5::XmlGui +) + +target_include_directories(ktorrent_magnetgenerator PRIVATE "$") + +install(TARGETS ktorrent_magnetgenerator DESTINATION ${KTORRENT_PLUGIN_INSTALL_DIR} ) +install(FILES ktorrent_magnetgeneratorui.rc DESTINATION ${KXMLGUI_INSTALL_DIR}/ktorrent ) diff --git a/plugins/magnetgenerator/ktmagnetgeneratorplugin.kcfg b/plugins/magnetgenerator/ktmagnetgeneratorplugin.kcfg new file mode 100644 index 0000000..92740ed --- /dev/null +++ b/plugins/magnetgenerator/ktmagnetgeneratorplugin.kcfg @@ -0,0 +1,34 @@ + + + + + + + + true + + + + false + + + + http://tracker.openbittorrent.com/announce + + + + true + + + + true + + + + true + + + diff --git a/plugins/magnetgenerator/ktorrent_magnetgenerator.desktop b/plugins/magnetgenerator/ktorrent_magnetgenerator.desktop new file mode 100644 index 0000000..7203339 --- /dev/null +++ b/plugins/magnetgenerator/ktorrent_magnetgenerator.desktop @@ -0,0 +1,100 @@ +[Desktop Entry] +Name=Magnet Generator +Name[ast]=Xenerador de magnets +Name[bs]=Generator magneta +Name[ca]=Generador de magnet +Name[ca@valencia]=Generador de magnet +Name[cs]=Generátor Magnet +Name[da]=Magnet-generator +Name[de]=Magnet-Generator +Name[el]=Magnet Generator +Name[en_GB]=Magnet Generator +Name[es]=Generador magnético +Name[et]=Magnetlinkide looja +Name[fi]=Magnet-generoija +Name[fr]=Générateur de liens « Magnet » +Name[ga]=Gineadóir Maighnéad +Name[gl]=Xerador de magnets +Name[hu]=Magnet-generátor +Name[is]=Magnet tilbúningur +Name[it]=Generatore Magnet +Name[kk]=Магнит құрғышы +Name[km]=កម្មវិធី​បង្កើត Generator +Name[ko]=마그넷 링크 생성기 +Name[lt]=Magnetų generatorius +Name[nb]=Magnet-generator +Name[nds]=Magnetmaker +Name[nl]=Magneet-generator +Name[pl]=Generator Magnet +Name[pt]=Gerador de Magnet +Name[pt_BR]=Gerador de magnético +Name[ro]=Generator Magnet +Name[ru]=Генератор magnet-ссылок +Name[si]=චුම්භක ජනකය +Name[sk]=Magnet Generátor +Name[sl]=Ustvarjalnik Magnet-ov +Name[sr]=Генератор магнета +Name[sr@ijekavian]=Генератор магнета +Name[sr@ijekavianlatin]=Generator magneta +Name[sr@latin]=Generator magneta +Name[sv]=Magnetgenerering +Name[tr]=Magnet Üretici +Name[ug]=ماگنىت ھاسىللىغۇچ +Name[uk]=Породжувач Magnet +Name[x-test]=xxMagnet Generatorxx +Name[zh_CN]=磁力链生成器 +Name[zh_TW]=Magnet 產生器 +Comment=Generates magnet links +Comment[ar]=تولّد وصلات ممغنطة +Comment[bs]=Stvara magnetske veze +Comment[ca]=Genera enllaços magnet +Comment[ca@valencia]=Genera enllaços magnet +Comment[cs]=Generuje odkazy magnet +Comment[da]=Genererer magnet-links +Comment[de]=Erzeugt Magnet-Verknüpfungen +Comment[el]=Δημιουργεί συνδέσμους magnet +Comment[en_GB]=Generates magnet links +Comment[es]=Genera enlaces magnéticos +Comment[et]=Magnetlinkide loomine +Comment[fi]=Generoi Magnet-linkkejä +Comment[fr]=Génère des liens « Magnet » +Comment[ga]=Gineann sé seo naisc mhaighnéid +Comment[gl]=Xera ligazóns magnet. +Comment[hu]=Magnet-hivatkozások generálása +Comment[is]=Býr til "magnet" tengla +Comment[it]=Genera collegamenti magnet +Comment[kk]=Магнит-сілтемені құру +Comment[km]=បង្កើត​តំណ magnet +Comment[ko]=마그넷 링크 생성 +Comment[lt]=Generuoja magnetų nuorodas +Comment[nb]=Genererer Magnet-lenker +Comment[nds]=Stellt Magnetlinks op +Comment[nl]=Genereert magneetlinks +Comment[pl]=Tworzy łącza magnet +Comment[pt]=Gera hiperligações Magnet +Comment[pt_BR]=Gera links magnéticos +Comment[ro]=Generează legături Magnet +Comment[ru]=Создаёт magnet-ссылки +Comment[si]=චුම්භක සබැඳි ජනනය කරයි +Comment[sk]=Generovať magnet odkazy +Comment[sl]=Ustvari povezave Magnet +Comment[sr]=Ствара магнетске везе +Comment[sr@ijekavian]=Ствара магнетске везе +Comment[sr@ijekavianlatin]=Stvara magnetske veze +Comment[sr@latin]=Stvara magnetske veze +Comment[sv]=Skapar magnetiska länkar +Comment[tr]=Magnet bağlantıları üretir +Comment[uk]=Створює посилання magnet +Comment[x-test]=xxGenerates magnet linksxx +Comment[zh_CN]=生成磁力链 +Comment[zh_TW]=產生 Magnet 連結 +Type=Service +X-KDE-Library=ktmagnetgeneratorplugin +X-KDE-PluginInfo-Author=Jonas Lundqvist +X-KDE-PluginInfo-Email=jonas@gannon.se +X-KDE-PluginInfo-Name=MagnetGeneratorPlugin +X-KDE-PluginInfo-Version=0.1 +X-KDE-PluginInfo-Website=http://kde.org/applications/internet/ktorrent/ +X-KDE-PluginInfo-License=GPL +X-KDE-PluginInfo-EnabledByDefault=false +Icon=kt-magnet diff --git a/plugins/magnetgenerator/ktorrent_magnetgeneratorui.rc b/plugins/magnetgenerator/ktorrent_magnetgeneratorui.rc new file mode 100644 index 0000000..9e3457c --- /dev/null +++ b/plugins/magnetgenerator/ktorrent_magnetgeneratorui.rc @@ -0,0 +1,7 @@ + + + + + + + diff --git a/plugins/magnetgenerator/magnetgeneratorplugin.cpp b/plugins/magnetgenerator/magnetgeneratorplugin.cpp new file mode 100644 index 0000000..8a507b8 --- /dev/null +++ b/plugins/magnetgenerator/magnetgeneratorplugin.cpp @@ -0,0 +1,126 @@ +/* + SPDX-FileCopyrightText: 2010 Jonas Lundqvist + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "magnetgeneratorplugin.h" + +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "magnetgeneratorpluginsettings.h" +#include "magnetgeneratorprefwidget.h" +#include +#include +#include +#include +#include +#include + +K_PLUGIN_FACTORY_WITH_JSON(ktorrent_magnetgenerator, "ktorrent_magnetgenerator.json", registerPlugin();) + +using namespace bt; +namespace kt +{ +MagnetGeneratorPlugin::MagnetGeneratorPlugin(QObject *parent, const QVariantList &args) + : Plugin(parent) +{ + Q_UNUSED(args); + pref = nullptr; + generate_magnet_action = new QAction(QIcon::fromTheme(QStringLiteral("kt-magnet")), i18n("Copy Magnet URI"), this); + connect(generate_magnet_action, &QAction::triggered, this, &MagnetGeneratorPlugin::generateMagnet); + actionCollection()->addAction(QStringLiteral("generate_magnet"), generate_magnet_action); + setXMLFile(QStringLiteral("ktorrent_magnetgeneratorui.rc")); +} + +MagnetGeneratorPlugin::~MagnetGeneratorPlugin() +{ +} + +void MagnetGeneratorPlugin::load() +{ + pref = new MagnetGeneratorPrefWidget(nullptr); + getGUI()->addPrefPage(pref); + TorrentActivityInterface *ta = getGUI()->getTorrentActivity(); + ta->addViewListener(this); + currentTorrentChanged(ta->getCurrentTorrent()); +} + +bool MagnetGeneratorPlugin::versionCheck(const QString &version) const +{ + return version == QStringLiteral(VERSION); +} + +void MagnetGeneratorPlugin::unload() +{ + getGUI()->removePrefPage(pref); + delete pref; + pref = nullptr; + TorrentActivityInterface *ta = getGUI()->getTorrentActivity(); + ta->removeViewListener(this); +} + +void MagnetGeneratorPlugin::currentTorrentChanged(bt::TorrentInterface *tc) +{ + generate_magnet_action->setEnabled(tc && (!tc->getStats().priv_torrent || !MagnetGeneratorPluginSettings::onlypublic())); +} + +void MagnetGeneratorPlugin::generateMagnet() +{ + bt::TorrentInterface *tor = getGUI()->getTorrentActivity()->getCurrentTorrent(); + if (!tor) + return; + + QUrl dn(tor->getStats().torrent_name); + SHA1Hash ih(tor->getInfoHash()); + + QString uri = QStringLiteral("magnet:?xt=urn:btih:") + ih.toString(); + + if (MagnetGeneratorPluginSettings::dn()) { + uri += QStringLiteral("&dn=") + QString::fromLatin1(QUrl::toPercentEncoding(dn.toString(), QByteArrayLiteral("{}"), nullptr)); + } + + if ((MagnetGeneratorPluginSettings::customtracker() && MagnetGeneratorPluginSettings::tr().length() > 0) + && !MagnetGeneratorPluginSettings::torrenttracker()) { + uri += (QStringLiteral("&tr=")) + + QString::fromLatin1(QUrl::toPercentEncoding(QUrl(MagnetGeneratorPluginSettings::tr()).toString(), QByteArrayLiteral("{}"), nullptr)); + } + + if (MagnetGeneratorPluginSettings::torrenttracker()) { + QList trackers = tor->getTrackersList()->getTrackers(); + if (!trackers.isEmpty()) { + Tracker *trk = (Tracker *)trackers.first(); + + uri += QLatin1String("&tr=") + QString::fromLatin1(QUrl::toPercentEncoding(QUrl(trk->trackerURL()).toString(), QByteArrayLiteral("{}"), nullptr)); + } + } + + addToClipboard(uri); + + if (MagnetGeneratorPluginSettings::popup()) + showPopup(); +} + +void MagnetGeneratorPlugin::addToClipboard(QString uri) +{ + QClipboard *cb = QApplication::clipboard(); + cb->setText(uri, QClipboard::Clipboard); + cb->setText(uri, QClipboard::Selection); +} + +void MagnetGeneratorPlugin::showPopup() +{ + KNotification::event(QStringLiteral("MagnetLinkCopied"), i18n("Magnet link copied to clipboard"), QString(), QStringLiteral("kt-magnet")); +} + +} + +#include "magnetgeneratorplugin.moc" diff --git a/plugins/magnetgenerator/magnetgeneratorplugin.h b/plugins/magnetgenerator/magnetgeneratorplugin.h new file mode 100644 index 0000000..b6c28b5 --- /dev/null +++ b/plugins/magnetgenerator/magnetgeneratorplugin.h @@ -0,0 +1,44 @@ +/* + SPDX-FileCopyrightText: 2010 Jonas Lundqvist + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KT_MAGNETGENERATORPLUGIN_H +#define KT_MAGNETGENERATORPLUGIN_H + +#include +#include + +namespace kt +{ +class MagnetGeneratorPrefWidget; + +class MagnetGeneratorPlugin : public Plugin, public ViewListener +{ + Q_OBJECT +public: + MagnetGeneratorPlugin(QObject *parent, const QVariantList &args); + ~MagnetGeneratorPlugin() override; + + void load() override; + void unload() override; + bool versionCheck(const QString &version) const override; + QString parentPart() const override + { + return QStringLiteral("torrentactivity"); + } + void currentTorrentChanged(bt::TorrentInterface *tc) override; + +private Q_SLOTS: + void generateMagnet(); + +private: + MagnetGeneratorPrefWidget *pref; + QAction *generate_magnet_action; + void addToClipboard(QString uri); + void showPopup(); +}; + +} + +#endif // KT_MAGNETGENERATORPLUGIN_H diff --git a/plugins/magnetgenerator/magnetgeneratorpluginsettings.kcfgc b/plugins/magnetgenerator/magnetgeneratorpluginsettings.kcfgc new file mode 100644 index 0000000..a4cf538 --- /dev/null +++ b/plugins/magnetgenerator/magnetgeneratorpluginsettings.kcfgc @@ -0,0 +1,7 @@ +# Code generation options for kconfig_compiler +File=ktmagnetgeneratorplugin.kcfg +ClassName=MagnetGeneratorPluginSettings +Namespace=kt +Singleton=true +Mutators=true +# will create the necessary code for setting those variables diff --git a/plugins/magnetgenerator/magnetgeneratorprefwidget.cpp b/plugins/magnetgenerator/magnetgeneratorprefwidget.cpp new file mode 100644 index 0000000..d9ecba9 --- /dev/null +++ b/plugins/magnetgenerator/magnetgeneratorprefwidget.cpp @@ -0,0 +1,44 @@ +/* + SPDX-FileCopyrightText: 2010 Jonas Lundqvist + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include + +#include "magnetgeneratorpluginsettings.h" +#include "magnetgeneratorprefwidget.h" + +using namespace bt; + +namespace kt +{ +MagnetGeneratorPrefWidget::MagnetGeneratorPrefWidget(QWidget *parent) + : PrefPageInterface(MagnetGeneratorPluginSettings::self(), i18n("Magnet Generator"), QStringLiteral("kt-magnet"), parent) +{ + setupUi(this); + connect(kcfg_customtracker, &QCheckBox::toggled, this, &MagnetGeneratorPrefWidget::customTrackerToggled); + connect(kcfg_torrenttracker, &QCheckBox::toggled, this, &MagnetGeneratorPrefWidget::torrentTrackerToggled); + kcfg_tr->setEnabled(MagnetGeneratorPluginSettings::customtracker()); +} + +MagnetGeneratorPrefWidget::~MagnetGeneratorPrefWidget() +{ +} + +void MagnetGeneratorPrefWidget::customTrackerToggled(bool on) +{ + if (on) + kcfg_torrenttracker->setCheckState(Qt::Unchecked); + + kcfg_tr->setEnabled(on); +} + +void MagnetGeneratorPrefWidget::torrentTrackerToggled(bool on) +{ + if (on) { + kcfg_customtracker->setCheckState(Qt::Unchecked); + kcfg_tr->setEnabled(!on); + } +} + +} diff --git a/plugins/magnetgenerator/magnetgeneratorprefwidget.h b/plugins/magnetgenerator/magnetgeneratorprefwidget.h new file mode 100644 index 0000000..3a56249 --- /dev/null +++ b/plugins/magnetgenerator/magnetgeneratorprefwidget.h @@ -0,0 +1,28 @@ +/* + SPDX-FileCopyrightText: 2010 Jonas Lundqvist + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef MAGNETGENERATORPREFWIDGET_H +#define MAGNETGENERATORPREFWIDGET_H + +#include "ui_magnetgeneratorprefwidget.h" +#include + +namespace kt +{ +class MagnetGeneratorPrefWidget : public PrefPageInterface, public Ui_MagnetGeneratorPrefWidget +{ + Q_OBJECT +public: + MagnetGeneratorPrefWidget(QWidget *parent = nullptr); + ~MagnetGeneratorPrefWidget() override; + +private Q_SLOTS: + void customTrackerToggled(bool on); + void torrentTrackerToggled(bool on); +}; + +} + +#endif diff --git a/plugins/magnetgenerator/magnetgeneratorprefwidget.ui b/plugins/magnetgenerator/magnetgeneratorprefwidget.ui new file mode 100644 index 0000000..3275867 --- /dev/null +++ b/plugins/magnetgenerator/magnetgeneratorprefwidget.ui @@ -0,0 +1,117 @@ + + + MagnetGeneratorPrefWidget + + + + 0 + 0 + 459 + 317 + + + + + 0 + 0 + + + + + 0 + 0 + + + + Magnet Generator Preferences + + + + + + Magnet Generator + + + + + + + + Grabs a tracker from the torrent file + + + Add a tracker from the torrent file + + + + + + + Define what tracker to add + + + Add a custom tracker URL + + + + + + + The tracker URL that will be a part of the magnet link + + + + + + + Add the torrent name in the URI + + + Add name + + + + + + + Only enable on public torrents + + + Disable the menu item for private torrents + + + + + + + Give feedback in form of a popup + + + Show popup + + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + diff --git a/plugins/mediaplayer/CMakeLists.txt b/plugins/mediaplayer/CMakeLists.txt new file mode 100644 index 0000000..46a1370 --- /dev/null +++ b/plugins/mediaplayer/CMakeLists.txt @@ -0,0 +1,49 @@ +find_package(Taglib REQUIRED) +#find_package(Qt5 ${QT_MIN_VERSION} CONFIG REQUIRED Multimedia MultimediaWidgets) +#include_directories(${Qt5Multimedia_INCLUDE_DIRS}) +include_directories(SYSTEM ${PHONON_INCLUDES}) + +add_library(ktorrent_mediaplayer MODULE) + +set(ktmediaplayerplugin_dbus_SRC) +set(screensaver_xml ${KTORRENT_DBUS_XML_DIR}/org.freedesktop.ScreenSaver.xml) +qt5_add_dbus_interface(ktmediaplayerplugin_dbus_SRC ${screensaver_xml} screensaver_interface) + +set(powermanagementinhibit_xml ${KTORRENT_DBUS_XML_DIR}/org.freedesktop.PowerManagement.Inhibit.xml) +qt5_add_dbus_interface(ktmediaplayerplugin_dbus_SRC ${powermanagementinhibit_xml} powermanagementinhibit_interface) + +target_sources(ktorrent_mediaplayer PRIVATE + ${ktmediaplayerplugin_dbus_SRC} + mediacontroller.cpp + playlist.cpp + playlistwidget.cpp + mediaplayeractivity.cpp + mediaplayerplugin.cpp + mediaview.cpp + mediamodel.cpp + mediafile.cpp + mediafilestream.cpp + mediaplayer.cpp + videowidget.cpp + videochunkbar.cpp +) + +ki18n_wrap_ui(ktorrent_mediaplayer mediacontroller.ui) +kconfig_add_kcfg_files(ktorrent_mediaplayer mediaplayerpluginsettings.kcfgc) + +kcoreaddons_desktop_to_json(ktorrent_mediaplayer ktorrent_mediaplayer.desktop) + +target_link_libraries( + ktorrent_mediaplayer + ktcore + Taglib::Taglib + Phonon::phonon4qt5 + KF5::Torrent + KF5::I18n + KF5::KIOFileWidgets + KF5::XmlGui + KF5::WidgetsAddons +# Qt5::MultimediaWidgets + ) +install(TARGETS ktorrent_mediaplayer DESTINATION ${KTORRENT_PLUGIN_INSTALL_DIR} ) +install(FILES ktorrent_mediaplayerui.rc DESTINATION ${KXMLGUI_INSTALL_DIR}/ktorrent ) diff --git a/plugins/mediaplayer/ktmediaplayerplugin.kcfg b/plugins/mediaplayer/ktmediaplayerplugin.kcfg new file mode 100644 index 0000000..f259bf4 --- /dev/null +++ b/plugins/mediaplayer/ktmediaplayerplugin.kcfg @@ -0,0 +1,16 @@ + + + + + + + true + + + true + + + diff --git a/plugins/mediaplayer/ktorrent_mediaplayer.desktop b/plugins/mediaplayer/ktorrent_mediaplayer.desktop new file mode 100644 index 0000000..275bcb4 --- /dev/null +++ b/plugins/mediaplayer/ktorrent_mediaplayer.desktop @@ -0,0 +1,115 @@ +[Desktop Entry] +Name=Media Player +Name[ar]=مشغّل الوسائط +Name[ast]=Reproductor multimedia +Name[bg]=Медиен плеър +Name[bs]=Izvođač medija +Name[ca]=Reproductor multimèdia +Name[ca@valencia]=Reproductor multimèdia +Name[cs]=Přehrávač médií +Name[da]=Medieafspiller +Name[de]=Mediaplayer +Name[el]=Media Player +Name[en_GB]=Media Player +Name[eo]=Medioludilo +Name[es]=Reproductor multimedia +Name[et]=Meediamängija +Name[fi]=Mediasoitin +Name[fr]=Lecteur multimédia +Name[ga]=Seinnteoir Meán +Name[gl]=Reprodutor +Name[hr]=Svirač multimedije +Name[hu]=Médialejátszó +Name[ia]=Media Player (Reproductor de Media) +Name[is]=Margmiðlunarspilari +Name[it]=Lettore multimediale +Name[ja]=メディアプレーヤー +Name[kk]=Медиаплейер +Name[km]=កម្មវិធី​ចាក់​មេឌៀ +Name[ko]=미디어 재생기 +Name[lt]=Media grotuvas +Name[lv]=Multivides atskaņotājs +Name[mr]=मीडिया प्लेयर +Name[nb]=Mediespiller +Name[nds]=Afspeler +Name[nl]=Mediaspeler +Name[nn]=Mediespelar +Name[pl]=Odtwarzacz multimediów +Name[pt]=Reprodutor Multimédia +Name[pt_BR]=Reprodutor de mídia +Name[ro]=Redare multimedia +Name[ru]=Проигрыватель +Name[si]=මාධ්‍යය ධාවකය +Name[sk]=Prehrávač médií +Name[sl]=Predstavnostni predvajalnik +Name[sq]=Luaj Media +Name[sr]=Медија плејер +Name[sr@ijekavian]=Медија плејер +Name[sr@ijekavianlatin]=Medija plejer +Name[sr@latin]=Medija plejer +Name[sv]=Mediaspelare +Name[tr]=Ortam Oynatıcısı +Name[ug]=ۋاسىتە قويغۇ +Name[uk]=Медіапрогравач +Name[x-test]=xxMedia Playerxx +Name[zh_CN]=媒体播放器 +Name[zh_TW]=媒體播放器 +Comment=Phonon-based media player +Comment[ar]=مشغّل وسائط يعتمد «فنون» +Comment[bg]=Програма за преглед на мултимедия на основата на Phonon +Comment[bs]=Medija plejer zasnovan na Fononu +Comment[ca]=Reproductor de suports basat en el Phonon +Comment[ca@valencia]=Reproductor de suports basat en el Phonon +Comment[cs]=Přehrávač médií založený na Phononu +Comment[da]=Phonon-baseret medieafspiller +Comment[de]=Phonon-basierter Mediaplayer für KTorrent +Comment[el]=Αναπαραγωγέας πολυμέσων με βάση το Phonon +Comment[en_GB]=Phonon-based media player +Comment[es]=Reproductor multimedia basado en Phonon +Comment[et]=Phononil põhinev meediamängija +Comment[fi]=Phonon-pohjainen mediasoitin +Comment[fr]=Lecteur multimédia utilisant Phonon +Comment[ga]=Seinnteoir meán bunaithe ar Phonon +Comment[gl]=Reprodutor de imaxe e son baseado en Phonon. +Comment[hu]=Phonon-alapú médialejátszó +Comment[is]=Margmiðlunarspilari byggður á Phonon +Comment[it]=Lettore multimediale basato su Phonon +Comment[ja]=Phonon をベースにしたメディアプレーヤー +Comment[kk]=Phonon-негізіндегі медиаплейер +Comment[km]=កម្មវិធី​ចាក់​មេឌៀ​ដែល​មាន​មូលដ្ឋាន​លើ Phonon +Comment[ko]=Phonon 기반 미디어 재생기 +Comment[lt]=Phonon pagrindu veikiantis media grotuvas +Comment[lv]=Phonon bāzēts multivides atskaņotājs +Comment[mr]=फोनॉन आधारित मीडिया प्लेयर +Comment[nb]=Phonon-basert mediespiller +Comment[nds]=Op Phonon opbuut Afspeler +Comment[nl]=Phonon-gebaseerde mediaspeler +Comment[nn]=Phonon-basert mediespelar +Comment[pl]=Odtwarzacz multimediów oparty na Phonon +Comment[pt]=Leitor multimédia baseado no Phonon +Comment[pt_BR]=Reprodutor de mídia baseado no Phonon +Comment[ro]=Lector multimedia bazat pe Phonon +Comment[ru]=Медиапроигрыватель на базе Phonon +Comment[si]=Phonon-මූලික මාධ්‍යය ධාවකය +Comment[sk]=Prehrávač multimédií založený na Phonone +Comment[sl]=Predvajalnik predstavnostnih datotek, ki temelji na Phononu +Comment[sr]=Медија плејер заснован на Фонону +Comment[sr@ijekavian]=Медија плејер заснован на Фонону +Comment[sr@ijekavianlatin]=Medija plejer zasnovan na Phononu +Comment[sr@latin]=Medija plejer zasnovan na Phononu +Comment[sv]=Phonon-baserad mediaspelare +Comment[tr]=Phonon temelli çoklu ortam oynatıcısı +Comment[uk]=Заснований на Phonon додаток програвача для KTorrent +Comment[x-test]=xxPhonon-based media playerxx +Comment[zh_CN]=基于 Phonon 的媒体播放器 +Comment[zh_TW]=Phonon 媒體播放器 +Type=Service +X-KDE-Library=ktmediaplayerplugin +X-KDE-PluginInfo-Author=Joris Guisson +X-KDE-PluginInfo-Email=joris.guisson@gmail.com +X-KDE-PluginInfo-Name=MediaPlayerPlugin +X-KDE-PluginInfo-Version=0.1 +X-KDE-PluginInfo-Website=http://kde.org/applications/internet/ktorrent/ +X-KDE-PluginInfo-License=GPL +X-KDE-PluginInfo-EnabledByDefault=false +Icon=applications-multimedia diff --git a/plugins/mediaplayer/ktorrent_mediaplayerui.rc b/plugins/mediaplayer/ktorrent_mediaplayerui.rc new file mode 100644 index 0000000..93c9461 --- /dev/null +++ b/plugins/mediaplayer/ktorrent_mediaplayerui.rc @@ -0,0 +1,12 @@ + + + +Media Player Menu + + + + + + + + diff --git a/plugins/mediaplayer/mediacontroller.cpp b/plugins/mediaplayer/mediacontroller.cpp new file mode 100644 index 0000000..f84c883 --- /dev/null +++ b/plugins/mediaplayer/mediacontroller.cpp @@ -0,0 +1,109 @@ +/* + SPDX-FileCopyrightText: 2010 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "mediacontroller.h" + +#include +#include + +#include + +#include "mediaplayer.h" +#include +#include + +namespace kt +{ +static QString t2q(const TagLib::String &t) +{ + return QString::fromWCharArray((const wchar_t *)t.toCWString(), t.length()); +} + +MediaController::MediaController(kt::MediaPlayer *player, KActionCollection *ac, QWidget *parent) + : QWidget(parent) +{ + setupUi(this); + + info_label->setText(i18n("Ready to play")); + seek_slider->setMediaObject(player->media0bject()); + volume->setAudioOutput(player->output()); + volume->setOrientation(Qt::Horizontal); + + connect(player, &MediaPlayer::stopped, this, &MediaController::stopped); + connect(player, &MediaPlayer::playing, this, &MediaController::playing); + + play->setDefaultAction(ac->action(QStringLiteral("media_play"))); + play->setAutoRaise(true); + pause->setDefaultAction(ac->action(QStringLiteral("media_pause"))); + pause->setAutoRaise(true); + stop->setDefaultAction(ac->action(QStringLiteral("media_stop"))); + stop->setAutoRaise(true); + prev->setDefaultAction(ac->action(QStringLiteral("media_prev"))); + prev->setAutoRaise(true); + next->setDefaultAction(ac->action(QStringLiteral("media_next"))); + next->setAutoRaise(true); + + setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum); +} + +MediaController::~MediaController() +{ +} + +void MediaController::playing(const MediaFileRef &file) +{ + if (file.path().isEmpty()) { + stopped(); + } else { + current_file = file; + metaDataChanged(); + } +} + +void MediaController::stopped() +{ + info_label->setText(i18n("Ready to play")); + current_file = MediaFileRef(QString()); +} + +void MediaController::metaDataChanged() +{ + QString extra_data; + QByteArray encoded = QFile::encodeName(current_file.path()); + TagLib::FileRef ref(encoded.data(), true, TagLib::AudioProperties::Fast); + if (ref.isNull()) { + info_label->setText(i18n("Playing: %1", current_file.name())); + return; + } + + TagLib::Tag *tag = ref.tag(); + if (!tag) { + info_label->setText(i18n("Playing: %1", current_file.name())); + return; + } + + QString artist = t2q(tag->artist()); + QString title = t2q(tag->title()); + QString album = t2q(tag->album()); + + bool has_artist = !artist.isEmpty(); + bool has_title = !title.isEmpty(); + bool has_album = !album.isEmpty(); + + if (has_artist && has_title && has_album) { + extra_data = i18n("%2 - %1 (Album: %3)", title, artist, album); + info_label->setText(extra_data); + } else if (has_title && has_artist) { + extra_data = i18n("%2 - %1", title, artist); + info_label->setText(extra_data); + } else if (has_title) { + extra_data = i18n("%1", title); + info_label->setText(extra_data); + } else { + info_label->setText(i18n("%1", current_file.name())); + } +} + +} diff --git a/plugins/mediaplayer/mediacontroller.h b/plugins/mediaplayer/mediacontroller.h new file mode 100644 index 0000000..c8b131e --- /dev/null +++ b/plugins/mediaplayer/mediacontroller.h @@ -0,0 +1,42 @@ +/* + SPDX-FileCopyrightText: 2010 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KT_MEDIACONTROLLER_H +#define KT_MEDIACONTROLLER_H + +#include +#include +#include +#include + +#include "mediafile.h" +#include "ui_mediacontroller.h" + +namespace kt +{ +class MediaPlayer; + +/** + * Widget containing all the things necessary to control the media playback. + */ +class MediaController : public QWidget, public Ui_MediaController +{ + Q_OBJECT +public: + MediaController(MediaPlayer *player, KActionCollection *ac, QWidget *parent = nullptr); + ~MediaController() override; + +private Q_SLOTS: + void playing(const MediaFileRef &file); + void stopped(); + void metaDataChanged(); + +private: + MediaFileRef current_file; +}; + +} + +#endif // KT_MEDIACONTROLLER_H diff --git a/plugins/mediaplayer/mediacontroller.ui b/plugins/mediaplayer/mediacontroller.ui new file mode 100644 index 0000000..b0ad8b3 --- /dev/null +++ b/plugins/mediaplayer/mediacontroller.ui @@ -0,0 +1,156 @@ + + + MediaController + + + + 0 + 0 + 687 + 80 + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + ... + + + + 32 + 32 + + + + + + + + ... + + + + 32 + 32 + + + + + + + + ... + + + + 32 + 32 + + + + + + + + ... + + + + 32 + 32 + + + + + + + + ... + + + + 32 + 32 + + + + false + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Qt::Horizontal + + + + + + + + + + + TextLabel + + + Qt::AlignCenter + + + + + + + Qt::Horizontal + + + + + + + + Phonon::SeekSlider + QSlider +
phonon/seekslider.h
+
+ + Phonon::VolumeSlider + QSlider +
phonon/volumeslider.h
+
+
+ + +
diff --git a/plugins/mediaplayer/mediafile.cpp b/plugins/mediaplayer/mediafile.cpp new file mode 100644 index 0000000..6722205 --- /dev/null +++ b/plugins/mediaplayer/mediafile.cpp @@ -0,0 +1,227 @@ +/* + SPDX-FileCopyrightText: 2010 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "mediafile.h" + +#include + +#include +#include +#include + +#include "mediafilestream.h" +#include "mediaplayer.h" +#include +#include +#include + +namespace kt +{ +MediaFile::MediaFile(bt::TorrentInterface *tc) + : tc(tc) + , idx(INVALID_INDEX) +{ +} + +MediaFile::MediaFile(bt::TorrentInterface *tc, int idx) + : tc(tc) + , idx(idx) +{ +} + +MediaFile::MediaFile(const kt::MediaFile &mf) + : tc(mf.tc) + , idx(mf.idx) +{ +} + +MediaFile::~MediaFile() +{ +} + +QString MediaFile::name() const +{ + if (tc->getStats().multi_file_torrent) { + if (idx < tc->getNumFiles()) { + QString path = tc->getTorrentFile(idx).getUserModifiedPath(); + QVector parts = path.splitRef(QLatin1Char('/')); + if (parts.count() == 0) + return path; + else + return parts.back().toString(); + } else + return QString(); + } else { + return tc->getDisplayName(); + } +} + +QString MediaFile::path() const +{ + if (tc->getStats().multi_file_torrent) { + if (idx < tc->getNumFiles()) + return tc->getTorrentFile(idx).getPathOnDisk(); + else + return QString(); + } else { + return tc->getStats().output_path; + } +} + +bool MediaFile::fullyAvailable() const +{ + if (tc->getStats().multi_file_torrent) { + return idx < tc->getNumFiles() && std::fabs(tc->getTorrentFile(idx).getDownloadPercentage() - 100.0f) < 0.0001f; + } else { + return tc->getStats().completed; + } +} + +bool MediaFile::previewAvailable() const +{ + if (tc->getStats().multi_file_torrent) { + return idx < tc->getNumFiles() && tc->getTorrentFile(idx).isPreviewAvailable(); + } else { + return tc->readyForPreview(); + } +} + +float MediaFile::downloadPercentage() const +{ + if (tc->getStats().multi_file_torrent) { + if (idx < tc->getNumFiles()) + return tc->getTorrentFile(idx).getDownloadPercentage(); + else + return 0.0f; + } else { + return bt::Percentage(tc->getStats()); + } +} + +bt::Uint64 MediaFile::size() const +{ + if (tc->getStats().multi_file_torrent) { + if (idx < tc->getNumFiles()) + return tc->getTorrentFile(idx).getSize(); + else + return 0; + } else { + return tc->getStats().total_bytes; + } +} + +bt::Uint32 MediaFile::firstChunk() const +{ + if (tc->getStats().multi_file_torrent) { + if (idx < tc->getNumFiles()) + return tc->getTorrentFile(idx).getFirstChunk(); + else + return 0; + } else { + return 0; + } +} + +bt::Uint32 MediaFile::lastChunk() const +{ + if (tc->getStats().multi_file_torrent) { + if (idx < tc->getNumFiles()) + return tc->getTorrentFile(idx).getLastChunk(); + else + return 0; + } else { + return tc->getStats().total_chunks - 1; + } +} + +bt::TorrentFileStream::WPtr MediaFile::stream() +{ + if (!tfs) { + // If some file is already in streaming mode, then try unstreamed mode + tfs = tc->createTorrentFileStream(idx, true, nullptr); + if (!tfs) + tfs = tc->createTorrentFileStream(idx, false, nullptr); + } + + return bt::TorrentFileStream::WPtr(tfs); +} + +bool MediaFile::isVideo() const +{ + if (tc->getStats().multi_file_torrent) { + return tc->getTorrentFile(idx).isVideo(); + } else { + QMimeDatabase db; + return db.mimeTypeForFile(path()).name().startsWith(QStringLiteral("video")); + } +} + +/////////////////////////////////////////////////////// +MediaFileRef::MediaFileRef() +{ +} + +MediaFileRef::MediaFileRef(const QString &p) + : file_path(p) +{ +} + +MediaFileRef::MediaFileRef(MediaFile::Ptr ptr) + : ptr(ptr) +{ + file_path = ptr->path(); +} + +MediaFileRef::MediaFileRef(const kt::MediaFileRef &other) + : ptr(other.ptr) + , file_path(other.file_path) +{ +} + +MediaFileRef::~MediaFileRef() +{ +} + +MediaFileRef &MediaFileRef::operator=(const kt::MediaFileRef &other) +{ + ptr = other.ptr; + file_path = other.file_path; + return *this; +} + +bool MediaFileRef::operator!=(const kt::MediaFileRef &other) const +{ + return file_path != other.path(); +} + +bool MediaFileRef::operator==(const kt::MediaFileRef &other) const +{ + return file_path == other.path(); +} + +Phonon::MediaSource MediaFileRef::createMediaSource(MediaPlayer *player) +{ + MediaFile::Ptr mf = mediaFile(); + if (mf && !mf->fullyAvailable()) { + MediaFileStream *stream = new MediaFileStream(mf->stream()); + QObject::connect(stream, &MediaFileStream::stateChanged, player, &MediaPlayer::streamStateChanged); + + Phonon::MediaSource ms(stream); + ms.setAutoDelete(true); + return ms; + } else + return Phonon::MediaSource(QUrl::fromLocalFile(file_path)); +} + +QString MediaFileRef::name() const +{ + int idx = file_path.lastIndexOf(bt::DirSeparator()); + if (idx != -1) + return file_path.mid(idx + 1); + else + return file_path; +} + +} diff --git a/plugins/mediaplayer/mediafile.h b/plugins/mediaplayer/mediafile.h new file mode 100644 index 0000000..d575730 --- /dev/null +++ b/plugins/mediaplayer/mediafile.h @@ -0,0 +1,158 @@ +/* + SPDX-FileCopyrightText: 2010 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KT_MEDIAFILE_H +#define KT_MEDIAFILE_H + +#include +#include +#include +#include + +#include +#include + +namespace bt +{ +class TorrentInterface; +class TorrentFileStream; +} + +namespace kt +{ +class MediaPlayer; + +const bt::Uint32 INVALID_INDEX = 0xFFFFFFFF; + +/** + Class representing a multimedia file of a torrent +*/ +class MediaFile +{ +public: + /** + Constructor for single file torrents + @param tc The TorrentInterface + */ + MediaFile(bt::TorrentInterface *tc); + + /** + Constructor for multi file torrents + @param tc The TorrentInterface + @param idx The index of the file in the torrent + */ + MediaFile(bt::TorrentInterface *tc, int idx); + + /** + Copy constructor + @param mf The MediaFile to copy + */ + MediaFile(const MediaFile &mf); + ~MediaFile(); + + /// Get the path of the MediaFile + QString path() const; + + /// Get the name of the MediaFile + QString name() const; + + /// Is a preview available + bool previewAvailable() const; + + /// Is it fully available + bool fullyAvailable() const; + + /// Get the download percentage + float downloadPercentage() const; + + /// Get the torrent of this MediaFile + bt::TorrentInterface *torrent() const + { + return tc; + } + + /// Get the size of the MediaFile + bt::Uint64 size() const; + + /// Get the first chunk of the file + bt::Uint32 firstChunk() const; + + /// Get the last chunk of the file + bt::Uint32 lastChunk() const; + + /// Create a TorrentFileStream object for this MediaFile and return a weak pointer to it + bt::TorrentFileStream::WPtr stream(); + + /// Is this a video ? + bool isVideo() const; + + typedef QSharedPointer Ptr; + typedef QWeakPointer WPtr; + +private: + bt::TorrentInterface *tc; + bt::Uint32 idx; + bt::TorrentFileStream::Ptr tfs; +}; + +/** + A MediaFileRef is a reference to a MediaFile + which keeps a weak pointer and the path of the file as backup. + If the weak pointer can no longer resolve to a strong pointer + the actual path can still be used. + + This can also be used for files only (without an actual MediaFile) +*/ +class MediaFileRef +{ +public: + /// Default constructor + MediaFileRef(); + + /// Simple file mode constructor + MediaFileRef(const QString &p); + + /// Strong pointer constructor + MediaFileRef(MediaFile::Ptr ptr); + + /// Copy constructor + MediaFileRef(const MediaFileRef &other); + ~MediaFileRef(); + + /// Get the MediaFile + MediaFile::Ptr mediaFile() + { + return ptr.toStrongRef(); + } + + /// Get the path + QString path() const + { + return file_path; + } + + /// Get the name of the file + QString name() const; + + /// Assignment operator + MediaFileRef &operator=(const MediaFileRef &other); + + /// Comparison operator + bool operator==(const MediaFileRef &other) const; + + /// Negative comparison operator + bool operator!=(const MediaFileRef &other) const; + + /// Create a Phonon::MediaSource for this MediaFileRef + Phonon::MediaSource createMediaSource(MediaPlayer *p); + +private: + MediaFile::WPtr ptr; + QString file_path; +}; + +} + +#endif // KT_MEDIAFILE_H diff --git a/plugins/mediaplayer/mediafilestream.cpp b/plugins/mediaplayer/mediafilestream.cpp new file mode 100644 index 0000000..718675e --- /dev/null +++ b/plugins/mediaplayer/mediafilestream.cpp @@ -0,0 +1,122 @@ +/* + SPDX-FileCopyrightText: 2010 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "mediafilestream.h" +#include +#include +#include + +using namespace bt; + +namespace kt +{ +const Uint32 MIN_AMOUNT_NEEDED = 16 * 1024; + +MediaFileStream::MediaFileStream(bt::TorrentFileStream::WPtr stream, QObject *parent) + : AbstractMediaStream(parent) + , stream(stream) + , waiting_for_data(false) +{ + TorrentFileStream::Ptr s = stream.toStrongRef(); + if (s) { + s->open(QIODevice::ReadOnly); + s->reset(); + setStreamSize(s->size()); + setStreamSeekable(!s->isSequential()); + connect(s.data(), &TorrentFileStream::readyRead, this, &MediaFileStream::dataReady); + } +} + +MediaFileStream::~MediaFileStream() +{ +} + +void MediaFileStream::dataReady() +{ + if (waiting_for_data) { + TorrentFileStream::Ptr s = stream.toStrongRef(); + // Make sure there is enough data buffered for smooth playback + if (s) { + qint64 left = s->size() - s->pos(); + qint64 min_amount_needed = MIN_AMOUNT_NEEDED; + if (left < min_amount_needed) + min_amount_needed = left; + + if (s->bytesAvailable() >= min_amount_needed) { + const QByteArray data = s->read(min_amount_needed); + if (!data.isEmpty()) { + writeData(data); + waiting_for_data = false; + stateChanged(PLAYING); + } + } else { + Out(SYS_MPL | LOG_DEBUG) << "Not enough data available: " << s->bytesAvailable() << " (need " << min_amount_needed << ")" << endl; + stateChanged(BUFFERING); + } + } else + endOfData(); + } +} + +void MediaFileStream::needData() +{ + // Out(SYS_GEN|LOG_DEBUG) << "MediaFileStream::needData" << endl; + TorrentFileStream::Ptr s = stream.toStrongRef(); + if (!s || s->atEnd()) { + endOfData(); + return; + } + + // Make sure there is enough data buffered for smooth playback + qint64 left = s->size() - s->pos(); + qint64 min_amount_needed = MIN_AMOUNT_NEEDED; + if (left < min_amount_needed) + min_amount_needed = left; + + if (s->bytesAvailable() >= min_amount_needed) { + QByteArray data = s->read(min_amount_needed); + if (data.isEmpty()) { + waiting_for_data = true; + } else { + writeData(data); + if (waiting_for_data) { + waiting_for_data = false; + stateChanged(PLAYING); + } + } + } else { + Out(SYS_MPL | LOG_DEBUG) << "Not enough data available: " << s->bytesAvailable() << " (need " << min_amount_needed << ")" << endl; + waiting_for_data = true; + stateChanged(BUFFERING); + + // Send some more data, otherwise phonon seems to get stuck + QByteArray data = s->read(4096); + if (!data.isEmpty()) + writeData(data); + } +} + +void MediaFileStream::reset() +{ + TorrentFileStream::Ptr s = stream.toStrongRef(); + if (s) + s->reset(); +} + +void MediaFileStream::enoughData() +{ + // Out(SYS_GEN|LOG_DEBUG) << "MediaFileStream::enoughData" << endl; + // waiting_for_data = false; +} + +void MediaFileStream::seekStream(qint64 offset) +{ + TorrentFileStream::Ptr s = stream.toStrongRef(); + if (!s) + return; + + s->seek(offset); +} +} diff --git a/plugins/mediaplayer/mediafilestream.h b/plugins/mediaplayer/mediafilestream.h new file mode 100644 index 0000000..571b121 --- /dev/null +++ b/plugins/mediaplayer/mediafilestream.h @@ -0,0 +1,59 @@ +/* + SPDX-FileCopyrightText: 2010 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KT_MEDIAFILESTREAM_H +#define KT_MEDIAFILESTREAM_H + +#include +#include + +namespace bt +{ +class TorrentFileStream; +} + +namespace kt +{ +/** + Class to stream a TorrentFileStream to phonon. + */ +class MediaFileStream : public Phonon::AbstractMediaStream +{ + Q_OBJECT +public: + MediaFileStream(bt::TorrentFileStream::WPtr stream, QObject *parent = nullptr); + ~MediaFileStream() override; + + enum StreamState { + PLAYING, + BUFFERING, + }; + + StreamState state() const + { + return waiting_for_data ? BUFFERING : PLAYING; + } + +protected: + void needData() override; + void reset() override; + void enoughData() override; + void seekStream(qint64 offset) override; + +Q_SIGNALS: + /// Emitted when the stream state changes + void stateChanged(int state); + +private Q_SLOTS: + void dataReady(); + +private: + bt::TorrentFileStream::WPtr stream; + bool waiting_for_data; +}; + +} + +#endif // KT_MEDIAFILESTREAM_H diff --git a/plugins/mediaplayer/mediamodel.cpp b/plugins/mediaplayer/mediamodel.cpp new file mode 100644 index 0000000..8b356b7 --- /dev/null +++ b/plugins/mediaplayer/mediamodel.cpp @@ -0,0 +1,229 @@ +/* + SPDX-FileCopyrightText: 2008 Joris Guisson + SPDX-FileCopyrightText: 2008 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include +#include +#include + +#include + +#include "mediamodel.h" +#include +#include +#include +#include +#include +#include +#include + +using namespace bt; + +namespace kt +{ +MediaModel::MediaModel(CoreInterface *core, QObject *parent) + : QAbstractListModel(parent) + , core(core) +{ + const QueueManager *const qman = core->getQueueManager(); + for (bt::TorrentInterface *tc : *qman) { + onTorrentAdded(tc); + } +} + +MediaModel::~MediaModel() +{ +} + +int MediaModel::rowCount(const QModelIndex &parent) const +{ + if (!parent.isValid()) + return items.count(); + else + return 0; +} + +int MediaModel::columnCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent); + return 1; +} + +QVariant MediaModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + Q_UNUSED(section); + Q_UNUSED(orientation); + Q_UNUSED(role); + return QVariant(); +} + +QVariant MediaModel::data(const QModelIndex &index, int role) const +{ + if (index.column() != 0 || index.row() < 0 || index.row() >= items.count()) + return QVariant(); + + MediaFile::Ptr mf = items.at(index.row()); + switch (role) { + case Qt::ToolTipRole: { + QString preview = mf->previewAvailable() ? i18n("Available") : i18n("Pending"); + return i18n("%1
Preview: %2
Downloaded: %3 %", mf->name(), preview, mf->downloadPercentage()); + } break; + case Qt::DisplayRole: + return mf->name(); + case Qt::DecorationRole: + return QIcon::fromTheme(m_mimeDatabase.mimeTypeForFile(mf->path()).iconName()); + case Qt::UserRole: // user role is for finding out if a torrent is complete + return mf->fullyAvailable(); + case Qt::UserRole + 1: + return QFileInfo(mf->path()).lastModified().toTime_t(); + default: + return QVariant(); + } + + return QVariant(); +} + +bool MediaModel::removeRows(int row, int count, const QModelIndex &parent) +{ + if (parent.isValid()) + return false; + + beginRemoveRows(QModelIndex(), row, row + count - 1); + for (int i = 0; i < count; i++) { + if (row >= 0 && row < items.count()) { + items.removeAt(row); + } + } + endRemoveRows(); + return true; +} + +bool MediaModel::insertRows(int row, int count, const QModelIndex &parent) +{ + if (parent.isValid()) + return false; + + beginInsertRows(QModelIndex(), row, row + count - 1); + endInsertRows(); + return true; +} + +void MediaModel::onTorrentAdded(bt::TorrentInterface *tc) +{ + if (tc->getStats().multi_file_torrent) { + int cnt = 0; + for (Uint32 i = 0; i < tc->getNumFiles(); i++) { + if (tc->getTorrentFile(i).isMultimedia()) { + MediaFile::Ptr p(new MediaFile(tc, i)); + items.append(p); + cnt++; + } + } + + if (cnt) + insertRows(items.count() - 1, cnt, QModelIndex()); + } else if (tc->isMultimedia()) { + MediaFile::Ptr p(new MediaFile(tc)); + items.append(p); + insertRow(items.count() - 1); + } +} + +void MediaModel::onTorrentRemoved(bt::TorrentInterface *tc) +{ + int row = 0; + int start = -1; + int cnt = 0; + for (MediaFile::Ptr mf : qAsConst(items)) { + if (mf->torrent() == tc) { + if (start == -1) { + // start of the range + start = row; + cnt = 1; + } else + cnt++; // Still in the middle of the media files of this torrent + } else if (start != -1) { + // We have found the end + break; + } + + row++; + } + + if (cnt > 0) + removeRows(start, cnt, QModelIndex()); +} + +MediaFileRef MediaModel::fileForIndex(const QModelIndex &idx) const +{ + if (idx.row() < 0 || idx.row() >= items.count()) + return MediaFileRef(QString()); + else + return MediaFileRef(items.at(idx.row())); +} + +QModelIndex MediaModel::indexForPath(const QString &path) const +{ + Uint32 idx = 0; + for (MediaFile::Ptr mf : qAsConst(items)) { + if (mf->path() == path) + return index(idx, 0, QModelIndex()); + idx++; + } + + return QModelIndex(); +} + +MediaFileRef MediaModel::find(const QString &path) +{ + for (MediaFile::Ptr mf : qAsConst(items)) { + if (mf->path() == path) + return MediaFileRef(mf); + } + + return MediaFileRef(path); +} + +Qt::ItemFlags MediaModel::flags(const QModelIndex &index) const +{ + Qt::ItemFlags defaultFlags = QAbstractItemModel::flags(index); + + if (index.isValid()) + return Qt::ItemIsDragEnabled | defaultFlags; + else + return defaultFlags; +} + +QStringList MediaModel::mimeTypes() const +{ + QStringList types; + types << QStringLiteral("text/uri-list"); + return types; +} + +QMimeData *MediaModel::mimeData(const QModelIndexList &indexes) const +{ + QMimeData *data = new QMimeData(); + QList urls; + for (const QModelIndex &idx : indexes) { + if (!idx.isValid() || idx.row() < 0 || idx.row() >= items.count()) + continue; + + MediaFile::Ptr p = items.at(idx.row()); + urls << QUrl::fromLocalFile(p->path()); + } + data->setUrls(urls); + return data; +} + +QModelIndex MediaModel::index(int row, int column, const QModelIndex &parent) const +{ + if (row < 0 || row >= items.count() || column != 0 || parent.isValid()) + return QModelIndex(); + + return createIndex(row, column); +} + +} diff --git a/plugins/mediaplayer/mediamodel.h b/plugins/mediaplayer/mediamodel.h new file mode 100644 index 0000000..e79f465 --- /dev/null +++ b/plugins/mediaplayer/mediamodel.h @@ -0,0 +1,77 @@ +/* + SPDX-FileCopyrightText: 2008 Joris Guisson + SPDX-FileCopyrightText: 2008 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KTMEDIAMODEL_H +#define KTMEDIAMODEL_H + +#include "mediafile.h" +#include +#include +#include + +namespace kt +{ +class CoreInterface; + +/** + Interface class to find MediaFileRef objects in the collection +*/ +class MediaFileCollection +{ +public: + virtual ~MediaFileCollection() + { + } + + /** + Find a MediaFileRef given a path, if the path is not in the collection + a simple file MediaFileRef will be constructed + */ + virtual MediaFileRef find(const QString &path) = 0; +}; + +/** + @author +*/ +class MediaModel : public QAbstractListModel, public MediaFileCollection +{ + Q_OBJECT +public: + MediaModel(CoreInterface *core, QObject *parent); + ~MediaModel() override; + + int rowCount(const QModelIndex &parent) const override; + int columnCount(const QModelIndex &parent) const override; + QVariant headerData(int section, Qt::Orientation orientation, int role) const override; + QVariant data(const QModelIndex &index, int role) const override; + bool removeRows(int row, int count, const QModelIndex &parent) override; + bool insertRows(int row, int count, const QModelIndex &parent) override; + Qt::ItemFlags flags(const QModelIndex &index) const override; + QStringList mimeTypes() const override; + QMimeData *mimeData(const QModelIndexList &indexes) const override; + QModelIndex index(int row, int column = 0, const QModelIndex &parent = QModelIndex()) const override; + + /// Get the file given a model index + MediaFileRef fileForIndex(const QModelIndex &idx) const; + + /// Get the index of a full path + QModelIndex indexForPath(const QString &path) const; + + MediaFileRef find(const QString &path) override; + +public Q_SLOTS: + void onTorrentAdded(bt::TorrentInterface *t); + void onTorrentRemoved(bt::TorrentInterface *t); + +private: + CoreInterface *core; + QList items; + QMimeDatabase m_mimeDatabase; +}; + +} + +#endif diff --git a/plugins/mediaplayer/mediaplayer.cpp b/plugins/mediaplayer/mediaplayer.cpp new file mode 100644 index 0000000..ffd3383 --- /dev/null +++ b/plugins/mediaplayer/mediaplayer.cpp @@ -0,0 +1,217 @@ +/* + SPDX-FileCopyrightText: 2008 Joris Guisson + SPDX-FileCopyrightText: 2008 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include +#include +#include +#include + +#include "mediaplayer.h" +#include +#include + +using namespace bt; + +namespace kt +{ +MediaPlayer::MediaPlayer(QObject *parent) + : QObject(parent) + , buffering(false) + , manually_paused(false) +{ + media = new Phonon::MediaObject(this); + audio = new Phonon::AudioOutput(this); + Phonon::createPath(media, audio); + + connect(media, &Phonon::MediaObject::stateChanged, this, &MediaPlayer::onStateChanged); + connect(media, &Phonon::MediaObject::hasVideoChanged, this, &MediaPlayer::hasVideoChanged); + connect(media, &Phonon::MediaObject::aboutToFinish, this, &MediaPlayer::aboutToFinish); + media->setTickInterval(1000); +} + +MediaPlayer::~MediaPlayer() +{ + stop(); +} + +bool MediaPlayer::paused() const +{ + return media->state() == Phonon::PausedState; +} + +void MediaPlayer::resume() +{ + if (paused() || manually_paused) { + if (buffering) + manually_paused = false; + else + media->play(); + } +} + +void MediaPlayer::play(kt::MediaFileRef file) +{ + buffering = false; + Out(SYS_MPL | LOG_NOTICE) << "MediaPlayer: playing " << file.path() << endl; + Phonon::MediaSource ms = file.createMediaSource(this); + media->setCurrentSource(ms); + + MediaFile::Ptr ptr = file.mediaFile(); + if (ptr && ptr->isVideo()) { + Out(SYS_MPL | LOG_DEBUG) << "Opening video widget !" << endl; + openVideo(); + } + + history.append(file); + playing(file); + current = file; + media->play(); +} + +void MediaPlayer::queue(kt::MediaFileRef file) +{ + Out(SYS_MPL | LOG_NOTICE) << "MediaPlayer: enqueue " << file.path() << endl; + media->enqueue(file.createMediaSource(this)); + history.append(file); + onStateChanged(media->state(), Phonon::StoppedState); +} + +void MediaPlayer::pause() +{ + if (!buffering) { + media->pause(); + } else { + Out(SYS_MPL | LOG_DEBUG) << "MediaPlayer: paused" << endl; + manually_paused = true; + int flags = MEDIA_PLAY | MEDIA_STOP; + if (history.count() > 1) + flags |= MEDIA_PREV; + + enableActions(flags); + } +} + +void MediaPlayer::stop() +{ + media->stop(); + media->clear(); + if (buffering) + buffering = false; + + current = MediaFileRef(); + onStateChanged(media->state(), Phonon::StoppedState); +} + +MediaFileRef MediaPlayer::prev() +{ + if (media->state() == Phonon::PausedState || media->state() == Phonon::PlayingState) { + if (history.count() >= 2) { + history.pop_back(); // remove the currently playing file + MediaFileRef &file = history.back(); + media->setCurrentSource(file.createMediaSource(this)); + media->play(); + Out(SYS_MPL | LOG_NOTICE) << "MediaPlayer: playing previous file " << file.path() << endl; + return file; + } + } else { + if (history.count() > 0) { + MediaFileRef &file = history.back(); + media->setCurrentSource(file.createMediaSource(this)); + media->play(); + Out(SYS_MPL | LOG_NOTICE) << "MediaPlayer: playing previous file " << file.path() << endl; + return file; + } + } + + return QString(); +} + +void MediaPlayer::onStateChanged(Phonon::State cur, Phonon::State) +{ + unsigned int flags = 0; + switch (cur) { + case Phonon::LoadingState: + Out(SYS_MPL | LOG_DEBUG) << "MediaPlayer: loading" << endl; + if (history.count() > 0) + flags |= MEDIA_PREV; + + enableActions(flags); + loading(); + break; + case Phonon::StoppedState: + Out(SYS_MPL | LOG_DEBUG) << "MediaPlayer: stopped" << endl; + flags = MEDIA_PLAY; + if (history.count() > 0) + flags |= MEDIA_PREV; + + enableActions(flags); + stopped(); + break; + case Phonon::PlayingState: + Out(SYS_MPL | LOG_DEBUG) << "MediaPlayer: playing " << getCurrentSource().path() << endl; + flags = MEDIA_PAUSE | MEDIA_STOP; + if (history.count() > 1) + flags |= MEDIA_PREV; + + enableActions(flags); + hasVideoChanged(media->hasVideo()); + playing(getCurrentSource()); + break; + case Phonon::BufferingState: + Out(SYS_MPL | LOG_DEBUG) << "MediaPlayer: buffering" << endl; + break; + case Phonon::PausedState: + if (!buffering) { + Out(SYS_MPL | LOG_DEBUG) << "MediaPlayer: paused" << endl; + flags = MEDIA_PLAY | MEDIA_STOP; + if (history.count() > 1) + flags |= MEDIA_PREV; + + enableActions(flags); + } + break; + case Phonon::ErrorState: + Out(SYS_MPL | LOG_IMPORTANT) << "MediaPlayer: error " << media->errorString() << endl; + flags = MEDIA_PLAY; + if (history.count() > 0) + flags |= MEDIA_PREV; + + enableActions(flags); + break; + } +} + +void MediaPlayer::streamStateChanged(int state) +{ + Out(SYS_MPL | LOG_DEBUG) << "Stream state changed: " << (state == MediaFileStream::BUFFERING ? "BUFFERING" : "PLAYING") << endl; + if (state == MediaFileStream::BUFFERING) { + buffering = true; + media->pause(); + onStateChanged(media->state(), Phonon::PlayingState); + } else if (buffering) { + buffering = false; + if (!manually_paused) + media->play(); + } +} + +MediaFileRef MediaPlayer::getCurrentSource() const +{ + if (history.isEmpty()) + return MediaFileRef(); + else + return MediaFileRef(history.back()); +} + +void MediaPlayer::hasVideoChanged(bool hasVideo) +{ + if (hasVideo) + openVideo(); + else + closeVideo(); +} + +} diff --git a/plugins/mediaplayer/mediaplayer.h b/plugins/mediaplayer/mediaplayer.h new file mode 100644 index 0000000..006dea9 --- /dev/null +++ b/plugins/mediaplayer/mediaplayer.h @@ -0,0 +1,128 @@ +/* + SPDX-FileCopyrightText: 2008 Joris Guisson + SPDX-FileCopyrightText: 2008 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KTAUDIOPLAYER_H +#define KTAUDIOPLAYER_H + +#include "mediafile.h" +#include "mediafilestream.h" +#include +#include +#include + +namespace Phonon +{ +class AudioOutput; +} + +namespace kt +{ +enum ActionFlags { + MEDIA_PLAY = 1, + MEDIA_PAUSE = 2, + MEDIA_STOP = 4, + MEDIA_PREV = 8, + MEDIA_NEXT = 16, +}; + +/** + @author +*/ +class MediaPlayer : public QObject +{ + Q_OBJECT +public: + MediaPlayer(QObject *parent); + ~MediaPlayer() override; + + Phonon::AudioOutput *output() + { + return audio; + } + Phonon::MediaObject *media0bject() + { + return media; + } + + /// Are we paused + bool paused() const; + + /// Resume paused stuff + void resume(); + + /// Play a file + void play(MediaFileRef file); + + /// Queue a file + void queue(MediaFileRef file); + + /// Pause playing + void pause(); + + /// Stop playing + void stop(); + + /// Get the current file we are playing + MediaFileRef getCurrentSource() const; + + /// Play the previous song + MediaFileRef prev(); + + void streamStateChanged(int state); + +private Q_SLOTS: + void onStateChanged(Phonon::State cur, Phonon::State old); + void hasVideoChanged(bool hasVideo); + +Q_SIGNALS: + /** + * Emitted to enable or disable the play buttons. + * @param flags Flags indicating which buttons to enable + */ + void enableActions(unsigned int flags); + + /** + * A video has been detected, create the video player window. + */ + void openVideo(); + + /** + * Emitted when the video widget needs to be closed. + */ + void closeVideo(); + + /** + * Emitted when we have finished playing something + */ + void stopped(); + + /** + * Emitted when the player is about to finish + */ + void aboutToFinish(); + + /** + * Emitted when the player starts playing + */ + void playing(const MediaFileRef &file); + + /** + * Emitted when the video is being loaded + */ + void loading(); + +private: + Phonon::MediaObject *media; + Phonon::AudioOutput *audio; + QList history; + MediaFileRef current; + bool buffering; + bool manually_paused; +}; + +} + +#endif diff --git a/plugins/mediaplayer/mediaplayeractivity.cpp b/plugins/mediaplayer/mediaplayeractivity.cpp new file mode 100644 index 0000000..210f80f --- /dev/null +++ b/plugins/mediaplayer/mediaplayeractivity.cpp @@ -0,0 +1,374 @@ +/* + SPDX-FileCopyrightText: 2009 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "mediaplayeractivity.h" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "mediacontroller.h" +#include "mediamodel.h" +#include "mediaplayer.h" +#include "mediaplayerpluginsettings.h" +#include "mediaview.h" +#include "playlist.h" +#include "playlistwidget.h" +#include "videowidget.h" +#include +#include +#include +#include +#include +#include + +using namespace bt; + +namespace kt +{ +MediaPlayerActivity::MediaPlayerActivity(CoreInterface *core, KActionCollection *ac, QWidget *parent) + : Activity(i18n("Media Player"), QStringLiteral("applications-multimedia"), 90, parent) + , ac(ac) +{ + action_flags = 0; + video = nullptr; + play_action = pause_action = stop_action = prev_action = next_action = nullptr; + fullscreen_mode = false; + + media_model = new MediaModel(core, this); + media_player = new MediaPlayer(this); + + QHBoxLayout *layout = new QHBoxLayout(this); + layout->setMargin(0); + tabs = new QTabWidget(this); + layout->addWidget(tabs); + + QWidget *tab = new QWidget(tabs); + tabs->addTab(tab, QIcon::fromTheme(QStringLiteral("applications-multimedia")), i18n("Media Player")); + QVBoxLayout *vbox = new QVBoxLayout(tab); + + splitter = new QSplitter(Qt::Horizontal, tab); + media_view = new MediaView(media_model, splitter); + play_list = new PlayListWidget(media_model, media_player, tabs); + setupActions(); + controller = new MediaController(media_player, ac, tab); + + splitter->addWidget(media_view); + splitter->addWidget(play_list); + vbox->addWidget(controller); + vbox->addWidget(splitter); + + close_button = new QToolButton(tabs); + tabs->setCornerWidget(close_button, Qt::TopRightCorner); + close_button->setIcon(QIcon::fromTheme(QStringLiteral("tab-close"))); + close_button->setEnabled(false); + connect(close_button, &QToolButton::clicked, this, &MediaPlayerActivity::closeTab); + + // tabs->setTabBarHidden(true); + tabs->setTabBarAutoHide(true); + + connect(core, &CoreInterface::torrentAdded, media_model, &MediaModel::onTorrentAdded); + connect(core, &CoreInterface::torrentRemoved, media_model, &MediaModel::onTorrentRemoved); + connect(media_player, &MediaPlayer::enableActions, this, &MediaPlayerActivity::enableActions); + connect(media_player, &MediaPlayer::openVideo, this, &MediaPlayerActivity::openVideo); + connect(media_player, &MediaPlayer::closeVideo, this, &MediaPlayerActivity::closeVideo); + connect(media_player, &MediaPlayer::aboutToFinish, this, &MediaPlayerActivity::aboutToFinishPlaying); + connect(play_list, &PlayListWidget::fileSelected, this, &MediaPlayerActivity::onSelectionChanged); + connect(media_view, &MediaView::doubleClicked, this, &MediaPlayerActivity::onDoubleClicked); + connect(play_list, &PlayListWidget::randomModeActivated, this, &MediaPlayerActivity::randomPlayActivated); + connect(play_list, qOverload(&PlayListWidget::doubleClicked), this, qOverload(&MediaPlayerActivity::play)); + connect(play_list, &PlayListWidget::enableNext, next_action, &QAction::setEnabled); + connect(tabs, &QTabWidget::currentChanged, this, &MediaPlayerActivity::currentTabChanged); +} + +MediaPlayerActivity::~MediaPlayerActivity() +{ + if (fullscreen_mode) + setVideoFullScreen(false); +} + +void MediaPlayerActivity::setupActions() +{ + play_action = new QAction(QIcon::fromTheme(QStringLiteral("media-playback-start")), i18n("Play"), this); + connect(play_action, &QAction::triggered, this, qOverload<>(&MediaPlayerActivity::play)); + ac->addAction(QStringLiteral("media_play"), play_action); + + pause_action = new QAction(QIcon::fromTheme(QStringLiteral("media-playback-pause")), i18n("Pause"), this); + connect(pause_action, &QAction::triggered, this, &MediaPlayerActivity::pause); + ac->addAction(QStringLiteral("media_pause"), pause_action); + + stop_action = new QAction(QIcon::fromTheme(QStringLiteral("media-playback-stop")), i18n("Stop"), this); + connect(stop_action, &QAction::triggered, this, &MediaPlayerActivity::stop); + ac->addAction(QStringLiteral("media_stop"), stop_action); + + prev_action = new QAction(QIcon::fromTheme(QStringLiteral("media-skip-backward")), i18n("Previous"), this); + connect(prev_action, &QAction::triggered, this, &MediaPlayerActivity::prev); + ac->addAction(QStringLiteral("media_prev"), prev_action); + + next_action = new QAction(QIcon::fromTheme(QStringLiteral("media-skip-forward")), i18n("Next"), this); + connect(next_action, &QAction::triggered, this, &MediaPlayerActivity::next); + ac->addAction(QStringLiteral("media_next"), next_action); + + show_video_action = new KToggleAction(QIcon::fromTheme(QStringLiteral("video-x-generic")), i18n("Show Video"), this); + connect(show_video_action, &QAction::toggled, this, &MediaPlayerActivity::showVideo); + ac->addAction(QStringLiteral("show_video"), show_video_action); + + add_media_action = new QAction(QIcon::fromTheme(QStringLiteral("document-open")), i18n("Add Media"), this); + connect(add_media_action, &QAction::triggered, play_list, &PlayListWidget::addMedia); + ac->addAction(QStringLiteral("add_media"), add_media_action); + + clear_action = new QAction(QIcon::fromTheme(QStringLiteral("edit-clear-list")), i18n("Clear Playlist"), this); + connect(clear_action, &QAction::triggered, play_list, &PlayListWidget::clearPlayList); + ac->addAction(QStringLiteral("clear_play_list"), clear_action); + + QAction *tfs = new QAction(QIcon::fromTheme(QStringLiteral("view-fullscreen")), i18n("Toggle Fullscreen"), this); + tfs->setCheckable(true); + ac->addAction(QStringLiteral("video_fullscreen"), tfs); + ac->setDefaultShortcut(tfs, QKeySequence(Qt::Key_F)); +} + +void MediaPlayerActivity::openVideo() +{ + QString path = media_player->getCurrentSource().path(); + int idx = path.lastIndexOf(bt::DirSeparator()); + if (idx >= 0) + path = path.mid(idx + 1); + + if (path.isEmpty()) + path = i18n("Media Player"); + + if (video) { + int idx = tabs->indexOf(video); + tabs->setTabText(idx, path); + tabs->setCurrentIndex(idx); + } else { + video = new VideoWidget(media_player, ac, nullptr); + connect(video, &VideoWidget::toggleFullScreen, this, &MediaPlayerActivity::setVideoFullScreen); + int idx = tabs->addTab(video, QIcon::fromTheme(QStringLiteral("video-x-generic")), path); + tabs->setTabToolTip(idx, i18n("Movie player")); + tabs->setCurrentIndex(idx); + } + // tabs->setTabBarHidden(false); + + if (!show_video_action->isChecked()) + show_video_action->setChecked(true); +} + +void MediaPlayerActivity::closeVideo() +{ + if (video) { + tabs->removeTab(tabs->indexOf(video)); + if (show_video_action->isChecked()) + show_video_action->setChecked(false); + // tabs->setTabBarHidden(true); + video->deleteLater(); + video = nullptr; + } +} + +void MediaPlayerActivity::showVideo(bool on) +{ + if (on) + openVideo(); + else + closeVideo(); +} + +void MediaPlayerActivity::play() +{ + if (media_player->paused()) { + media_player->resume(); + } else { + curr_item = play_list->play(); + if (curr_item.isValid()) { + bool random = play_list->randomOrder(); + QModelIndex n = play_list->next(curr_item, random); + next_action->setEnabled(n.isValid()); + } + } +} + +void MediaPlayerActivity::play(const MediaFileRef &file) +{ + media_player->play(file); + QModelIndex idx = play_list->indexForFile(file.path()); + if (idx.isValid()) { + curr_item = idx; + bool random = play_list->randomOrder(); + QModelIndex n = play_list->next(curr_item, random); + next_action->setEnabled(n.isValid()); + } +} + +void MediaPlayerActivity::onDoubleClicked(const MediaFileRef &file) +{ + if (bt::Exists(file.path())) { + play(file); + } +} + +void MediaPlayerActivity::pause() +{ + media_player->pause(); +} + +void MediaPlayerActivity::stop() +{ + media_player->stop(); +} + +void MediaPlayerActivity::prev() +{ + media_player->prev(); +} + +void MediaPlayerActivity::next() +{ + bool random = play_list->randomOrder(); + QModelIndex n = play_list->next(curr_item, random); + if (!n.isValid()) + return; + + QString path = play_list->fileForIndex(n); + if (bt::Exists(path)) { + media_player->play(path); + curr_item = n; + n = play_list->next(curr_item, random); + next_action->setEnabled(n.isValid()); + } +} + +void MediaPlayerActivity::enableActions(unsigned int flags) +{ + pause_action->setEnabled(flags & kt::MEDIA_PAUSE); + stop_action->setEnabled(flags & kt::MEDIA_STOP); + play_action->setEnabled(false); + + QModelIndex idx = play_list->selectedItem(); + if (idx.isValid()) { + PlayList *pl = play_list->playList(); + MediaFileRef file = pl->fileForIndex(idx); + if (bt::Exists(file.path())) + play_action->setEnabled((flags & kt::MEDIA_PLAY) || file != media_player->getCurrentSource()); + else + play_action->setEnabled(action_flags & kt::MEDIA_PLAY); + } else + play_action->setEnabled(flags & kt::MEDIA_PLAY); + + prev_action->setEnabled(flags & kt::MEDIA_PREV); + action_flags = flags; +} + +void MediaPlayerActivity::onSelectionChanged(const MediaFileRef &file) +{ + if (bt::Exists(file.path())) + play_action->setEnabled((action_flags & kt::MEDIA_PLAY) || file != media_player->getCurrentSource()); + else if (!file.path().isEmpty()) + play_action->setEnabled(action_flags & kt::MEDIA_PLAY); + else + play_action->setEnabled(false); +} + +void MediaPlayerActivity::randomPlayActivated(bool on) +{ + QModelIndex next = play_list->next(curr_item, on); + next_action->setEnabled(next.isValid()); +} + +void MediaPlayerActivity::aboutToFinishPlaying() +{ + bool random = play_list->randomOrder(); + QModelIndex n = play_list->next(curr_item, random); + if (!n.isValid()) + return; + + QString path = play_list->fileForIndex(n); + if (bt::Exists(path)) { + media_player->queue(path); + curr_item = n; + n = play_list->next(curr_item, random); + next_action->setEnabled(n.isValid()); + } +} + +void MediaPlayerActivity::closeTab() +{ + if (video != tabs->currentWidget()) + return; + + stop(); + closeVideo(); +} + +void MediaPlayerActivity::setVideoFullScreen(bool on) +{ + if (!video) + return; + + if (on && !fullscreen_mode) { + tabs->removeTab(tabs->indexOf(video)); + video->setParent(nullptr); + video->setFullScreen(true); + video->show(); + fullscreen_mode = true; + } else if (!on && fullscreen_mode) { + video->hide(); + video->setFullScreen(false); + + QString path = media_player->getCurrentSource().path(); + int idx = path.lastIndexOf(bt::DirSeparator()); + if (idx >= 0) + path = path.mid(idx + 1); + + if (path.isEmpty()) + path = i18n("Media Player"); + + idx = tabs->addTab(video, QIcon::fromTheme(QStringLiteral("video-x-generic")), path); + tabs->setTabToolTip(idx, i18n("Movie player")); + tabs->setCurrentIndex(idx); + fullscreen_mode = false; + } +} + +void MediaPlayerActivity::saveState(KSharedConfigPtr cfg) +{ + KConfigGroup g = cfg->group("MediaPlayerActivity"); + g.writeEntry("splitter_state", splitter->saveState()); + play_list->saveState(cfg); + play_list->playList()->save(kt::DataDir() + QLatin1String("playlist")); + + media_view->saveState(cfg); +} + +void MediaPlayerActivity::loadState(KSharedConfigPtr cfg) +{ + KConfigGroup g = cfg->group("MediaPlayerActivity"); + QByteArray d = g.readEntry("splitter_state", QByteArray()); + if (!d.isEmpty()) + splitter->restoreState(d); + + play_list->loadState(cfg); + if (bt::Exists(kt::DataDir() + QLatin1String("playlist"))) + play_list->playList()->load(kt::DataDir() + QLatin1String("playlist")); + + QModelIndex next = play_list->next(curr_item, play_list->randomOrder()); + next_action->setEnabled(next.isValid()); + + media_view->loadState(cfg); +} + +void MediaPlayerActivity::currentTabChanged(int idx) +{ + close_button->setEnabled(idx != 0); +} + +} diff --git a/plugins/mediaplayer/mediaplayeractivity.h b/plugins/mediaplayer/mediaplayeractivity.h new file mode 100644 index 0000000..8bdfc17 --- /dev/null +++ b/plugins/mediaplayer/mediaplayeractivity.h @@ -0,0 +1,90 @@ +/* + SPDX-FileCopyrightText: 2009 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KT_MEDIAPLAYERACTIVITY_H +#define KT_MEDIAPLAYERACTIVITY_H + +#include "mediafile.h" +#include +#include + +class QToolButton; +class QSplitter; +class QTabWidget; +class QAction; +class KActionCollection; + +namespace kt +{ +class MediaView; +class MediaPlayer; +class MediaModel; +class CoreInterface; +class VideoWidget; +class PlayListWidget; +class MediaController; + +/** + * Activity for the media player plugin. + */ +class MediaPlayerActivity : public Activity +{ + Q_OBJECT +public: + MediaPlayerActivity(CoreInterface *core, KActionCollection *ac, QWidget *parent); + ~MediaPlayerActivity() override; + + void setupActions(); + void saveState(KSharedConfigPtr cfg); + void loadState(KSharedConfigPtr cfg); + +public Q_SLOTS: + void play(); + void play(const MediaFileRef &file); + void pause(); + void stop(); + void prev(); + void next(); + void enableActions(unsigned int flags); + void onSelectionChanged(const MediaFileRef &file); + void openVideo(); + void closeVideo(); + void setVideoFullScreen(bool on); + void onDoubleClicked(const MediaFileRef &file); + void randomPlayActivated(bool on); + void aboutToFinishPlaying(); + void showVideo(bool on); + void closeTab(); + void currentTabChanged(int idx); + +private: + QSplitter *splitter; + MediaModel *media_model; + MediaPlayer *media_player; + MediaView *media_view; + MediaController *controller; + QTabWidget *tabs; + int action_flags; + VideoWidget *video; + bool fullscreen_mode; + QModelIndex curr_item; + PlayListWidget *play_list; + QToolButton *close_button; + + QAction *play_action; + QAction *pause_action; + QAction *stop_action; + QAction *prev_action; + QAction *next_action; + QAction *show_video_action; + QAction *clear_action; + QAction *add_media_action; + QAction *status_action; + KActionCollection *ac; +}; + +} + +#endif // KT_MEDIAPLAYERACTIVITY_H diff --git a/plugins/mediaplayer/mediaplayerplugin.cpp b/plugins/mediaplayer/mediaplayerplugin.cpp new file mode 100644 index 0000000..2aa5a0d --- /dev/null +++ b/plugins/mediaplayer/mediaplayerplugin.cpp @@ -0,0 +1,61 @@ +/* + SPDX-FileCopyrightText: 2008 Joris Guisson + SPDX-FileCopyrightText: 2008 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "mediaplayerplugin.h" + +#include +#include + +#include "mediaplayeractivity.h" +#include +#include +#include + +K_PLUGIN_FACTORY_WITH_JSON(ktorrent_mediaplayer, "ktorrent_mediaplayer.json", registerPlugin();) + +using namespace bt; + +namespace kt +{ +MediaPlayerPlugin::MediaPlayerPlugin(QObject *parent, const QVariantList &args) + : Plugin(parent) +{ + Q_UNUSED(args); +} + +MediaPlayerPlugin::~MediaPlayerPlugin() +{ +} + +void MediaPlayerPlugin::load() +{ + LogSystemManager::instance().registerSystem(i18n("Media Player"), SYS_MPL); + CoreInterface *core = getCore(); + act = new MediaPlayerActivity(core, actionCollection(), nullptr); + getGUI()->addActivity(act); + setXMLFile(QStringLiteral("ktorrent_mediaplayerui.rc")); + act->enableActions(0); + act->loadState(KSharedConfig::openConfig()); +} + +void MediaPlayerPlugin::unload() +{ + LogSystemManager::instance().unregisterSystem(i18n("Media Player")); + act->saveState(KSharedConfig::openConfig()); + act->setVideoFullScreen(false); + getGUI()->removeActivity(act); + delete act; + act = nullptr; +} + +bool MediaPlayerPlugin::versionCheck(const QString &version) const +{ + return version == QStringLiteral(VERSION); +} + +} + +#include "mediaplayerplugin.moc" diff --git a/plugins/mediaplayer/mediaplayerplugin.h b/plugins/mediaplayer/mediaplayerplugin.h new file mode 100644 index 0000000..9e57115 --- /dev/null +++ b/plugins/mediaplayer/mediaplayerplugin.h @@ -0,0 +1,38 @@ +/* + SPDX-FileCopyrightText: 2008 Joris Guisson + SPDX-FileCopyrightText: 2008 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KTMEDIAPLAYERPLUGIN_H +#define KTMEDIAPLAYERPLUGIN_H + +#include +#include +#include + +namespace kt +{ +class MediaPlayerActivity; + +/** + @author +*/ +class MediaPlayerPlugin : public Plugin +{ + Q_OBJECT +public: + MediaPlayerPlugin(QObject *parent, const QVariantList &args); + ~MediaPlayerPlugin() override; + + void load() override; + void unload() override; + bool versionCheck(const QString &version) const override; + +private: + MediaPlayerActivity *act; +}; + +} + +#endif diff --git a/plugins/mediaplayer/mediaplayerpluginsettings.kcfgc b/plugins/mediaplayer/mediaplayerpluginsettings.kcfgc new file mode 100644 index 0000000..befb9d3 --- /dev/null +++ b/plugins/mediaplayer/mediaplayerpluginsettings.kcfgc @@ -0,0 +1,7 @@ +# Code generation options for kconfig_compiler +File=ktmediaplayerplugin.kcfg +ClassName=MediaPlayerPluginSettings +Namespace=kt +Singleton=true +Mutators=true +# will create the necessary code for setting those variables diff --git a/plugins/mediaplayer/mediaview.cpp b/plugins/mediaplayer/mediaview.cpp new file mode 100644 index 0000000..3edb767 --- /dev/null +++ b/plugins/mediaplayer/mediaview.cpp @@ -0,0 +1,145 @@ +/* + SPDX-FileCopyrightText: 2008 Joris Guisson + SPDX-FileCopyrightText: 2008 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "mediamodel.h" +#include "mediaplayer.h" +#include "mediaplayerpluginsettings.h" +#include "mediaview.h" +#include + +using namespace bt; + +namespace kt +{ +MediaViewFilter::MediaViewFilter(QObject *parent) + : QSortFilterProxyModel(parent) + , show_incomplete(false) +{ +} + +MediaViewFilter::~MediaViewFilter() +{ +} + +void MediaViewFilter::setShowIncomplete(bool on) +{ + show_incomplete = on; + invalidateFilter(); +} + +bool MediaViewFilter::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const +{ + if (show_incomplete) + return QSortFilterProxyModel::filterAcceptsRow(source_row, source_parent); + + MediaModel *model = (MediaModel *)sourceModel(); + MediaFileRef ref = model->fileForIndex(model->index(source_row)); + MediaFile::Ptr file = ref.mediaFile(); + if (file->fullyAvailable()) + return QSortFilterProxyModel::filterAcceptsRow(source_row, source_parent); + else + return false; +} + +void MediaViewFilter::refresh() +{ + invalidateFilter(); +} + +MediaView::MediaView(MediaModel *model, QWidget *parent) + : QWidget(parent) + , model(model) +{ + filter = new MediaViewFilter(this); + filter->setSourceModel(model); + filter->setFilterRole(Qt::DisplayRole); + filter->setFilterCaseSensitivity(Qt::CaseInsensitive); + filter->setSortRole(Qt::UserRole + 1); + filter->sort(0, Qt::DescendingOrder); + + QVBoxLayout *layout = new QVBoxLayout(this); + layout->setSpacing(0); + layout->setMargin(0); + + QHBoxLayout *hbox = new QHBoxLayout(); + hbox->setSpacing(0); + hbox->setMargin(0); + + tool_bar = new KToolBar(this); + hbox->addWidget(tool_bar); + + show_incomplete = tool_bar->addAction(QIcon::fromTheme(QStringLiteral("task-ongoing")), i18n("Show incomplete files")); + show_incomplete->setCheckable(true); + show_incomplete->setChecked(false); + connect(show_incomplete, &QAction::toggled, this, &MediaView::showIncompleteChanged); + + refresh = tool_bar->addAction(QIcon::fromTheme(QStringLiteral("view-refresh")), i18n("Refresh"), filter, &MediaViewFilter::refresh); + refresh->setToolTip(i18n("Refresh media files")); + + search_box = new QLineEdit(this); + search_box->setClearButtonEnabled(true); + search_box->setPlaceholderText(i18n("Search media files")); + connect(search_box, &QLineEdit::textChanged, filter, &MediaViewFilter::setFilterFixedString); + hbox->addWidget(search_box); + + layout->addLayout(hbox); + + media_tree = new QListView(this); + media_tree->setModel(filter); + media_tree->setDragEnabled(true); + media_tree->setSelectionMode(QAbstractItemView::ContiguousSelection); + media_tree->setAlternatingRowColors(true); + layout->addWidget(media_tree); + + connect(media_tree, &QListView::doubleClicked, this, &MediaView::onDoubleClicked); +} + +MediaView::~MediaView() +{ +} + +void MediaView::onDoubleClicked(const QModelIndex &index) +{ + if (!index.isValid()) + return; + + QModelIndex idx = filter->mapToSource(index); + if (!idx.isValid()) + return; + + doubleClicked(model->fileForIndex(idx)); +} + +void MediaView::showIncompleteChanged(bool on) +{ + filter->setShowIncomplete(on); +} + +void MediaView::loadState(KSharedConfig::Ptr cfg) +{ + KConfigGroup g = cfg->group("MediaView"); + show_incomplete->setChecked(g.readEntry("show_incomplete", false)); + search_box->setText(g.readEntry("search_text", QString())); +} + +void MediaView::saveState(KSharedConfig::Ptr cfg) +{ + KConfigGroup g = cfg->group("MediaView"); + g.writeEntry("show_incomplete", show_incomplete->isChecked()); + g.writeEntry("search_text", search_box->text()); +} + +} diff --git a/plugins/mediaplayer/mediaview.h b/plugins/mediaplayer/mediaview.h new file mode 100644 index 0000000..bfd1007 --- /dev/null +++ b/plugins/mediaplayer/mediaview.h @@ -0,0 +1,78 @@ +/* + SPDX-FileCopyrightText: 2008 Joris Guisson + SPDX-FileCopyrightText: 2008 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KTMEDIAVIEW_H +#define KTMEDIAVIEW_H + +#include +#include +#include +#include + +#include "mediafile.h" + +class QLineEdit; +class KToolBar; + +namespace kt +{ +class MediaModel; + +/** + * QSortFilterProxyModel to filter out incomplete files + */ +class MediaViewFilter : public QSortFilterProxyModel +{ + Q_OBJECT +public: + MediaViewFilter(QObject *parent = nullptr); + ~MediaViewFilter() override; + + bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override; + + /// Enable or disable showing of incomplete files + void setShowIncomplete(bool on); + +public Q_SLOTS: + void refresh(); + +private: + bool show_incomplete; +}; + +/** + @author +*/ +class MediaView : public QWidget +{ + Q_OBJECT +public: + MediaView(MediaModel *model, QWidget *parent); + ~MediaView() override; + + void saveState(KSharedConfig::Ptr cfg); + void loadState(KSharedConfig::Ptr cfg); + +Q_SIGNALS: + void doubleClicked(const MediaFileRef &mf); + +private Q_SLOTS: + void onDoubleClicked(const QModelIndex &index); + void showIncompleteChanged(bool on); + +private: + MediaModel *model; + QListView *media_tree; + QLineEdit *search_box; + MediaViewFilter *filter; + KToolBar *tool_bar; + QAction *show_incomplete; + QAction *refresh; +}; + +} + +#endif diff --git a/plugins/mediaplayer/playlist.cpp b/plugins/mediaplayer/playlist.cpp new file mode 100644 index 0000000..b9dcb16 --- /dev/null +++ b/plugins/mediaplayer/playlist.cpp @@ -0,0 +1,319 @@ +/* + SPDX-FileCopyrightText: 2009 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "playlist.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "mediaplayer.h" +#include +#include + +using namespace bt; + +namespace kt +{ +PlayList::PlayList(kt::MediaFileCollection *collection, kt::MediaPlayer *player, QObject *parent) + : QAbstractItemModel(parent) + , collection(collection) + , player(player) +{ + connect(player, &MediaPlayer::playing, this, &PlayList::onPlaying); +} + +PlayList::~PlayList() +{ +} + +void PlayList::addFile(const MediaFileRef &file) +{ + QByteArray name = QFile::encodeName(file.path()); + TagLib::FileRef *ref = new TagLib::FileRef(name.data(), true, TagLib::AudioProperties::Fast); + files.append(qMakePair(file, ref)); + insertRow(files.count() - 1); +} + +void PlayList::removeFile(const MediaFileRef &file) +{ + int row = 0; + bool found = false; + for (const PlayListItem &item : qAsConst(files)) { + if (item.first == file) { + found = true; + break; + } + row++; + } + + if (found) + removeRow(row); +} + +MediaFileRef PlayList::fileForIndex(const QModelIndex &index) const +{ + if (!index.isValid() || index.row() < 0 || index.row() >= files.count()) + return MediaFileRef(QString()); + else + return files.at(index.row()).first; +} + +void PlayList::clear() +{ + beginResetModel(); + files.clear(); + endResetModel(); +} + +QVariant PlayList::headerData(int section, Qt::Orientation orientation, int role) const +{ + if (orientation == Qt::Vertical || role != Qt::DisplayRole) + return QVariant(); + + switch (section) { + case 0: + return i18n("Title"); + case 1: + return i18n("Artist"); + case 2: + return i18n("Album"); + case 3: + return i18n("Length"); + case 4: + return i18n("Year"); + default: + return QVariant(); + } +} + +QVariant PlayList::data(const QModelIndex &index, int role) const +{ + if (!index.isValid() || (role != Qt::DisplayRole && role != Qt::UserRole && role != Qt::DecorationRole)) + return QVariant(); + + const PlayListItem &item = files.at(index.row()); + const MediaFileRef &file = item.first; + const TagLib::FileRef *ref = item.second; + if (!ref) { + QByteArray name = QFile::encodeName(file.path()); + files[index.row()].second = new TagLib::FileRef(name.data(), true, TagLib::AudioProperties::Fast); + ref = item.second; + } + + if (!ref || ref->isNull()) { + if (index.column() == 0) + return QFileInfo(file.path()).fileName(); + else + return QVariant(); + } + + TagLib::Tag *tag = ref->tag(); + if (!tag) { + if (index.column() == 0) + return QFileInfo(file.path()).fileName(); + else + return QVariant(); + } + + if (role == Qt::DisplayRole || role == Qt::UserRole) { + switch (index.column()) { + case 0: { + QString title = TStringToQString(tag->title()); + return title.isEmpty() ? QFileInfo(file.path()).fileName() : title; + } + case 1: + return TStringToQString(tag->artist()); + case 2: + return TStringToQString(tag->album()); + case 3: + if (role == Qt::UserRole) { + return ref->audioProperties()->length(); + } else { + QTime t(0, 0); + t = t.addSecs(ref->audioProperties()->length()); + return t.toString(QStringLiteral("m:ss")); + } + case 4: + return tag->year() == 0 ? QVariant() : tag->year(); + default: + return QVariant(); + } + } + + if (role == Qt::DecorationRole && index.column() == 0) { + if (file == player->getCurrentSource()) + return QIcon::fromTheme(QStringLiteral("arrow-right")); + } + + return QVariant(); +} + +int PlayList::columnCount(const QModelIndex &parent) const +{ + if (parent.isValid()) + return 0; + else + return 5; +} + +int PlayList::rowCount(const QModelIndex &parent) const +{ + return parent.isValid() ? 0 : files.count(); +} + +QModelIndex PlayList::parent(const QModelIndex &child) const +{ + Q_UNUSED(child); + return QModelIndex(); +} + +QModelIndex PlayList::index(int row, int column, const QModelIndex &parent) const +{ + if (parent.isValid()) + return QModelIndex(); + else + return createIndex(row, column); +} + +Qt::DropActions PlayList::supportedDropActions() const +{ + return Qt::CopyAction | Qt::MoveAction; +} + +Qt::ItemFlags PlayList::flags(const QModelIndex &index) const +{ + Qt::ItemFlags defaultFlags = QAbstractItemModel::flags(index); + + if (index.isValid()) + return Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled | defaultFlags; + else + return Qt::ItemIsDropEnabled | defaultFlags; +} + +QStringList PlayList::mimeTypes() const +{ + QStringList types; + types << QStringLiteral("text/uri-list"); + return types; +} + +QMimeData *PlayList::mimeData(const QModelIndexList &indexes) const +{ + dragged_rows.clear(); + QMimeData *data = new QMimeData(); + QList urls; + for (const QModelIndex &index : indexes) { + if (index.isValid() && index.column() == 0) { + urls << QUrl::fromLocalFile(files.at(index.row()).first.path()); + dragged_rows.append(index.row()); + } + } + + data->setUrls(urls); + return data; +} + +bool PlayList::dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) +{ + if (action == Qt::IgnoreAction) + return true; + + const QList urls = data->urls(); + if (urls.count() == 0 || column > 0) + return false; + + if (row == -1) + row = parent.row(); + + if (row == -1) + row = rowCount(QModelIndex()); + + // Remove dragged rows if there are any + std::sort(dragged_rows.begin(), dragged_rows.end()); + int nr = 0; + for (int r : qAsConst(dragged_rows)) { + r -= nr; + removeRow(r); + nr++; + } + + row -= nr; + + for (const QUrl &url : urls) { + PlayListItem item = qMakePair(collection->find(url.toLocalFile()), (TagLib::FileRef *)nullptr); + files.insert(row, item); + } + insertRows(row, urls.count(), QModelIndex()); + dragged_rows.clear(); + itemsDropped(); + return true; +} + +bool PlayList::insertRows(int row, int count, const QModelIndex &parent) +{ + Q_UNUSED(parent); + beginInsertRows(QModelIndex(), row, row + count - 1); + endInsertRows(); + return true; +} + +bool PlayList::removeRows(int row, int count, const QModelIndex &parent) +{ + Q_UNUSED(parent); + beginRemoveRows(QModelIndex(), row, row + count - 1); + for (int i = 0; i < count; i++) + files.removeAt(i + row); + endRemoveRows(); + return true; +} + +void PlayList::save(const QString &file) +{ + QFile fptr(file); + if (!fptr.open(QIODevice::WriteOnly)) { + Out(SYS_GEN | LOG_NOTICE) << "Failed to open file " << file << endl; + return; + } + + QTextStream out(&fptr); + for (const PlayListItem &f : qAsConst(files)) + out << f.first.path() << Qt::endl; +} + +void PlayList::load(const QString &file) +{ + QFile fptr(file); + if (!fptr.open(QIODevice::ReadOnly)) { + Out(SYS_GEN | LOG_NOTICE) << "Failed to open file " << file << endl; + return; + } + + beginResetModel(); + QTextStream in(&fptr); + while (!in.atEnd()) { + QString file = in.readLine(); + TagLib::FileRef *ref = new TagLib::FileRef(QFile::encodeName(file).data(), true, TagLib::AudioProperties::Fast); + files.append(qMakePair(collection->find(file), ref)); + } + endResetModel(); +} + +void PlayList::onPlaying(const kt::MediaFileRef &file) +{ + Q_UNUSED(file); + dataChanged(index(0, 0), index(files.count() - 1, 0)); +} + +} diff --git a/plugins/mediaplayer/playlist.h b/plugins/mediaplayer/playlist.h new file mode 100644 index 0000000..2ac28dd --- /dev/null +++ b/plugins/mediaplayer/playlist.h @@ -0,0 +1,65 @@ +/* + SPDX-FileCopyrightText: 2009 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef PLAYLIST_H +#define PLAYLIST_H + +#include +#include + +#include "mediafile.h" +#include "mediamodel.h" +#include +#include + +namespace kt +{ +/** + * PlayList containing a list of files to play. + */ +class PlayList : public QAbstractItemModel +{ + Q_OBJECT +public: + PlayList(MediaFileCollection *collection, MediaPlayer *player, QObject *parent); + ~PlayList() override; + + void addFile(const MediaFileRef &file); + void removeFile(const MediaFileRef &file); + MediaFileRef fileForIndex(const QModelIndex &index) const; + void save(const QString &file); + void load(const QString &file); + void clear(); + + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + QVariant headerData(int section, Qt::Orientation orientation, int role) const override; + int columnCount(const QModelIndex &parent = QModelIndex()) const override; + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QModelIndex parent(const QModelIndex &child) const override; + QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override; + Qt::DropActions supportedDropActions() const override; + Qt::ItemFlags flags(const QModelIndex &index) const override; + QStringList mimeTypes() const override; + QMimeData *mimeData(const QModelIndexList &indexes) const override; + bool dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) override; + bool removeRows(int row, int count, const QModelIndex &parent) override; + bool insertRows(int row, int count, const QModelIndex &parent) override; + +private Q_SLOTS: + void onPlaying(const MediaFileRef &file); + +Q_SIGNALS: + void itemsDropped(); + +private: + typedef QPair PlayListItem; + mutable QList files; + mutable QList dragged_rows; + MediaFileCollection *collection; + MediaPlayer *player; +}; +} + +#endif // PLAYLIST_H diff --git a/plugins/mediaplayer/playlistwidget.cpp b/plugins/mediaplayer/playlistwidget.cpp new file mode 100644 index 0000000..ad21ac3 --- /dev/null +++ b/plugins/mediaplayer/playlistwidget.cpp @@ -0,0 +1,246 @@ +/* + SPDX-FileCopyrightText: 2009 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "playlistwidget.h" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "mediaplayer.h" +#include "mediaplayerpluginsettings.h" +#include "playlist.h" + +namespace kt +{ +PlayListWidget::PlayListWidget(kt::MediaFileCollection *collection, kt::MediaPlayer *player, QWidget *parent) + : QWidget(parent) + , player(player) + , menu(nullptr) + , collection(collection) +{ + QVBoxLayout *layout = new QVBoxLayout(this); + layout->setMargin(0); + layout->setSpacing(0); + + QAction *remove_action = new QAction(QIcon::fromTheme(QStringLiteral("list-remove")), i18n("Remove"), this); + connect(remove_action, &QAction::triggered, this, &PlayListWidget::removeFiles); + QAction *add_action = new QAction(QIcon::fromTheme(QStringLiteral("document-open")), i18n("Add Media"), this); + connect(add_action, &QAction::triggered, this, &PlayListWidget::addMedia); + QAction *clear_action = new QAction(QIcon::fromTheme(QStringLiteral("edit-clear-list")), i18n("Clear Playlist"), this); + connect(clear_action, &QAction::triggered, this, &PlayListWidget::clearPlayList); + + tool_bar = new QToolBar(this); + tool_bar->addAction(add_action); + tool_bar->addAction(remove_action); + tool_bar->addAction(clear_action); + random_mode = new QCheckBox(i18n("Random play order"), tool_bar); + connect(random_mode, &QCheckBox::toggled, this, &PlayListWidget::randomModeActivated); + tool_bar->addWidget(random_mode); + layout->addWidget(tool_bar); + + play_list = new PlayList(collection, player, this); + connect(play_list, &PlayList::itemsDropped, this, &PlayListWidget::onItemsDropped); + proxy_model = new QSortFilterProxyModel(this); + proxy_model->setSourceModel(play_list); + proxy_model->setSortRole(Qt::UserRole); + + view = new QTreeView(this); + view->setModel(proxy_model); + view->setDragEnabled(true); + view->setDropIndicatorShown(true); + view->setAcceptDrops(true); + view->setAlternatingRowColors(true); + view->setRootIsDecorated(false); + view->setContextMenuPolicy(Qt::CustomContextMenu); + view->setSelectionMode(QAbstractItemView::ExtendedSelection); + view->setSortingEnabled(true); + layout->addWidget(view); + connect(view, &QTreeView::customContextMenuRequested, this, &PlayListWidget::showContextMenu); + + connect(view->selectionModel(), &QItemSelectionModel::selectionChanged, this, &PlayListWidget::onSelectionChanged); + connect(view, &QTreeView::doubleClicked, this, qOverload(&PlayListWidget::doubleClicked)); + + menu = new QMenu(this); + menu->addAction(remove_action); + menu->addSeparator(); + menu->addAction(add_action); + menu->addAction(clear_action); +} + +PlayListWidget::~PlayListWidget() +{ +} + +QModelIndex PlayListWidget::selectedItem() const +{ + QModelIndexList rows = view->selectionModel()->selectedRows(); + if (rows.count() > 0) + return proxy_model->mapToSource(rows.front()); + else + return QModelIndex(); +} + +void PlayListWidget::onSelectionChanged(const QItemSelection &s, const QItemSelection &d) +{ + Q_UNUSED(d); + QModelIndexList idx = s.indexes(); + if (idx.count() > 0) + fileSelected(fileForIndex(idx.front())); + else + fileSelected(MediaFileRef()); +} + +QModelIndex PlayListWidget::play() +{ + QModelIndex pidx = view->currentIndex(); + QModelIndex idx = proxy_model->mapToSource(pidx); + MediaFileRef file = play_list->fileForIndex(idx); + if (!file.path().isEmpty()) { + player->play(file); + } + return pidx; +} + +void PlayListWidget::doubleClicked(const QModelIndex &index) +{ + MediaFileRef file = play_list->fileForIndex(proxy_model->mapToSource(index)); + if (!file.path().isEmpty()) + doubleClicked(file); +} + +void PlayListWidget::saveState(KSharedConfigPtr cfg) +{ + KConfigGroup g = cfg->group("PlayListWidget"); + QHeaderView *v = view->header(); + g.writeEntry("play_list_state", v->saveState()); + g.writeEntry("random_mode", random_mode->isChecked()); +} + +void PlayListWidget::loadState(KSharedConfigPtr cfg) +{ + KConfigGroup g = cfg->group("PlayListWidget"); + QByteArray d = g.readEntry("play_list_state", QByteArray()); + if (!d.isEmpty()) + view->header()->restoreState(d); + + view->header()->setSortIndicatorShown(true); + random_mode->setChecked(g.readEntry("random_mode", false)); +} + +void PlayListWidget::showContextMenu(QPoint pos) +{ + menu->popup(view->viewport()->mapToGlobal(pos)); +} + +void PlayListWidget::clearPlayList() +{ + play_list->clear(); + enableNext(false); + fileSelected(MediaFileRef()); +} + +void PlayListWidget::addMedia() +{ + QString recentDirClass; + const QString startURL = KFileWidget::getStartUrl(QUrl(QStringLiteral("kfiledialog:///add_media")), recentDirClass).toLocalFile(); + const QStringList files = QFileDialog::getOpenFileNames(this, QString(), startURL); + + if (files.isEmpty()) + return; + + if (!recentDirClass.isEmpty()) + KRecentDirs::add(recentDirClass, QFileInfo(files.first()).absolutePath()); + + for (const QString &file : files) + play_list->addFile(collection->find(file)); + + enableNext(play_list->rowCount() > 0); +} + +void PlayListWidget::removeFiles() +{ + QList files; + const QModelIndexList indexes = view->selectionModel()->selectedRows(); + for (const QModelIndex &idx : indexes) + files.append(play_list->fileForIndex(idx)); + + for (const MediaFileRef &f : qAsConst(files)) + play_list->removeFile(f); + + enableNext(play_list->rowCount() > 0); +} + +void PlayListWidget::onItemsDropped() +{ + enableNext(play_list->rowCount() > 0); +} + +QModelIndex PlayListWidget::next(const QModelIndex &idx, bool random) const +{ + if (play_list->rowCount() == 0) + return QModelIndex(); + + if (!idx.isValid()) { + if (!random) { + return proxy_model->index(0, 0, QModelIndex()); + } else { + return randomNext(QModelIndex()); + } + } else if (!random) { + return next(idx); + } else { + return randomNext(idx); + } +} + +QModelIndex PlayListWidget::next(const QModelIndex &idx) const +{ + if (idx.isValid()) + return idx.sibling(idx.row() + 1, 0); // take a look at the next sibling + else + return play_list->index(0, 0); +} + +QModelIndex PlayListWidget::randomNext(const QModelIndex &idx) const +{ + int count = play_list->rowCount(); + if (count <= 1) + return QModelIndex(); + + int r = QRandomGenerator::global()->bounded(count); + while (r == idx.row()) + r = QRandomGenerator::global()->bounded(count); + + return proxy_model->index(r, 0, QModelIndex()); +} + +QString PlayListWidget::fileForIndex(const QModelIndex &index) const +{ + return play_list->fileForIndex(proxy_model->mapToSource(index)).path(); +} + +QModelIndex PlayListWidget::indexForFile(const QString &file) const +{ + int count = proxy_model->rowCount(); + for (int i = 0; i < count; i++) { + QModelIndex idx = proxy_model->index(i, 0); + if (fileForIndex(idx) == file) + return idx; + } + + return QModelIndex(); +} + +} diff --git a/plugins/mediaplayer/playlistwidget.h b/plugins/mediaplayer/playlistwidget.h new file mode 100644 index 0000000..fb1b041 --- /dev/null +++ b/plugins/mediaplayer/playlistwidget.h @@ -0,0 +1,97 @@ +/* + SPDX-FileCopyrightText: 2009 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef PLAYLISTWIDGET_H +#define PLAYLISTWIDGET_H + +#include +#include +#include +#include +#include +#include + +#include + +#include "mediafile.h" + +class QSortFilterProxyModel; + +namespace kt +{ +class PlayList; +class MediaPlayer; +class MediaFileCollection; + +class PlayListWidget : public QWidget +{ + Q_OBJECT +public: + PlayListWidget(MediaFileCollection *collection, MediaPlayer *player, QWidget *parent); + ~PlayListWidget() override; + + /// Get the play list + PlayList *playList() + { + return play_list; + } + + /// Get the current selected item + QModelIndex selectedItem() const; + + void saveState(KSharedConfigPtr cfg); + void loadState(KSharedConfigPtr cfg); + + /// Get the next item to play, if idx is invalid return the first playable item + QModelIndex next(const QModelIndex &idx, bool random) const; + + /// Get the file of a given index + QString fileForIndex(const QModelIndex &index) const; + + /// Get the index of a file + QModelIndex indexForFile(const QString &file) const; + + /// Is random mode activated ? + bool randomOrder() const + { + return random_mode->isChecked(); + } + +public Q_SLOTS: + QModelIndex play(); + void addMedia(); + void clearPlayList(); + +private Q_SLOTS: + void onSelectionChanged(const QItemSelection &s, const QItemSelection &d); + void doubleClicked(const QModelIndex &index); + void showContextMenu(QPoint pos); + void removeFiles(); + void onItemsDropped(); + +Q_SIGNALS: + void fileSelected(const MediaFileRef &file); + void doubleClicked(const MediaFileRef &file); + void randomModeActivated(bool random); + void enableNext(bool on); + +private: + QModelIndex next(const QModelIndex &idx) const; + QModelIndex randomNext(const QModelIndex &idx) const; + +private: + MediaPlayer *player; + PlayList *play_list; + QToolBar *tool_bar; + QTreeView *view; + QCheckBox *random_mode; + + QMenu *menu; + QSortFilterProxyModel *proxy_model; + MediaFileCollection *collection; +}; +} + +#endif // PLAYLISTWIDGET_H diff --git a/plugins/mediaplayer/videochunkbar.cpp b/plugins/mediaplayer/videochunkbar.cpp new file mode 100644 index 0000000..853f8e3 --- /dev/null +++ b/plugins/mediaplayer/videochunkbar.cpp @@ -0,0 +1,109 @@ +/* + SPDX-FileCopyrightText: 2010 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "videochunkbar.h" + +#include +#include +#include +#include +#include + +namespace kt +{ +VideoChunkBar::VideoChunkBar(const kt::MediaFileRef &mf, QWidget *parent) + : ChunkBar(parent) + , mfile(mf) + , current_chunk(0) +{ + setMediaFile(mf); +} + +VideoChunkBar::~VideoChunkBar() +{ +} + +void VideoChunkBar::setMediaFile(const kt::MediaFileRef &mf) +{ + mfile = mf; + MediaFile::Ptr file = mfile.mediaFile(); + if (file && !file->fullyAvailable()) { + bt::TorrentFileStream::Ptr stream = file->stream().toStrongRef(); + if (stream) + connect(stream.data(), &bt::TorrentFileStream::readyRead, this, &VideoChunkBar::updateChunkBar); + + updateBitSet(); + updateChunkBar(); + } +} + +void VideoChunkBar::updateBitSet() +{ + MediaFile::Ptr file = mfile.mediaFile(); + if (file) { + bt::TorrentFileStream::Ptr stream = file->stream().toStrongRef(); + if (stream) + bitset = stream->chunksBitSet(); + else + bitset.clear(); + } else + bitset.clear(); +} + +void VideoChunkBar::updateChunkBar() +{ + updateBitSet(); + updateBar(true); + setVisible(!bitset.allOn()); +} + +void VideoChunkBar::timeElapsed(qint64 time) +{ + Q_UNUSED(time); + MediaFile::Ptr file = mfile.mediaFile(); + if (!file) + return; + + bt::TorrentFileStream::Ptr stream = file->stream().toStrongRef(); + if (!stream) + return; + + if (current_chunk != stream->currentChunk() || stream->chunksBitSet() != bitset) + updateChunkBar(); +} + +void VideoChunkBar::drawBarContents(QPainter *p) +{ + ChunkBar::drawBarContents(p); + + MediaFile::Ptr file = mfile.mediaFile(); + if (!file) + return; + + bt::TorrentFileStream::Ptr stream = file->stream().toStrongRef(); + if (!stream) + return; + + current_chunk = stream->currentChunk(); + qreal f = (qreal)current_chunk / bitset.getNumBits(); + int x = (int)(f * contentsRect().width()); + + QStyleOptionSlider option; + option.orientation = Qt::Horizontal; + option.minimum = 0; + option.maximum = bitset.getNumBits(); + option.tickPosition = QSlider::NoTicks; + // option.sliderValue = current_chunk; + option.sliderPosition = current_chunk; + option.rect = QRect(x - 5, 0, 11, contentsRect().height()); + + QApplication::style()->drawControl(QStyle::CE_ScrollBarSlider, &option, p, this); +} + +const bt::BitSet &VideoChunkBar::getBitSet() const +{ + return bitset; +} +} diff --git a/plugins/mediaplayer/videochunkbar.h b/plugins/mediaplayer/videochunkbar.h new file mode 100644 index 0000000..6c0556e --- /dev/null +++ b/plugins/mediaplayer/videochunkbar.h @@ -0,0 +1,48 @@ +/* + SPDX-FileCopyrightText: 2010 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KT_VIDEOCHUNKBAR_H +#define KT_VIDEOCHUNKBAR_H + +#include "mediafile.h" +#include + +namespace kt +{ +/** + ChunkBar for a video during streaming mode + */ +class VideoChunkBar : public ChunkBar +{ + Q_OBJECT +public: + VideoChunkBar(const MediaFileRef &mfile, QWidget *parent); + ~VideoChunkBar() override; + + /// Set the media file + void setMediaFile(const MediaFileRef &mf); + + /// Get the bitset + const bt::BitSet &getBitSet() const override; + + /// Time has elapsed during playing, update the bar if necessary + void timeElapsed(qint64 time); + +private Q_SLOTS: + void updateChunkBar(); + void updateBitSet(); + +private: + void drawBarContents(QPainter *p) override; + +private: + MediaFileRef mfile; + bt::BitSet bitset; + bt::Uint32 current_chunk; +}; + +} + +#endif // KT_VIDEOCHUNKBAR_H diff --git a/plugins/mediaplayer/videowidget.cpp b/plugins/mediaplayer/videowidget.cpp new file mode 100644 index 0000000..335d4f2 --- /dev/null +++ b/plugins/mediaplayer/videowidget.cpp @@ -0,0 +1,261 @@ +/* + SPDX-FileCopyrightText: 2008 Joris Guisson + SPDX-FileCopyrightText: 2008 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "videowidget.h" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "mediaplayer.h" +#include "powermanagementinhibit_interface.h" +#include "screensaver_interface.h" +#include "videochunkbar.h" +#include +#include + +using namespace bt; + +namespace kt +{ +VideoWidget::VideoWidget(MediaPlayer *player, KActionCollection *ac, QWidget *parent) + : QWidget(parent) + , player(player) + , chunk_bar(nullptr) + , fullscreen(false) + , screensaver_cookie(0) + , powermanagement_cookie(0) +{ + QVBoxLayout *vlayout = new QVBoxLayout(this); + vlayout->setMargin(0); + vlayout->setSpacing(0); + + video = new Phonon::VideoWidget(this); + Phonon::createPath(player->media0bject(), video); + video->installEventFilter(this); + + chunk_bar = new VideoChunkBar(player->getCurrentSource(), this); + chunk_bar->setVisible(player->media0bject()->currentSource().type() == Phonon::MediaSource::Stream); + + QHBoxLayout *hlayout = new QHBoxLayout(nullptr); + + play_action = new QAction(QIcon::fromTheme(QStringLiteral("media-playback-start")), i18n("Play"), this); + connect(play_action, &QAction::triggered, this, &VideoWidget::play); + + stop_action = new QAction(QIcon::fromTheme(QStringLiteral("media-playback-stop")), i18n("Stop"), this); + connect(stop_action, &QAction::triggered, this, &VideoWidget::stop); + + tb = new KToolBar(this); + tb->setToolButtonStyle(Qt::ToolButtonIconOnly); + tb->addAction(play_action); + tb->addAction(ac->action(QStringLiteral("media_pause"))); + tb->addAction(stop_action); + QAction *tfs = ac->action(QStringLiteral("video_fullscreen")); + connect(tfs, &QAction::toggled, this, &VideoWidget::toggleFullScreen); + tb->addAction(tfs); + + slider = new Phonon::SeekSlider(this); + slider->setMediaObject(player->media0bject()); + slider->setMaximumHeight(tb->iconSize().height()); + + volume = new Phonon::VolumeSlider(this); + volume->setAudioOutput(player->output()); + volume->setMaximumHeight(tb->iconSize().height()); + volume->setMaximumWidth(5 * tb->iconSize().width()); + + time_label = new QLabel(this); + time_label->setText(formatTime(player->media0bject()->currentTime(), player->media0bject()->totalTime())); + time_label->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum); + + hlayout->addWidget(tb); + hlayout->addWidget(slider); + hlayout->addWidget(volume); + hlayout->addWidget(time_label); + + chunk_bar->setFixedHeight(hlayout->sizeHint().height() * 0.75); + + vlayout->addWidget(chunk_bar); + vlayout->addWidget(video); + vlayout->addLayout(hlayout); + + connect(player->media0bject(), &Phonon::MediaObject::tick, this, &VideoWidget::timerTick); + connect(player, &MediaPlayer::playing, this, &VideoWidget::playing); + connect(player, &MediaPlayer::enableActions, this, &VideoWidget::enableActions); + + inhibitScreenSaver(true); +} + +VideoWidget::~VideoWidget() +{ + inhibitScreenSaver(false); +} + +void VideoWidget::play() +{ + player->media0bject()->play(); +} + +void VideoWidget::stop() +{ + Phonon::MediaObject *mo = player->media0bject(); + if (mo->state() == Phonon::PausedState) { + mo->seek(0); + mo->stop(); + } else { + mo->stop(); + } +} + +void VideoWidget::setControlsVisible(bool on) +{ + slider->setVisible(on); + volume->setVisible(on); + tb->setVisible(on); + chunk_bar->setVisible(player->media0bject()->currentSource().type() == Phonon::MediaSource::Stream && on); + time_label->setVisible(on); +} + +bool VideoWidget::eventFilter(QObject *dst, QEvent *event) +{ + Q_UNUSED(dst); + if (fullscreen && event->type() == QEvent::MouseMove) + mouseMoveEvent((QMouseEvent *)event); + + return true; +} + +void VideoWidget::mouseMoveEvent(QMouseEvent *event) +{ + if (!fullscreen) + return; + + bool streaming = player->media0bject()->currentSource().type() == Phonon::MediaSource::Stream; + if (slider->isVisible()) { + int bh = height() - slider->height(); + int th = streaming ? chunk_bar->height() : 0; + if (event->y() < bh - 10 && event->y() > th + 10) // use a 10 pixel safety buffer to avoid fibrilation + setControlsVisible(false); + } else { + int bh = height() - slider->height(); + int th = streaming ? chunk_bar->height() : 0; + if (event->y() >= bh || event->y() <= th) + setControlsVisible(true); + } +} + +void VideoWidget::setFullScreen(bool on) +{ + if (on) { + setWindowState(windowState() | Qt::WindowFullScreen); + setControlsVisible(false); + } else { + setWindowState(windowState() & ~Qt::WindowFullScreen); + setControlsVisible(true); + } + fullscreen = on; + setMouseTracking(fullscreen); +} + +void VideoWidget::inhibitScreenSaver(bool on) +{ + org::freedesktop::ScreenSaver screensaver(QStringLiteral("org.freedesktop.ScreenSaver"), QStringLiteral("/ScreenSaver"), QDBusConnection::sessionBus()); + org::freedesktop::PowerManagement::Inhibit powerManagement(QStringLiteral("org.freedesktop.PowerManagement.Inhibit"), + QStringLiteral("/org/freedesktop/PowerManagement/Inhibit"), + QDBusConnection::sessionBus()); + if (on) { + QString msg = i18n("KTorrent is playing a video."); + auto pendingReply = screensaver.Inhibit(QStringLiteral("ktorrent"), msg); + auto pendingCallWatcher = new QDBusPendingCallWatcher(pendingReply, this); + connect(pendingCallWatcher, &QDBusPendingCallWatcher::finished, this, [=](QDBusPendingCallWatcher *callWatcher) { + QDBusPendingReply reply = *callWatcher; + if (reply.isValid()) { + screensaver_cookie = reply.value(); + Out(SYS_MPL | LOG_NOTICE) << "Screensaver inhibited (cookie " << screensaver_cookie << ")" << endl; + } else + Out(SYS_GEN | LOG_IMPORTANT) << "Failed to suppress screensaver" << endl; + }); + + auto pendingReply2 = powerManagement.Inhibit(QStringLiteral("ktorrent"), msg); + auto pendingCallWatcher2 = new QDBusPendingCallWatcher(pendingReply2, this); + connect(pendingCallWatcher2, &QDBusPendingCallWatcher::finished, this, [=](QDBusPendingCallWatcher *callWatcher) { + QDBusPendingReply reply = *callWatcher; + if (reply.isValid()) { + screensaver_cookie = reply.value(); + Out(SYS_MPL | LOG_NOTICE) << "PowerManagement inhibited (cookie " << powermanagement_cookie << ")" << endl; + } else + Out(SYS_GEN | LOG_IMPORTANT) << "Failed to suppress sleeping" << endl; + }); + } else { + auto pendingReply = screensaver.UnInhibit(screensaver_cookie); + auto pendingCallWatcher = new QDBusPendingCallWatcher(pendingReply, this); + connect(pendingCallWatcher, &QDBusPendingCallWatcher::finished, this, [=](QDBusPendingCallWatcher *callWatcher) { + QDBusPendingReply reply = *callWatcher; + if (reply.isValid()) { + screensaver_cookie = 0; + Out(SYS_MPL | LOG_NOTICE) << "Screensaver uninhibited" << endl; + } else + Out(SYS_MPL | LOG_IMPORTANT) << "Failed uninhibit screensaver" << endl; + }); + + auto pendingReply2 = powerManagement.UnInhibit(powermanagement_cookie); + auto pendingCallWatcher2 = new QDBusPendingCallWatcher(pendingReply2, this); + connect(pendingCallWatcher2, &QDBusPendingCallWatcher::finished, this, [=](QDBusPendingCallWatcher *callWatcher) { + QDBusPendingReply reply = *callWatcher; + if (reply.isValid()) { + powermanagement_cookie = 0; + Out(SYS_MPL | LOG_NOTICE) << "Power management uninhibited" << endl; + } else + Out(SYS_MPL | LOG_IMPORTANT) << "Failed uninhibit power management" << endl; + }); + } +} + +void VideoWidget::timerTick(qint64 time) +{ + time_label->setText(formatTime(time, player->media0bject()->totalTime())); + if (chunk_bar->isVisible()) + chunk_bar->timeElapsed(time); +} + +QString VideoWidget::formatTime(qint64 cur, qint64 total) +{ + QTime ct(cur / (60 * 60 * 1000), (cur / (60 * 1000)) % 60, (cur / 1000) % 60, cur % 1000); + QTime tt(total / (60 * 60 * 1000), (total / (60 * 1000)) % 60, (total / 1000) % 60, total % 1000); + return QStringLiteral(" %1 / %2 ").arg(ct.toString(QStringLiteral("hh:mm:ss")), tt.toString(QStringLiteral("hh:mm:ss"))); +} + +void VideoWidget::playing(const MediaFileRef &mfile) +{ + bool stream = player->media0bject()->currentSource().type() == Phonon::MediaSource::Stream; + if (fullscreen && stream) + chunk_bar->setVisible(slider->isVisible()); + else + chunk_bar->setVisible(stream); + + chunk_bar->setMediaFile(mfile); +} + +void VideoWidget::enableActions(unsigned int flags) +{ + play_action->setEnabled(flags & kt::MEDIA_PLAY); + stop_action->setEnabled(flags & kt::MEDIA_STOP); +} + +} diff --git a/plugins/mediaplayer/videowidget.h b/plugins/mediaplayer/videowidget.h new file mode 100644 index 0000000..640f850 --- /dev/null +++ b/plugins/mediaplayer/videowidget.h @@ -0,0 +1,81 @@ +/* + SPDX-FileCopyrightText: 2008 Joris Guisson + SPDX-FileCopyrightText: 2008 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KTVIDEOWIDGET_H +#define KTVIDEOWIDGET_H + +#include +#include +#include +#include +#include +#include + +class QAction; +class QLabel; +class KToolBar; +class KActionCollection; + +namespace kt +{ +class VideoChunkBar; +class MediaPlayer; +class MediaFileRef; + +/** + * Widget to display a video + * @author Joris Guisson + */ +class VideoWidget : public QWidget +{ + Q_OBJECT +public: + VideoWidget(MediaPlayer *player, KActionCollection *ac, QWidget *parent); + ~VideoWidget() override; + + /** + * Make the widget full screen or not. + * @param on + */ + void setFullScreen(bool on); + +protected: + void mouseMoveEvent(QMouseEvent *event) override; + bool eventFilter(QObject *dst, QEvent *event) override; + +private Q_SLOTS: + void play(); + void stop(); + void setControlsVisible(bool on); + void timerTick(qint64 time); + void playing(const MediaFileRef &mfile); + void enableActions(unsigned int flags); + +Q_SIGNALS: + void toggleFullScreen(bool on); + +private: + void inhibitScreenSaver(bool on); + QString formatTime(qint64 cur, qint64 total); + +private: + Phonon::VideoWidget *video; + MediaPlayer *player; + Phonon::SeekSlider *slider; + KToolBar *tb; + QAction *play_action; + QAction *stop_action; + QLabel *time_label; + Phonon::VolumeSlider *volume; + VideoChunkBar *chunk_bar; + bool fullscreen; + quint32 screensaver_cookie; + quint32 powermanagement_cookie; +}; + +} + +#endif diff --git a/plugins/scanfolder/CMakeLists.txt b/plugins/scanfolder/CMakeLists.txt new file mode 100644 index 0000000..c51b5f2 --- /dev/null +++ b/plugins/scanfolder/CMakeLists.txt @@ -0,0 +1,25 @@ +add_library(ktorrent_scanfolder MODULE) + +target_sources(ktorrent_scanfolder PRIVATE + scanthread.cpp + torrentloadqueue.cpp + scanfolder.cpp + scanfolderplugin.cpp + scanfolderprefpage.cpp) + +ki18n_wrap_ui(ktorrent_scanfolder scanfolderprefpage.ui) +kconfig_add_kcfg_files(ktorrent_scanfolder scanfolderpluginsettings.kcfgc) + +kcoreaddons_desktop_to_json(ktorrent_scanfolder ktorrent_scanfolder.desktop) + +target_link_libraries( + ktorrent_scanfolder + ktcore + KF5::Torrent + KF5::ConfigCore + KF5::CoreAddons + KF5::I18n + KF5::KIOCore +) +install(TARGETS ktorrent_scanfolder DESTINATION ${KTORRENT_PLUGIN_INSTALL_DIR} ) + diff --git a/plugins/scanfolder/ktorrent_scanfolder.desktop b/plugins/scanfolder/ktorrent_scanfolder.desktop new file mode 100644 index 0000000..38a2708 --- /dev/null +++ b/plugins/scanfolder/ktorrent_scanfolder.desktop @@ -0,0 +1,109 @@ +[Desktop Entry] +Name=Scan Folder +Name[bg]=Претърсване на директория +Name[bs]=Pretraga direktorija +Name[ca]=Explora les carpetes +Name[ca@valencia]=Explora les carpetes +Name[cs]=Procházení složek +Name[da]=Scan mappe +Name[de]=Ordner durchsuchen +Name[el]=Σάρωση φακέλου +Name[en_GB]=Scan Folder +Name[es]=Exploración de carpeta +Name[et]=Kataloogi uurimine +Name[fi]=Etsi kansiosta +Name[fr]=Analyser un dossier +Name[ga]=Scan Fillteán +Name[gl]=Exame de cartafoles +Name[hr]=Pretraži direktorij +Name[hu]=Mappafigyelő +Name[ia]=Scande Dossier +Name[is]=Leita í möppu +Name[it]=Scansione cartella +Name[ja]=スキャンフォルダ +Name[kk]=Қапшықты зерттеу +Name[km]=វិភាគ​ថត +Name[ko]=폴더 탐색 +Name[lt]=Skenuoti aplanką +Name[lv]=Mapes skenēšana +Name[nb]=Mappeundersøker +Name[nds]=Ornern dörkieken +Name[nl]=Scanner-map +Name[nn]=Mappesøk +Name[pl]=Przeszukiwanie katalogów +Name[pt]=Sondagem de Pastas +Name[pt_BR]=Examinar pasta +Name[ro]=Citește dosar +Name[ru]=Проверка папок +Name[si]=බහලුම පරිලෝකනය +Name[sk]=Prehľadať priečinok +Name[sl]=Preišči mapo +Name[sq]=Skano Dosjen +Name[sr]=Претрага фасцикле +Name[sr@ijekavian]=Претрага фасцикле +Name[sr@ijekavianlatin]=Pretraga fascikle +Name[sr@latin]=Pretraga fascikle +Name[sv]=Katalogsökning +Name[tr]=Dizin Tara +Name[ug]=قىسقۇچ تەكشۈر +Name[uk]=Сканувати теку +Name[x-test]=xxScan Folderxx +Name[zh_CN]=扫描文件夹 +Name[zh_TW]=掃描資料夾 +Comment=Scan folders for torrent files and load them +Comment[bg]=Претърсване на директории за торент-файлове за добавяне +Comment[bs]=Pretraži direktorije i učitaj torent datoteke iz njih +Comment[ca]=Explora les carpetes cercant fitxers torrent i els carrega +Comment[ca@valencia]=Explora les carpetes cercant fitxers torrent i els carrega +Comment[cs]=Vyhledávat ve složkách a načítat torrenty +Comment[da]=Scan mapper efter torrent-filer og indlæs dem +Comment[de]=Ordner nach Torrent-Dateien durchsuchen und diese laden +Comment[el]=Σάρωση φακέλων για αρχεία torrent και φόρτωσή τους +Comment[en_GB]=Scan folders for torrent files and load them +Comment[es]=Explora carpetas en búsqueda de archivos torrent y los carga +Comment[et]=Kataloogidest torrent-failide otsimine ja nende laadimine +Comment[fi]=Etsii kansiosta torrent-tiedostoja ja lataa ne +Comment[fr]=Analyse des dossiers pour rechercher des fichiers de torrents et pour les charger +Comment[ga]=Lorg comhaid torrent i bhfillteáin agus luchtaigh iad +Comment[gl]=Examina cartafoles na busca de ficheiros torrent e os carga. +Comment[hu]=Mappák figyelése és a torrentfájlok betöltése +Comment[is]=Leita að torrent straumaskrám í möppum og hleður þeim inn +Comment[it]=Analizza le cartelle alla ricerca di torrent e procede al loro caricamento +Comment[ja]=フォルダをスキャンして見つかった torrent ファイルをロードします +Comment[kk]=Қапшқтарды торрент файлдар бар ма деп зерттеп оларды жүктеу +Comment[km]=វិភាគ​ថត​រក​ឯកសារ torrent ហើយ​ផ្ទុក​ពួកវា +Comment[ko]=폴더의 토렌트 파일 검사 및 항목 불러오기 +Comment[lt]=Skenuoti aplankus dėl torrent failų ir juos įkelti +Comment[lv]=Skenē mapēs torrent failus un ielādē tos +Comment[nb]=Søker etter BitTorrent-filer i en mappe og åpne dem +Comment[nds]=Ornern op Torrent-Dateien dörkieken un de denn laden +Comment[nl]=Scan mappen op torrentbestanden en laad deze +Comment[nn]=Søk etter torrentfiler i mapper og last dei inn +Comment[pl]=Przeszukiwanie katalogów w poszukiwaniu torrentów i ich wczytywanie +Comment[pt]=Pesquisar as pastas por ficheiros de torrentes e carregá-los +Comment[pt_BR]=Procura na pastas por arquivos torrent e os carrega +Comment[ro]=Citește dosare pentru fișiere-torent și le încarcă +Comment[ru]=Ищет торренты в выбранных папках и загружает их +Comment[si]=ටොරෙන්ට් ගොනු සඳහා බහලුම් පිරික්සා ඒවා පූර්‍ණය කරන්න +Comment[sk]=Prehľadať adresáre pre torrenty a načítať ich +Comment[sl]=V mapah poišče datoteke torrent in jih naloži +Comment[sr]=Потражите и учитајте торент фајлове из фасцикли +Comment[sr@ijekavian]=Потражите и учитајте торент фајлове из фасцикли +Comment[sr@ijekavianlatin]=Potražite i učitajte torent fajlove iz fascikli +Comment[sr@latin]=Potražite i učitajte torent fajlove iz fascikli +Comment[sv]=Sök igenom kataloger efter dataflödesfiler och ladda dem +Comment[tr]=Dizinleri torrent dosyaları için tarar ve bulunan dosyaları yükler +Comment[uk]=Знайти у теках файли торентів і завантажити їх +Comment[x-test]=xxScan folders for torrent files and load themxx +Comment[zh_CN]=扫描文件夹以装入其中的种子文件 +Comment[zh_TW]=掃描資料夾尋找 torrent 檔案並載入 +Type=Service +X-KDE-Library=ktscanfolderplugin +X-KDE-PluginInfo-Author=Joris Guisson, Ivan Vasic +X-KDE-PluginInfo-Email=joris.guisson@gmail.com, ivasic@gmail.com +X-KDE-PluginInfo-Name=ScanFolderPlugin +X-KDE-PluginInfo-Version=0.1 +X-KDE-PluginInfo-Website=http://kde.org/applications/internet/ktorrent/ +X-KDE-PluginInfo-License=GPL +X-KDE-PluginInfo-EnabledByDefault=false +Icon=folder-open diff --git a/plugins/scanfolder/ktscanfolderplugin.kcfg b/plugins/scanfolder/ktscanfolderplugin.kcfg new file mode 100644 index 0000000..40c6b60 --- /dev/null +++ b/plugins/scanfolder/ktscanfolderplugin.kcfg @@ -0,0 +1,39 @@ + + + + + + + + + + + false + + + + + false + + + + + false + + + + + false + + + + false + + + + + + diff --git a/plugins/scanfolder/scanfolder.cpp b/plugins/scanfolder/scanfolder.cpp new file mode 100644 index 0000000..0b2c5be --- /dev/null +++ b/plugins/scanfolder/scanfolder.cpp @@ -0,0 +1,78 @@ +/* + SPDX-FileCopyrightText: 2006 Ivan Vasić + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "scanfolder.h" + +#include + +#include +#include +#include +#include +#include + +#include "scanthread.h" +#include "torrentloadqueue.h" +#include +#include +#include + +using namespace bt; + +namespace kt +{ +ScanFolder::ScanFolder(ScanThread *scanner, const QUrl &dir, bool recursive) + : scanner(scanner) + , scan_directory(dir) + , watch(nullptr) + , recursive(recursive) +{ + bt::Out(SYS_SNF | LOG_NOTICE) << "ScanFolder: scanning " << dir << endl; + + KConfigGroup config(KSharedConfig::openConfig(), "DirWatch"); + config.writeEntry("NFSPollInterval", 5000); + config.writeEntry("nfsPreferredMethod", "Stat"); // Force the usage of Stat method for NFS + config.sync(); + + watch = new KDirWatch(this); + connect(watch, &KDirWatch::dirty, this, &ScanFolder::scanDir); + connect(watch, &KDirWatch::created, this, &ScanFolder::scanDir); + + watch->addDir(dir.toLocalFile(), recursive ? KDirWatch::WatchSubDirs : KDirWatch::WatchDirOnly); + + scanner->addDirectory(dir, recursive); +} + +ScanFolder::~ScanFolder() +{ +} + +void ScanFolder::scanDir(const QString &path) +{ + if (!QFileInfo(path).isDir()) + return; + + QDir dir(path); + if (!recursive && dir != QDir(scan_directory.toLocalFile())) + return; + + // ignore loaded directories + if (dir.dirName() == i18nc("folder name part", "loaded")) + return; + + Out(SYS_SNF | LOG_NOTICE) << "Directory dirty: " << path << endl; + scanner->addDirectory(QUrl::fromLocalFile(path), false); +} + +void ScanFolder::setRecursive(bool rec) +{ + if (recursive != rec) { + recursive = rec; + watch->removeDir(scan_directory.toLocalFile()); + watch->addDir(scan_directory.toLocalFile(), recursive ? KDirWatch::WatchSubDirs : KDirWatch::WatchDirOnly); + } +} + +} diff --git a/plugins/scanfolder/scanfolder.h b/plugins/scanfolder/scanfolder.h new file mode 100644 index 0000000..fd8549f --- /dev/null +++ b/plugins/scanfolder/scanfolder.h @@ -0,0 +1,48 @@ +/* + SPDX-FileCopyrightText: 2006 Ivan Vasić + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef SCANFOLDER_H +#define SCANFOLDER_H + +#include +#include +#include + +namespace kt +{ +class ScanThread; + +/** + * Monitors a folder for changes, and passes torrents to load to the TorrentLoadQueue + */ +class ScanFolder : public QObject +{ + Q_OBJECT +public: + /** + * Default constructor. + * @param scanner The ScanThread + * @param dir The directory + */ + ScanFolder(ScanThread *scanner, const QUrl &dir, bool recursive); + ~ScanFolder() override; + + /** + * Set if the ScanFolder needs to scan subdirectories recursively + * @param rec Recursive or not + */ + void setRecursive(bool rec); + +public Q_SLOTS: + void scanDir(const QString &path); + +private: + ScanThread *scanner; + QUrl scan_directory; + KDirWatch *watch; + bool recursive; +}; +} +#endif diff --git a/plugins/scanfolder/scanfolderplugin.cpp b/plugins/scanfolder/scanfolderplugin.cpp new file mode 100644 index 0000000..39ff64d --- /dev/null +++ b/plugins/scanfolder/scanfolderplugin.cpp @@ -0,0 +1,97 @@ +/* + SPDX-FileCopyrightText: 2006 Ivan Vasić + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include + +#include "scanfolder.h" +#include "scanfolderplugin.h" +#include "scanfolderpluginsettings.h" +#include "scanfolderprefpage.h" +#include "scanthread.h" +#include "torrentloadqueue.h" + +using namespace bt; + +K_PLUGIN_FACTORY_WITH_JSON(ktorrent_scanfolder, "ktorrent_scanfolder.json", registerPlugin();) + +namespace kt +{ +ScanFolderPlugin::ScanFolderPlugin(QObject *parent, const QVariantList &args) + : Plugin(parent) + , tlq(nullptr) +{ + Q_UNUSED(args); +} + +ScanFolderPlugin::~ScanFolderPlugin() +{ +} + +void ScanFolderPlugin::load() +{ + LogSystemManager::instance().registerSystem(i18nc("plugin name", "Scan Folder"), SYS_SNF); + tlq = new TorrentLoadQueue(getCore(), this); + scanner = new ScanThread(); + connect(scanner, &ScanThread::found, tlq, qOverload &>(&TorrentLoadQueue::add), Qt::QueuedConnection); + pref = new ScanFolderPrefPage(this, nullptr); + getGUI()->addPrefPage(pref); + connect(getCore(), &CoreInterface::settingsChanged, this, &ScanFolderPlugin::updateScanFolders); + scanner->start(QThread::IdlePriority); + updateScanFolders(); +} + +void ScanFolderPlugin::unload() +{ + LogSystemManager::instance().unregisterSystem(i18nc("plugin name", "Scan Folder")); + disconnect(getCore(), &CoreInterface::settingsChanged, this, &ScanFolderPlugin::updateScanFolders); + getGUI()->removePrefPage(pref); + scanner->stop(); + delete scanner; + scanner = nullptr; + delete pref; + pref = nullptr; + delete tlq; + tlq = nullptr; +} + +void ScanFolderPlugin::updateScanFolders() +{ + QStringList folders = ScanFolderPluginSettings::folders(); + + // make sure folders end with / + for (QString &s : folders) { + if (s.endsWith(bt::DirSeparator())) + s += bt::DirSeparator(); + } + + if (ScanFolderPluginSettings::actionDelete()) + tlq->setLoadedTorrentAction(DeleteAction); + else if (ScanFolderPluginSettings::actionMove()) + tlq->setLoadedTorrentAction(MoveAction); + else + tlq->setLoadedTorrentAction(DefaultAction); + + scanner->setRecursive(ScanFolderPluginSettings::recursive()); + scanner->setFolderList(folders); +} + +bool ScanFolderPlugin::versionCheck(const QString &version) const +{ + return version == QStringLiteral(VERSION); +} +} + +#include "scanfolderplugin.moc" diff --git a/plugins/scanfolder/scanfolderplugin.h b/plugins/scanfolder/scanfolderplugin.h new file mode 100644 index 0000000..ce41c34 --- /dev/null +++ b/plugins/scanfolder/scanfolderplugin.h @@ -0,0 +1,46 @@ +/* + SPDX-FileCopyrightText: 2006 Ivan Vasić + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KTSCANFOLDERPLUGIN_H +#define KTSCANFOLDERPLUGIN_H + +#include + +class QString; + +namespace kt +{ +class ScanFolderPrefPage; +class TorrentLoadQueue; +class ScanThread; + +/** + * @author Ivan Vasic + * @brief KTorrent ScanFolder plugin + * Automatically scans selected folder for torrent files and loads them. + */ +class ScanFolderPlugin : public Plugin +{ + Q_OBJECT +public: + ScanFolderPlugin(QObject *parent, const QVariantList &args); + ~ScanFolderPlugin() override; + + void load() override; + void unload() override; + bool versionCheck(const QString &version) const override; + +public Q_SLOTS: + void updateScanFolders(); + +private: + ScanFolderPrefPage *pref; + TorrentLoadQueue *tlq; + ScanThread *scanner; +}; + +} + +#endif diff --git a/plugins/scanfolder/scanfolderpluginsettings.kcfgc b/plugins/scanfolder/scanfolderpluginsettings.kcfgc new file mode 100644 index 0000000..af1ebbc --- /dev/null +++ b/plugins/scanfolder/scanfolderpluginsettings.kcfgc @@ -0,0 +1,7 @@ +# Code generation options for kconfig_compiler +File=ktscanfolderplugin.kcfg +ClassName=ScanFolderPluginSettings +Namespace=kt +Singleton=true +Mutators=true +# will create the necessary code for setting those variables \ No newline at end of file diff --git a/plugins/scanfolder/scanfolderprefpage.cpp b/plugins/scanfolder/scanfolderprefpage.cpp new file mode 100644 index 0000000..3a77d29 --- /dev/null +++ b/plugins/scanfolder/scanfolderprefpage.cpp @@ -0,0 +1,128 @@ +/* + SPDX-FileCopyrightText: 2006 Ivan Vasić + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "scanfolderprefpage.h" +#include "scanfolderplugin.h" + +#include +#include + +#include "scanfolderpluginsettings.h" +#include +#include +#include + +namespace kt +{ +ScanFolderPrefPage::ScanFolderPrefPage(ScanFolderPlugin *plugin, QWidget *parent) + : PrefPageInterface(ScanFolderPluginSettings::self(), i18nc("plugin name", "Scan Folder"), QStringLiteral("folder-open"), parent) + , m_plugin(plugin) +{ + setupUi(this); + connect(kcfg_actionDelete, &QCheckBox::toggled, kcfg_actionMove, &QCheckBox::setDisabled); + connect(m_add, &QPushButton::clicked, this, &ScanFolderPrefPage::addPressed); + connect(m_remove, &QPushButton::clicked, this, &ScanFolderPrefPage::removePressed); + connect(m_folders, &QListWidget::itemSelectionChanged, this, &ScanFolderPrefPage::selectionChanged); + connect(m_group, qOverload(&QComboBox::currentIndexChanged), this, &ScanFolderPrefPage::currentGroupChanged); +} + +ScanFolderPrefPage::~ScanFolderPrefPage() +{ +} + +void ScanFolderPrefPage::loadSettings() +{ + kcfg_actionMove->setEnabled(!ScanFolderPluginSettings::actionDelete()); + + m_group->clear(); + + GroupManager *gman = m_plugin->getCore()->getGroupManager(); + QStringList grps; + GroupManager::Itr it = gman->begin(); + int current = 0; + int cnt = 0; + // now custom ones + while (it != gman->end()) { + if (it->second->groupFlags() & Group::CUSTOM_GROUP) { + grps << it->first; + if (it->first == ScanFolderPluginSettings::group()) + current = cnt; + cnt++; + } + ++it; + } + m_group->addItems(grps); + m_group->setEnabled(ScanFolderPluginSettings::addToGroup() && grps.count() > 0); + m_group->setCurrentIndex(current); + kcfg_addToGroup->setEnabled(grps.count() > 0); + + m_folders->clear(); + folders = ScanFolderPluginSettings::folders(); + for (const QString &f : qAsConst(folders)) { + m_folders->addItem(new QListWidgetItem(QIcon::fromTheme(QStringLiteral("folder")), f)); + } + selectionChanged(); +} + +void ScanFolderPrefPage::loadDefaults() +{ + kcfg_actionMove->setEnabled(!ScanFolderPluginSettings::actionDelete()); + + m_folders->clear(); + folders.clear(); +} + +void ScanFolderPrefPage::updateSettings() +{ + if (kcfg_addToGroup->isChecked() && kcfg_addToGroup->isEnabled()) + ScanFolderPluginSettings::setGroup(m_group->currentText()); + else + ScanFolderPluginSettings::setGroup(QString()); + + ScanFolderPluginSettings::setFolders(folders); + ScanFolderPluginSettings::self()->save(); + m_plugin->updateScanFolders(); +} + +void ScanFolderPrefPage::addPressed() +{ + QString p = QFileDialog::getExistingDirectory(this); + if (!p.isEmpty()) { + if (!p.endsWith(bt::DirSeparator())) + p += bt::DirSeparator(); + m_folders->addItem(new QListWidgetItem(QIcon::fromTheme(QStringLiteral("folder")), p)); + folders.append(p); + } + + updateButtons(); +} + +void ScanFolderPrefPage::removePressed() +{ + const QList sel = m_folders->selectedItems(); + for (QListWidgetItem *i : sel) { + folders.removeAll(i->text()); + delete i; + } + + updateButtons(); +} + +void ScanFolderPrefPage::selectionChanged() +{ + m_remove->setEnabled(m_folders->selectedItems().count() > 0); +} + +void ScanFolderPrefPage::currentGroupChanged(int idx) +{ + Q_UNUSED(idx); + updateButtons(); +} + +bool ScanFolderPrefPage::customWidgetsChanged() +{ + return ScanFolderPluginSettings::group() != m_group->currentText() || folders != ScanFolderPluginSettings::folders(); +} +} diff --git a/plugins/scanfolder/scanfolderprefpage.h b/plugins/scanfolder/scanfolderprefpage.h new file mode 100644 index 0000000..3b9e354 --- /dev/null +++ b/plugins/scanfolder/scanfolderprefpage.h @@ -0,0 +1,45 @@ +/* + SPDX-FileCopyrightText: 2006 Ivan Vasić + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KTSCANFOLDERPREFPAGE_H +#define KTSCANFOLDERPREFPAGE_H + +#include "scanfolderplugin.h" +#include "ui_scanfolderprefpage.h" +#include + +namespace kt +{ +/** + * ScanFolder plugin preferences page + * @author Ivan Vasić + */ +class ScanFolderPrefPage : public PrefPageInterface, public Ui_ScanFolderPrefPage +{ + Q_OBJECT + +public: + ScanFolderPrefPage(ScanFolderPlugin *plugin, QWidget *parent); + ~ScanFolderPrefPage() override; + + void loadSettings() override; + void loadDefaults() override; + void updateSettings() override; + bool customWidgetsChanged() override; + +private Q_SLOTS: + void addPressed(); + void removePressed(); + void selectionChanged(); + void currentGroupChanged(int idx); + +private: + ScanFolderPlugin *m_plugin; + QStringList folders; +}; + +} + +#endif diff --git a/plugins/scanfolder/scanfolderprefpage.ui b/plugins/scanfolder/scanfolderprefpage.ui new file mode 100644 index 0000000..eb96e90 --- /dev/null +++ b/plugins/scanfolder/scanfolderprefpage.ui @@ -0,0 +1,162 @@ + + + ScanFolderPrefPage + + + + 0 + 0 + 559 + 590 + + + + + + + Folders to scan for torrents: + + + + + + + + + List of folders which will be scanned for torrents by this plugin. + + + + + + + + + Add a new folder to be scanned. + + + Add Folder + + + + + + + Remove a folder from the list. + + + Remove Folder + + + + + + + Qt::Vertical + + + + 20 + 221 + + + + + + + + + + + + Options + + + + + + Open the torrents without asking any questions. + + + Open silently + + + + + + + Scan the folder recursively for torrents. <br/><br/> +Note: This will not be done for any folder named loaded. + + + Scan subfolders + + + + + + + When a torrent file has been found and loaded, delete it.<br/><br/> +Warning: you will permanently lose this file. + + + Remove torrent file after loading + + + + + + + When a torrent file is loaded, move it to a subdirectory named loaded. If the folder does not exist, it will be created. + + + Move torrent file to loaded directory + + + + + + + + + Add torrents opened with this plugin to a group. + + + Add torrent to group: + + + + + + + Group to add torrents to. + + + + + + + + + + + + + + kcfg_addToGroup + toggled(bool) + m_group + setEnabled(bool) + + + 251 + 567 + + + 366 + 567 + + + + + diff --git a/plugins/scanfolder/scanthread.cpp b/plugins/scanfolder/scanthread.cpp new file mode 100644 index 0000000..7dbd4aa --- /dev/null +++ b/plugins/scanfolder/scanthread.cpp @@ -0,0 +1,183 @@ +/* + SPDX-FileCopyrightText: 2011 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "scanthread.h" + +#include +#include +#include +#include + +#include + +#include + +namespace kt +{ +const int UPDATE_FOLDER_EVENT = QEvent::User + 1; +const int RECURSIVE_SCAN_EVENT = QEvent::User + 2; + +class UpdateFolderEvent : public QEvent +{ +public: + UpdateFolderEvent() + : QEvent((QEvent::Type)UPDATE_FOLDER_EVENT) + { + } + + ~UpdateFolderEvent() override + { + } +}; + +class RecursiveScanEvent : public QEvent +{ +public: + RecursiveScanEvent(const QUrl &url) + : QEvent((QEvent::Type)RECURSIVE_SCAN_EVENT) + , url(url) + { + } + + ~RecursiveScanEvent() override + { + } + + QUrl url; +}; + +ScanThread::ScanThread() + : stop_requested(false) + , recursive(false) +{ + scan_folders.setAutoDelete(true); + moveToThread(this); +} + +ScanThread::~ScanThread() +{ +} + +void ScanThread::setRecursive(bool rec) +{ + recursive = rec; +} + +void ScanThread::addDirectory(const QUrl &url, bool recursive) +{ + scan(url, recursive); +} + +void ScanThread::setFolderList(const QStringList &folders) +{ + QMutexLocker lock(&mutex); + if (this->folders != folders) { + this->folders = folders; + // Use custom event to wake up scanner thread + QCoreApplication::postEvent(this, new UpdateFolderEvent()); + } +} + +void ScanThread::customEvent(QEvent *ev) +{ + if (ev->type() == UPDATE_FOLDER_EVENT) { + updateFolders(); + } else if (ev->type() == RECURSIVE_SCAN_EVENT) { + RecursiveScanEvent *rev = (RecursiveScanEvent *)ev; + scan(rev->url, true); + } + ev->accept(); +} + +void ScanThread::updateFolders() +{ + QStringList tmp; + + mutex.lock(); + tmp = folders; // Use tmp list to not block the mutex for to long + mutex.unlock(); + + // first erase folders we don't need anymore + bt::PtrMap::iterator i = scan_folders.begin(); + while (i != scan_folders.end()) { + if (!tmp.contains(i->first)) { + QString f = i->first; + i++; + scan_folders.erase(f); + } else { + i->second->setRecursive(recursive); + i++; + } + } + + for (const QString &folder : qAsConst(tmp)) { + if (scan_folders.find(folder)) + continue; + + if (QDir(folder).exists()) { + // only add folder when it exists + ScanFolder *sf = new ScanFolder(this, QUrl::fromLocalFile(folder), recursive); + scan_folders.insert(folder, sf); + } + } +} + +void ScanThread::run() +{ + updateFolders(); + exec(); +} + +void ScanThread::stop() +{ + stop_requested = true; + + // XXX seems like deleting KDirWatch object(s) created in scan_folders + // in destructor of this QThread after it has been stopped + // causes memory corruption, so we delete them early + scan_folders.clear(); + exit(); + wait(); +} + +bool ScanThread::alreadyLoaded(const QDir &d, const QString &torrent) +{ + return d.exists(QLatin1Char('.') + torrent); +} + +void ScanThread::scan(const QUrl &dir, bool recursive) +{ + if (stop_requested) + return; + + QStringList filters; + filters << QStringLiteral("*.torrent"); + QDir d(dir.toLocalFile()); + const QStringList files = d.entryList(filters, QDir::Readable | QDir::Files); + + QList torrents; + for (const QString &tor : files) { + if (!alreadyLoaded(d, tor)) + torrents.append(QUrl::fromLocalFile(d.absoluteFilePath(tor))); + } + + found(torrents); + + if (stop_requested) + return; + + if (recursive) { + const QString loaded_localized = i18nc("folder name part", "loaded"); + + const QStringList dirs = d.entryList(QDir::Readable | QDir::Dirs); + for (const QString &subdir : dirs) { + if (subdir != QStringLiteral(".") && subdir != QStringLiteral("..") && subdir != loaded_localized) { + QCoreApplication::postEvent(this, new RecursiveScanEvent(QUrl::fromLocalFile(d.absoluteFilePath(subdir)))); + } + } + } +} + +} diff --git a/plugins/scanfolder/scanthread.h b/plugins/scanfolder/scanthread.h new file mode 100644 index 0000000..d13aab3 --- /dev/null +++ b/plugins/scanfolder/scanthread.h @@ -0,0 +1,83 @@ +/* + SPDX-FileCopyrightText: 2011 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KT_SCANTHREAD_H +#define KT_SCANTHREAD_H + +#include +#include +#include +#include + +#include "scanfolder.h" +#include + +#include + +class QDir; + +namespace kt +{ +/** + * Thread which scans directories in the background and looks for torrent files. + */ +class ScanThread : public QThread +{ + Q_OBJECT +public: + ScanThread(); + ~ScanThread() override; + + /** + * Set whether to scan recursively or not + * @param rec Recursive or not + */ + void setRecursive(bool rec); + + /** + * Add a directory to scan. + * @param url Directory + * @param recursive Whether or not to scan resursively + */ + void addDirectory(const QUrl &url, bool recursive); + + /** + * Stop the scanning thread. + */ + void stop(); + + /** + * Set the list of folders to scan. + * @param folders List of folders + */ + void setFolderList(const QStringList &folders); + +protected: + void run() override; + +private: + void scan(const QUrl &dir, bool recursive); + bool alreadyLoaded(const QDir &d, const QString &torrent); + void updateFolders(); + void customEvent(QEvent *ev) override; + +Q_SIGNALS: + /** + * Emitted when one or more torrents are found. + * @param torrents The list of torrents + */ + void found(const QList &torrents); + +private: + QMutex mutex; + QStringList folders; + std::atomic stop_requested; + std::atomic recursive; + bt::PtrMap scan_folders; +}; + +} + +#endif // KT_SCANTHREAD_H diff --git a/plugins/scanfolder/torrentloadqueue.cpp b/plugins/scanfolder/torrentloadqueue.cpp new file mode 100644 index 0000000..67f33f3 --- /dev/null +++ b/plugins/scanfolder/torrentloadqueue.cpp @@ -0,0 +1,153 @@ +/* + SPDX-FileCopyrightText: 2011 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "torrentloadqueue.h" + +#include +#include +#include + +#include +#include + +#include "scanfolderpluginsettings.h" +#include +#include +#include +#include +#include +#include + +namespace kt +{ +TorrentLoadQueue::TorrentLoadQueue(CoreInterface *core, QObject *parent) + : QObject(parent) + , core(core) +{ + connect(&timer, &QTimer::timeout, this, &TorrentLoadQueue::loadOne); + timer.setSingleShot(true); +} + +TorrentLoadQueue::~TorrentLoadQueue() +{ +} + +void TorrentLoadQueue::add(const QUrl &url) +{ + to_load.append(url); + if (!timer.isActive()) + timer.start(1000); +} + +void TorrentLoadQueue::add(const QList &urls) +{ + to_load.append(urls); + if (!timer.isActive()) + timer.start(1000); +} + +bool TorrentLoadQueue::validateTorrent(const QUrl &url, QByteArray &data) +{ + // try to decode file, if it is syntactically correct, we can try to load it + QFile fptr(url.toLocalFile()); + if (!fptr.open(QIODevice::ReadOnly)) + return false; + + try { + data = fptr.readAll(); + + bt::BDecoder dec(data, false); + bt::BNode *n = dec.decode(); + if (n) { + // valid node, so file is complete + delete n; + return true; + } else { + // decoding failed so incomplete + return false; + } + } catch (...) { + // any error means shit happened and the file is incomplete + return false; + } +} + +void TorrentLoadQueue::loadOne() +{ + if (to_load.isEmpty()) + return; + + QUrl url = to_load.takeFirst(); + + QByteArray data; + if (validateTorrent(url, data)) { + // Load it + load(url, data); + } else { + // Not valid, so two options: + // - not a torrent + // - incomplete torrent, still being written + // We use the last modified time to determine this + if (QFileInfo(url.toLocalFile()).lastModified().secsTo(QDateTime::currentDateTime()) < 2) { + // Still being written, lets try again later + to_load.append(url); + } + } + + if (!to_load.isEmpty()) + timer.start(1000); +} + +void TorrentLoadQueue::load(const QUrl &url, const QByteArray &data) +{ + bt::Out(SYS_SNF | LOG_NOTICE) << "ScanFolder: loading " << url.toDisplayString() << bt::endl; + QString group; + if (ScanFolderPluginSettings::addToGroup()) + group = ScanFolderPluginSettings::group(); + + if (ScanFolderPluginSettings::openSilently()) + core->loadSilently(data, url, group, QString()); + else + core->load(data, url, group, QString()); + + loadingFinished(url); +} + +void TorrentLoadQueue::loadingFinished(const QUrl &url) +{ + QString name = url.fileName(); + QString dirname = QFileInfo(url.toLocalFile()).absolutePath(); + if (!dirname.endsWith(bt::DirSeparator())) + dirname += bt::DirSeparator(); + + switch (action) { + case DeleteAction: + // If torrent has it's hidden complement - remove it too. + if (bt::Exists(dirname + QLatin1Char('.') + name)) + bt::Delete(dirname + QLatin1Char('.') + name, true); + + bt::Delete(url.toLocalFile(), true); + break; + case MoveAction: + // If torrent has it's hidden complement - remove it too. + if (bt::Exists(dirname + QLatin1Char('.') + name)) + bt::Delete(dirname + QLatin1Char('.') + name, true); + + if (!bt::Exists(dirname + i18nc("folder name part", "loaded"))) + bt::MakeDir(dirname + i18nc("folder name part", "loaded"), true); + + KIO::file_move(url, + QUrl::fromLocalFile(dirname + i18nc("folder name part", "loaded") + bt::DirSeparator() + name), + -1, + KIO::HideProgressInfo | KIO::Overwrite); + break; + case DefaultAction: + QFile f(dirname + QLatin1Char('.') + name); + f.open(QIODevice::WriteOnly); + f.close(); + break; + } +} +} diff --git a/plugins/scanfolder/torrentloadqueue.h b/plugins/scanfolder/torrentloadqueue.h new file mode 100644 index 0000000..8187092 --- /dev/null +++ b/plugins/scanfolder/torrentloadqueue.h @@ -0,0 +1,94 @@ +/* + SPDX-FileCopyrightText: 2011 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KT_TORRENTLOADQUEUE_H +#define KT_TORRENTLOADQUEUE_H + +#include +#include + +namespace kt +{ +class CoreInterface; + +/// Action to perform after loading torrent. +enum LoadedTorrentAction { + DeleteAction, + MoveAction, + DefaultAction, +}; + +/** + * Queue of potential torrents. It will try to load them one by one, + * in a sane and none GUI blocking way. + */ +class TorrentLoadQueue : public QObject +{ + Q_OBJECT +public: + TorrentLoadQueue(CoreInterface *core, QObject *parent = nullptr); + ~TorrentLoadQueue() override; + + /// Set the loaded torrent action + void setLoadedTorrentAction(LoadedTorrentAction act) + { + action = act; + } + + /// Get the loaded torrent action + LoadedTorrentAction loadedTorrentAction() const + { + return action; + } + +public Q_SLOTS: + /** + * Add a torrent to load. + */ + void add(const QUrl &url); + + /** + * Add a list of torrents + */ + void add(const QList &urls); + +private: + /** + * Validate if a file is a torrent. + * @param url The file url + * @param data The torrent data will be put into this array upon success + * @return true upon success, false otherwise + */ + bool validateTorrent(const QUrl &url, QByteArray &data); + + /** + * Load a torrent + * @param url The file url + * @param data The torrent data + */ + void load(const QUrl &url, const QByteArray &data); + +private Q_SLOTS: + /** + * Attempt to load one torrent + */ + void loadOne(); + +private: + /** + * Loading of a torrent has finished. + * @param url The url + */ + void loadingFinished(const QUrl &url); + +private: + CoreInterface *core; + QList to_load; + LoadedTorrentAction action; + QTimer timer; +}; +} + +#endif diff --git a/plugins/scanforlostfiles/CMakeLists.txt b/plugins/scanforlostfiles/CMakeLists.txt new file mode 100644 index 0000000..9037d28 --- /dev/null +++ b/plugins/scanforlostfiles/CMakeLists.txt @@ -0,0 +1,25 @@ +add_library(ktorrent_scanforlostfiles MODULE) + +target_sources(ktorrent_scanforlostfiles PRIVATE + scanforlostfilesplugin.cpp + scanforlostfileswidget.cpp + scanforlostfilesprefpage.cpp + scanforlostfilesthread.cpp + nodeoperations.cpp) + +ki18n_wrap_ui(ktorrent_scanforlostfiles scanforlostfilesprefpage.ui scanforlostfileswidget.ui) +kconfig_add_kcfg_files(ktorrent_scanforlostfiles scanforlostfilespluginsettings.kcfgc) + +kcoreaddons_desktop_to_json(ktorrent_scanforlostfiles ktorrent_scanforlostfiles.desktop) + +target_link_libraries( + ktorrent_scanforlostfiles + ktcore + KF5::Torrent + KF5::ConfigCore + KF5::CoreAddons + KF5::I18n + KF5::KIOCore +) +install(TARGETS ktorrent_scanforlostfiles DESTINATION ${KTORRENT_PLUGIN_INSTALL_DIR} ) + diff --git a/plugins/scanforlostfiles/fsproxymodel.h b/plugins/scanforlostfiles/fsproxymodel.h new file mode 100644 index 0000000..e6c22ad --- /dev/null +++ b/plugins/scanforlostfiles/fsproxymodel.h @@ -0,0 +1,90 @@ +/* + SPDX-FileCopyrightText: 2020 Alexander Trufanov + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef FSPROXYMODEL_H +#define FSPROXYMODEL_H +#include +#include +#include + +namespace kt +{ +/** + * A simple proxy model that may filter out all data + * that isn't presented in its filter + */ +class FSProxyModel : public QSortFilterProxyModel +{ +public: + FSProxyModel(QObject *parent = nullptr) + : QSortFilterProxyModel(parent) + , m_filter(nullptr) + , m_filtered(true) + { + } + + ~FSProxyModel() + { + if (m_filter) + delete m_filter; + } + + /** + * @return a pointer to the filter set. + */ + const QSet *filter() const + { + return m_filter; + } + + /** + * Sets a new filter. The previous one is destroyed. + * @param filter A pointer to the new filter. + */ + void setFilter(QSet *filter) + { + if (m_filter && m_filter != filter) + delete m_filter; + m_filter = filter; + } + + /** + * @return true if filter is applied. + */ + bool filtered() const + { + return m_filtered; + } + + /** + * Disable or enable filtering (if any filter is set). + * @param val Indicates if filtering is turned on or off. + */ + void setFiltered(bool val) + { + m_filtered = val; + } + +protected: + bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override + { + if (m_filter && m_filtered) { + QFileSystemModel *m = reinterpret_cast(sourceModel()); + QModelIndex i = m->index(source_row, 0, source_parent); + QString fpath = m->filePath(i); + return m_filter->contains(fpath); + } else { + return !m_filtered; + } + } + +private: + QSet *m_filter; + bool m_filtered; +}; + +} + +#endif // FSPROXYMODEL_H diff --git a/plugins/scanforlostfiles/ktorrent_scanforlostfiles.desktop b/plugins/scanforlostfiles/ktorrent_scanforlostfiles.desktop new file mode 100644 index 0000000..11ff370 --- /dev/null +++ b/plugins/scanforlostfiles/ktorrent_scanforlostfiles.desktop @@ -0,0 +1,55 @@ +[Desktop Entry] +Name=Scan for lost files +Name[ca]=Explora els fitxers perduts +Name[ca@valencia]=Explora els fitxers perduts +Name[cs]=Vyhledávat ztracené soubory +Name[de]=Suchen nach verlorenen Dateien +Name[el]=Σάρωση για χαμένα αρχεία +Name[en_GB]=Scan for lost files +Name[es]=Escanear archivos perdidos +Name[fi]=Etsi kadonneita tiedostoja +Name[fr]=Rechercher des fichiers perdus +Name[it]=Ricerca dei file persi +Name[ko]=잃어버린 파일 검색 +Name[nl]=Zoek naar verloren bestanden +Name[pl]=Poszukaj zgubionych plików +Name[pt]=Sondar por ficheiros perdidos +Name[pt_BR]=Escanear por arquivos perdidos +Name[sk]=Skenovať stratené súbory +Name[sl]=Preišči za izgubljenimi datotekami +Name[sv]=Sök efter förlorade filer +Name[uk]=Пошук втрачених файлів +Name[x-test]=xxScan for lost filesxx +Name[zh_CN]=扫描遗落的文件 +Comment=Display files in the selected folder that don't belong to any torrent +Comment[ca]=Mostra els fitxers de la carpeta seleccionada que no pertanyen a cap torrent +Comment[ca@valencia]=Mostra els fitxers de la carpeta seleccionada que no pertanyen a cap torrent +Comment[cs]=Zobrazit ve vybrané složce soubory, které nepatří žádnému torrentu +Comment[de]=Dateien im ausgewählten Ordner anzeigen, die zu keinem Torrent gehören +Comment[el]=Να εμφανίζονται αρχεία στον επιλεγμένο φάκελο που δεν ανήκουν σε κανένα torrent +Comment[en_GB]=Display files in the selected folder that don't belong to any torrent +Comment[es]=Mostrar archivos en la carpeta seleccionada que no pertenecen a ningún torrent +Comment[fi]=Näyttää valitun kansion tiedostot, jotka eivät kuulu mihinkään torrenttiin +Comment[fr]=Afficher les fichiers dans le dossier sélectionné n'appartenant à aucun flux « torrent » +Comment[it]=Visualizza i file nella cartella selezionata che non appartengono ad alcun torrent +Comment[ko]=선택한 폴더에 있는 토렌트에 속하지 않은 표시 +Comment[nl]=Bestanden in de geselecteerde map tonen die niet behoren tot een torrent +Comment[pl]=Pokaż pliki w zaznaczonym katalogu, które nie należą do żadnego torrenta. +Comment[pt]=Mostrar os ficheiros na pasta seleccionada que não pertencem a nenhuma torrente +Comment[pt_BR]=Mostra arquivos na pasta selecionada que não pertencem a nenhum torrent +Comment[sk]=Zobraziť súbory vo vybranom priečinku, ktoré nepatria k žiadnemu torrentu +Comment[sl]=Prikažite datoteke v izbrani mapi, ki ne pripadajo nobenemu toku +Comment[sv]=Visa filer i den markerade katalogen som inte hör till något dataflöde +Comment[uk]=Показати файли у вибраній теці, які не належать до жодного торента +Comment[x-test]=xxDisplay files in the selected folder that don't belong to any torrentxx +Comment[zh_CN]=显示所选文件夹中不属于任何种子的文件 +Type=Service +X-KDE-Library=ktscanforlostfilesplugin +X-KDE-PluginInfo-Author=Alexander Trufanov +X-KDE-PluginInfo-Email=trufanovan@gmail.com +X-KDE-PluginInfo-Name=ScanForLostFilesPlugin +X-KDE-PluginInfo-Version=0.1 +X-KDE-PluginInfo-Website=http://kde.org/applications/internet/ktorrent/ +X-KDE-PluginInfo-License=GPL +X-KDE-PluginInfo-EnabledByDefault=false +Icon=edit-find diff --git a/plugins/scanforlostfiles/ktscanforlostfilesplugin.kcfg b/plugins/scanforlostfiles/ktscanforlostfilesplugin.kcfg new file mode 100644 index 0000000..a4c6bed --- /dev/null +++ b/plugins/scanforlostfiles/ktscanforlostfilesplugin.kcfg @@ -0,0 +1,16 @@ + + + + + + + + 0 + 0 + 2 + + + diff --git a/plugins/scanforlostfiles/nodeoperations.cpp b/plugins/scanforlostfiles/nodeoperations.cpp new file mode 100644 index 0000000..195d120 --- /dev/null +++ b/plugins/scanforlostfiles/nodeoperations.cpp @@ -0,0 +1,199 @@ +/* + SPDX-FileCopyrightText: 2020 Alexander Trufanov + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "nodeoperations.h" + +#include + +namespace kt +{ +FNode *NodeOperations::getChild(FNode *root, const QString &name, bool is_dir) +{ + FNode *child = root->first_child; + while (child && (child->name != name || child->is_dir != is_dir)) { + child = child->next; + } + return child; +} + +FNode *NodeOperations::addChild(FNode *root, const QString &name, bool is_dir) +{ + FNode *n = new FNode(); + n->parent = root; + n->name = name; + n->is_dir = is_dir; + if (!root->first_child) { + root->first_child = n; + } else { + FNode *last_child = root->first_child; + while (last_child->next) + last_child = last_child->next; + last_child->next = n; + n->prev = last_child; + } + return n; +} + +void NodeOperations::removeNode(FNode *n) +{ + while (n->first_child) { + removeNode(n->first_child); + } + + if (n->parent) { + if (n->parent->first_child == n) { + n->parent->first_child = n->next; + } + } + + if (n->prev) { + n->prev->next = n->next; + } + + if (n->next) { + n->next->prev = n->prev; + } + + free(n); +} + +FNode *NodeOperations::makePath(FNode *root, const QString &fname, bool is_dir) +{ + int idx = fname.indexOf(QLatin1Char('/')); + FNode *existing; + + if (idx == -1) { + existing = getChild(root, fname, is_dir); + if (existing) + return existing; + return addChild(root, fname, is_dir); + } else { + existing = getChild(root, fname.left(idx), true); + if (!existing) + existing = addChild(root, fname.left(idx), true); + return makePath(existing, fname.right(fname.size() - 1 - idx), is_dir); + } +} + +FNode *NodeOperations::findChild(FNode *root, const QString &fname, bool is_dir) +{ + int idx = fname.indexOf(QLatin1Char('/')); + if (idx == -1) { + return getChild(root, fname, is_dir); + } else { + FNode *n = getChild(root, fname.left(idx), true); + if (n) + n = findChild(n, fname.right(fname.size() - 1 - idx), is_dir); + return n; + } +} + +void NodeOperations::fillFromDir(FNode *root, const QDir &dir) +{ + if (QThread::currentThread()->isInterruptionRequested()) { + return; + } + + // QStringLists must be const to suppress "warning: c++11 range-loop might detach Qt container" + const QStringList sl_f = dir.entryList(QDir::NoDotAndDotDot | QDir::Files | QDir::Hidden); + for (const QString &s : sl_f) { + addChild(root, s, false); + } + + const QStringList sl_d = dir.entryList(QDir::NoDotAndDotDot | QDir::Dirs | QDir::Hidden); + QDir next_dir; + for (const QString &s : sl_d) { + FNode *d = addChild(root, s, true); + next_dir.setPath(dir.path() + QLatin1String("/") + s); + fillFromDir(d, next_dir); + } +} + +void NodeOperations::subtractTreesOnFiles(FNode *tree1, FNode *tree2) +{ + if (QThread::currentThread()->isInterruptionRequested()) { + return; + } + + FNode *c = tree2->first_child; + while (c) { + FNode *f = getChild(tree1, c->name, c->is_dir); + if (f) { + if (c->is_dir) + subtractTreesOnFiles(f, c); + else + removeNode(f); + } + c = c->next; + } +} + +void NodeOperations::pruneEmptyFolders(FNode *start_folder) +{ + FNode *c = start_folder->first_child; + while (c) { + if (c->is_dir) + pruneEmptyFolders(c); + c = c->next; + } + + if (!start_folder->first_child) { + removeNode(start_folder); + } +} + +void NodeOperations::pruneEmptyFolders(FNode *tree1, FNode *tree2) +{ + if (QThread::currentThread()->isInterruptionRequested()) { + return; + } + + FNode *c = tree2->first_child; + while (c) { + if (c->is_dir) { + FNode *f = getChild(tree1, c->name, c->is_dir); + if (f) { + pruneEmptyFolders(f, c); + } + } + c = c->next; + } + + if (!tree2->first_child) { + pruneEmptyFolders(tree1); + } +} + +void NodeOperations::printTree(FNode *root, const QString &path, QSet &set) +{ + if (QThread::currentThread()->isInterruptionRequested()) { + return; + } + + QString new_path; + if (!root->name.isEmpty()) { + new_path = path + QLatin1String("/") + root->name; + set += new_path; + } + + FNode *c = root->first_child; + + while (c) { + if (c->is_dir) { + printTree(c, new_path, set); + } else { + set += new_path + QLatin1String("/") + c->name; + } + c = c->next; + } +} + +void NodeOperations::printTree(FNode *root, QSet &set) +{ + QString path; + printTree(root, path, set); +} + +} diff --git a/plugins/scanforlostfiles/nodeoperations.h b/plugins/scanforlostfiles/nodeoperations.h new file mode 100644 index 0000000..15e640b --- /dev/null +++ b/plugins/scanforlostfiles/nodeoperations.h @@ -0,0 +1,137 @@ +/* + SPDX-FileCopyrightText: 2020 Alexander Trufanov + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef NODE_OPERATIONS_H +#define NODE_OPERATIONS_H +#include +#include +#include + +/** + * A structure to represent a filetree in memory and a set of operations on it + */ +namespace kt +{ +/** + * A node in a linked list which represents directory or file + */ +struct FNode { + FNode() + { + parent = prev = next = first_child = nullptr; + } + + QString name; + bool is_dir; + + FNode *parent; + FNode *prev; + FNode *next; + FNode *first_child; +}; + +class NodeOperations +{ +public: + /** + * Find a child node with a specified name (not recursive). + * + * @param root A parent node. + * @param name A node name to find. + * @param is_dir Are we looking for directory or file. + * + * @return pointer to node found or nullptr otherwise. + */ + static FNode *getChild(FNode *root, const QString &name, bool is_dir); + + /** + * Create a child node. Does not check if node already exists. + * + * @param root A parent node. + * @param name A new node name. + * @param is_dir Is new node a directory. + * + * @return pointer to new node. + */ + static FNode *addChild(FNode *root, const QString &name, bool is_dir); + + /** + * Remove and destroy node and all its children (recursive). + * @param n A node to remove. + */ + static void removeNode(FNode *n); + + /** + * Creates a subtree that represents a given filepath starting + * from the given root node. Performs check for already existing + * nodes and reuses them. Returns pointer to the node that + * represents last file or folder in filepath. + * + * @param root A parent node. + * @param fname A filepath to file or directory. + * @param is_dir Is filepath points to directory or file. + * + * @return pointer to the node that represents last file or + * folder in filepath + */ + static FNode *makePath(FNode *root, const QString &fname, bool is_dir); + + /** + * Find a child node that corresponds to the folder or file with + * a given filepath. + * + * @param root A node to start search. + * @param fname A filepath to search. + * @param is_dir Are we looking for directory or file. + * + * @return pointer to node found or nullptr otherwise. + */ + static FNode *findChild(FNode *root, const QString &fname, bool is_dir); + + /** + * Creates a subtree that represents a content of a directory + * including subfolders. Does not check if node already exists. + * + * @param root A parent node. + * @param dir A QDir that is set to directory whose content + * should be represented under the root. + */ + static void fillFromDir(FNode *root, const QDir &dir); + + /** + * Removes all file nodes in tree1 that are exist in tree2. + * Folder nodes are ignored. + * + * @param tree1 Pointer to parent node of first filetree. + * @param tree2 Pointer to parent node of subtracted filetree. + */ + static void subtractTreesOnFiles(FNode *tree1, FNode *tree2); + + /** + * Removes all folder nodes that contain no file + * children nodes using tree2 as a mask. Required to remove + * empty folders (and subfolders) that corresponds to the + * torrents folders. + * + * @param tree1 Pointer to parent node of first filetree. + * @param tree2 Pointer to parent node of mask filetree. + */ + static void pruneEmptyFolders(FNode *tree1, FNode *tree2); + + /** + * Add a filepath to every node in tree to the set. + * + * @param root Pointer to parent node of filetree. + * @param set Reference to set that shall keep resulting + * filepaths. + */ + static void printTree(FNode *root, QSet &set); + +private: + static void pruneEmptyFolders(FNode *start_folder); + static void printTree(FNode *root, const QString &path, QSet &set); +}; +} +#endif // NODE_OPERATIONS_H diff --git a/plugins/scanforlostfiles/scanforlostfilesplugin.cpp b/plugins/scanforlostfiles/scanforlostfilesplugin.cpp new file mode 100644 index 0000000..52e3c01 --- /dev/null +++ b/plugins/scanforlostfiles/scanforlostfilesplugin.cpp @@ -0,0 +1,125 @@ +/* + SPDX-FileCopyrightText: 2020 Alexander Trufanov + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include +#include +#include + +#include +#include + +#include +#include +#include +#include + +#include "scanforlostfilesplugin.h" +#include "scanforlostfilespluginsettings.h" +#include "scanforlostfilesprefpage.h" +#include "scanforlostfileswidget.h" + +using namespace bt; + +K_PLUGIN_FACTORY_WITH_JSON(ktorrent_scanforlostfiles, "ktorrent_scanforlostfiles.json", registerPlugin();) + +namespace kt +{ +ScanForLostFilesPlugin::ScanForLostFilesPlugin(QObject *parent, const QVariantList &args) + : Plugin(parent) + , m_view(nullptr) + , m_dock(nullptr) + , m_pref(nullptr) + , m_pos(SEPARATE_ACTIVITY) +{ + Q_UNUSED(args); +} + +ScanForLostFilesPlugin::~ScanForLostFilesPlugin() +{ +} + +void ScanForLostFilesPlugin::load() +{ + m_view = new ScanForLostFilesWidget(this); + m_pref = new ScanForLostFilesPrefPage(this, nullptr); + m_pos = (SFLFPosition)ScanForLostFilesPluginSettings::scanForLostFilesWidgetPosition(); + + addToGUI(); + getGUI()->addPrefPage(m_pref); + connect(getCore(), &CoreInterface::settingsChanged, this, &ScanForLostFilesPlugin::updateScanForLostFiles); + updateScanForLostFiles(); +} + +void ScanForLostFilesPlugin::unload() +{ + m_pref->saveSettings(); + disconnect(getCore(), &CoreInterface::settingsChanged, this, &ScanForLostFilesPlugin::updateScanForLostFiles); + + getGUI()->removePrefPage(m_pref); + removeFromGUI(); + delete m_pref; + m_pref = nullptr; + delete m_view; + m_view = nullptr; +} + +void ScanForLostFilesPlugin::updateScanForLostFiles() +{ + SFLFPosition p = (SFLFPosition)ScanForLostFilesPluginSettings::scanForLostFilesWidgetPosition(); + if (m_pos != p) { + removeFromGUI(); + m_pos = p; + addToGUI(); + } +} + +bool ScanForLostFilesPlugin::versionCheck(const QString &version) const +{ + return version == QStringLiteral(VERSION); +} + +void ScanForLostFilesPlugin::addToGUI() +{ + switch (m_pos) { + case SEPARATE_ACTIVITY: + getGUI()->addActivity(m_view); + break; + case DOCKABLE_WIDGET: { + KMainWindow *mwnd = getGUI()->getMainWindow(); + m_dock = new QDockWidget(mwnd); + m_dock->setWidget(m_view); + m_dock->setObjectName(QStringLiteral("ScanForLostFilesDockWidget")); + mwnd->addDockWidget(Qt::BottomDockWidgetArea, m_dock); + break; + } + case TORRENT_ACTIVITY: + getGUI()->getTorrentActivity()->addToolWidget(m_view, m_view->name(), m_view->icon(), m_view->toolTip()); + break; + } +} + +void ScanForLostFilesPlugin::removeFromGUI() +{ + switch (m_pos) { + case SEPARATE_ACTIVITY: + getGUI()->removeActivity(m_view); + break; + case TORRENT_ACTIVITY: + getGUI()->getTorrentActivity()->removeToolWidget(m_view); + break; + case DOCKABLE_WIDGET: { + KMainWindow *mwnd = getGUI()->getMainWindow(); + mwnd->removeDockWidget(m_dock); + m_dock->setWidget(nullptr); + m_view->setParent(nullptr); + delete m_dock; + m_dock = nullptr; + break; + } + } +} +} + +#include "scanforlostfilesplugin.moc" diff --git a/plugins/scanforlostfiles/scanforlostfilesplugin.h b/plugins/scanforlostfiles/scanforlostfilesplugin.h new file mode 100644 index 0000000..69fc615 --- /dev/null +++ b/plugins/scanforlostfiles/scanforlostfilesplugin.h @@ -0,0 +1,57 @@ +/* + SPDX-FileCopyrightText: 2020 Alexander Trufanov + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KTSCANFORLOSTFILESPLUGIN_H +#define KTSCANFORLOSTFILESPLUGIN_H + +#include + +class QString; +class QDockWidget; + +namespace kt +{ +class ScanForLostFilesPrefPage; +class ScanForLostFilesWidget; + +enum SFLFPosition { + SEPARATE_ACTIVITY = 0, + DOCKABLE_WIDGET = 1, + TORRENT_ACTIVITY = 2, +}; + +/** + * @author Alexander Trufanov + * @brief KTorrent ScanForLostFiles plugin + * Display files in selected folder that do not belong to any torrent. + */ +class ScanForLostFilesPlugin : public Plugin +{ + Q_OBJECT +public: + ScanForLostFilesPlugin(QObject *parent, const QVariantList &args); + ~ScanForLostFilesPlugin() override; + + void load() override; + void unload() override; + bool versionCheck(const QString &version) const override; + +public Q_SLOTS: + void updateScanForLostFiles(); + +private: + void addToGUI(); + void removeFromGUI(); + +private: + ScanForLostFilesWidget *m_view; + QDockWidget *m_dock; + ScanForLostFilesPrefPage *m_pref; + SFLFPosition m_pos; +}; + +} + +#endif diff --git a/plugins/scanforlostfiles/scanforlostfilespluginsettings.kcfgc b/plugins/scanforlostfiles/scanforlostfilespluginsettings.kcfgc new file mode 100644 index 0000000..64c7cb6 --- /dev/null +++ b/plugins/scanforlostfiles/scanforlostfilespluginsettings.kcfgc @@ -0,0 +1,7 @@ +# Code generation options for kconfig_compiler +File=ktscanforlostfilesplugin.kcfg +ClassName=ScanForLostFilesPluginSettings +Namespace=kt +Singleton=true +Mutators=true +# will create the necessary code for setting those variables diff --git a/plugins/scanforlostfiles/scanforlostfilesprefpage.cpp b/plugins/scanforlostfiles/scanforlostfilesprefpage.cpp new file mode 100644 index 0000000..e0ad9d5 --- /dev/null +++ b/plugins/scanforlostfiles/scanforlostfilesprefpage.cpp @@ -0,0 +1,55 @@ +/* + SPDX-FileCopyrightText: 2020 Alexander Trufanov + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "scanforlostfilesprefpage.h" +#include "scanforlostfilesplugin.h" + +#include + +#include "scanforlostfilespluginsettings.h" +#include +#include + +namespace kt +{ +ScanForLostFilesPrefPage::ScanForLostFilesPrefPage(ScanForLostFilesPlugin *plugin, QWidget *parent) + : PrefPageInterface(ScanForLostFilesPluginSettings::self(), i18nc("plugin name", "Scan for lost files"), QStringLiteral("edit-find"), parent) + , m_plugin(plugin) +{ + setupUi(this); +} + +ScanForLostFilesPrefPage::~ScanForLostFilesPrefPage() +{ +} + +void ScanForLostFilesPrefPage::loadSettings() +{ + kcfg_ScanForLostFilesWidgetPosition->setCurrentIndex(ScanForLostFilesPluginSettings::scanForLostFilesWidgetPosition()); +} + +void ScanForLostFilesPrefPage::loadDefaults() +{ + kcfg_ScanForLostFilesWidgetPosition->setCurrentIndex(0); +} + +void ScanForLostFilesPrefPage::saveSettings() +{ + ScanForLostFilesPluginSettings::setScanForLostFilesWidgetPosition(kcfg_ScanForLostFilesWidgetPosition->currentIndex()); + ScanForLostFilesPluginSettings::self()->save(); +} + +void ScanForLostFilesPrefPage::updateSettings() +{ + saveSettings(); + m_plugin->updateScanForLostFiles(); +} + +bool ScanForLostFilesPrefPage::customWidgetsChanged() +{ + return true; +} + +} diff --git a/plugins/scanforlostfiles/scanforlostfilesprefpage.h b/plugins/scanforlostfiles/scanforlostfilesprefpage.h new file mode 100644 index 0000000..2532302 --- /dev/null +++ b/plugins/scanforlostfiles/scanforlostfilesprefpage.h @@ -0,0 +1,41 @@ +/* + SPDX-FileCopyrightText: 2020 Alexander Trufanov + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KTSCANFORLOSTFILESPREFPAGE_H +#define KTSCANFORLOSTFILESPREFPAGE_H + +#include "scanforlostfilesplugin.h" +#include "ui_scanforlostfilesprefpage.h" + +#include + +namespace kt +{ +/** + * ScanForLostFiles plugin preferences page + */ + +class ScanForLostFilesPrefPage : public PrefPageInterface, public Ui::ScanForLostFilesPrefPage +{ + Q_OBJECT + +public: + ScanForLostFilesPrefPage(ScanForLostFilesPlugin *plugin, QWidget *parent); + ~ScanForLostFilesPrefPage() override; + + void loadSettings() override; + void loadDefaults() override; + void updateSettings() override; + bool customWidgetsChanged() override; + + void saveSettings(); + +private: + ScanForLostFilesPlugin *m_plugin; +}; + +} + +#endif diff --git a/plugins/scanforlostfiles/scanforlostfilesprefpage.ui b/plugins/scanforlostfiles/scanforlostfilesprefpage.ui new file mode 100644 index 0000000..fbb333d --- /dev/null +++ b/plugins/scanforlostfiles/scanforlostfilesprefpage.ui @@ -0,0 +1,80 @@ + + + ScanForLostFilesPrefPage + + + + 0 + 0 + 559 + 590 + + + + + + + + + Widget position: + + + + + + + + 0 + 0 + + + + + Separate activity + + + + + Dockable widget + + + + + Torrent activity + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + diff --git a/plugins/scanforlostfiles/scanforlostfilesthread.cpp b/plugins/scanforlostfiles/scanforlostfilesthread.cpp new file mode 100644 index 0000000..e5cdaa1 --- /dev/null +++ b/plugins/scanforlostfiles/scanforlostfilesthread.cpp @@ -0,0 +1,80 @@ +/* + SPDX-FileCopyrightText: 2020 Alexander Trufanov + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "scanforlostfilesthread.h" + +#include "nodeoperations.h" +#include +#include +#include + +namespace kt +{ +ScanForLostFilesThread::ScanForLostFilesThread(const QString &folder, CoreInterface *core, QObject *parent) + : QThread(parent) + , m_core(core) +{ + m_root_folder = folder; + while (m_root_folder.endsWith(QLatin1String("/")) && m_root_folder != QLatin1String("/")) { + m_root_folder.chop(1); + } +} + +void ScanForLostFilesThread::run() +{ + if (!m_core) { + Q_EMIT filterReady(nullptr); + return; + } + + FNode *torrent_files = new FNode(); + FNode *torrent_folders = new FNode(); + NodeOperations::makePath(torrent_files, m_root_folder, true); + + kt::QueueManager *qman = m_core->getQueueManager(); + if (qman) { + QList::iterator it = qman->begin(); + while (it != qman->end()) { + if (isInterruptionRequested()) + break; + + bt::TorrentInterface *tor = *it; + if (tor->getStats().multi_file_torrent) { + for (bt::Uint32 i = 0; i < tor->getNumFiles(); i++) { + NodeOperations::makePath(torrent_files, tor->getTorrentFile(i).getPathOnDisk(), false); + } + if (tor->getNumFiles()) { + QString folderpath = tor->getTorrentFile(0).getPathOnDisk(); + int idx = folderpath.lastIndexOf(tor->getTorrentFile(0).getUserModifiedPath()); + QString out_folder = folderpath.left(idx - 1); + NodeOperations::makePath(torrent_folders, out_folder, true); + } + } else + NodeOperations::makePath(torrent_files, tor->getStats().output_path, false); + + it++; + } + } + + FNode *existing_files = new FNode(); + FNode *folder_node = NodeOperations::makePath(existing_files, m_root_folder, true); + QDir dir(m_root_folder); + + if (!isInterruptionRequested()) { + NodeOperations::fillFromDir(folder_node, dir); + NodeOperations::subtractTreesOnFiles(existing_files, torrent_files); + NodeOperations::pruneEmptyFolders(existing_files, torrent_folders); + } + + QSet *filter = new QSet(); + NodeOperations::printTree(existing_files, *filter); + Q_EMIT filterReady(filter); + + NodeOperations::removeNode(torrent_files); + NodeOperations::removeNode(torrent_folders); + NodeOperations::removeNode(existing_files); +} + +} diff --git a/plugins/scanforlostfiles/scanforlostfilesthread.h b/plugins/scanforlostfiles/scanforlostfilesthread.h new file mode 100644 index 0000000..ea6a230 --- /dev/null +++ b/plugins/scanforlostfiles/scanforlostfilesthread.h @@ -0,0 +1,51 @@ +/* + SPDX-FileCopyrightText: 2020 Alexander Trufanov + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef SCANFORLOSTFILESTHREAD_H +#define SCANFORLOSTFILESTHREAD_H + +#include +#include + +namespace kt +{ +class CoreInterface; + +/** + * ScanForLostFiles working thread. It: + * 1. Lists the folder content with QDirIterator + * 2. Lists the files that belongs to torrents + * 3. Substracts the results of 2nd step from results of 1st step + * 4. Returns resulting file tree as a set of filepaths by emitting filterReady signal + */ + +class ScanForLostFilesThread : public QThread +{ + Q_OBJECT +public: + /** + * Set the list of folders to scan. + * @param folder A folder whose content shall be displayed + * @param core A core interface pointer to get list of torrent files + */ + ScanForLostFilesThread(const QString &folder, CoreInterface *core, QObject *parent = nullptr); + +protected: + void run() override; + +Q_SIGNALS: + /** + * Emitted when filter generation is complete. + * @param filter Pointer to set of filepaths that are not belong to any torrent + */ + void filterReady(QSet *filter); + +private: + QString m_root_folder; + CoreInterface *m_core; +}; + +} +#endif // SCANFORLOSTFILESTHREAD_H diff --git a/plugins/scanforlostfiles/scanforlostfileswidget.cpp b/plugins/scanforlostfiles/scanforlostfileswidget.cpp new file mode 100644 index 0000000..67aae49 --- /dev/null +++ b/plugins/scanforlostfiles/scanforlostfileswidget.cpp @@ -0,0 +1,201 @@ +/* + SPDX-FileCopyrightText: 2020 Alexander Trufanov + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "scanforlostfileswidget.h" +#include "scanforlostfilesplugin.h" +#include +#include +#include +#include +#include + +#include "fsproxymodel.h" +#include "scanforlostfilesthread.h" +#include +#include +#include + +#include "scanforlostfilespluginsettings.h" +#include +#include +#include + +namespace kt +{ +ScanForLostFilesWidget::ScanForLostFilesWidget(ScanForLostFilesPlugin *plugin, QWidget *parent) + : Activity(i18n("Scan for lost files"), QStringLiteral("edit-find"), 1000, parent) + , m_plugin(plugin) + , m_thread(nullptr) +{ + setupUi(this); + + m_model = new QFileSystemModel(this); + m_model->setFilter(QDir::NoDotAndDotDot | QDir::AllEntries | QDir::Hidden); + + m_proxy = new FSProxyModel(this); + + connect(cbShowAllFiles, &QCheckBox::stateChanged, [=](int val) { + m_proxy->setFiltered(!val); + setupModels(); + }); + + connect(actionCopy_to_clipboard, &QAction::triggered, [=]() { + QModelIndex m = treeView->currentIndex(); + m = m_proxy->mapToSource(m); + const QString fname = m_model->fileName(m); + QGuiApplication::clipboard()->setText(fname); + }); + + connect(actionOpen_file, &QAction::triggered, [=]() { + QModelIndex index = treeView->currentIndex(); + new KRun(QUrl::fromLocalFile(m_model->filePath(m_proxy->mapToSource(index))), nullptr, true); + }); + + treeView->setSortingEnabled(true); + m_menu = new QMenu(treeView); + m_menu->addAction(actionCopy_to_clipboard); + m_menu->addAction(actionOpen_file); + m_menu->addAction(actionDelete_on_disk); + treeView->setContextMenuPolicy(Qt::CustomContextMenu); + + setupModels(); + + progressBar->setVisible(false); + + reqFolder->setMode(KFile::Directory | KFile::ExistingOnly); + connect(reqFolder, &KUrlRequester::urlSelected, btnScanFolder, &QPushButton::click); + connect(reqFolder, QOverload<>::of(&KUrlRequester::returnPressed), btnScanFolder, &QPushButton::click); + if (CoreInterface *c = m_plugin->getCore()) { + if (GroupManager *gm = c->getGroupManager()) { + if (Group *all = gm->allGroup()) { + const QString default_save_location = all->groupPolicy().default_save_location; + if (!default_save_location.isEmpty()) { + reqFolder->setUrl(QUrl::fromLocalFile(default_save_location)); + } + } + } + } +} + +void ScanForLostFilesWidget::setupModels() +{ + const QString root_folder = reqFolder->text(); + m_proxy->setSourceModel(nullptr); + treeView->setModel(nullptr); + m_model->setRootPath(root_folder); + m_proxy->setSourceModel(m_model); + treeView->setModel(m_proxy); + treeView->header()->hideSection(2); // hide Type column + treeView->header()->setSectionResizeMode(0, QHeaderView::ResizeToContents); + QModelIndex m = m_model->index(root_folder); + m = m_proxy->mapFromSource(m); + if (m.isValid()) { + treeView->setRootIndex(m); + } +} + +ScanForLostFilesWidget::~ScanForLostFilesWidget() +{ +} + +void ScanForLostFilesWidget::directoryLoaded(const QString &path) +{ + QModelIndex m = m_model->index(path); + if (m_model->canFetchMore(m)) { + m_model->fetchMore(m); + } + treeView->expandAll(); +} + +void ScanForLostFilesWidget::on_btnExpandAll_clicked() +{ + connect(m_model, &QFileSystemModel::directoryLoaded, this, &ScanForLostFilesWidget::directoryLoaded); + treeView->expandAll(); +} + +void ScanForLostFilesWidget::on_btnCollapseAll_clicked() +{ + disconnect(m_model, &QFileSystemModel::directoryLoaded, this, &ScanForLostFilesWidget::directoryLoaded); + treeView->collapseAll(); +} + +void ScanForLostFilesWidget::on_btnScanFolder_clicked() +{ + if (treeView->model()) { + treeView->setModel(nullptr); + } + + if (m_thread) { + m_thread->requestInterruption(); + m_thread->terminate(); + m_thread->wait(); + m_thread = nullptr; + return; + } + + const QString root_folder = reqFolder->text(); + + m_thread = new ScanForLostFilesThread(root_folder, m_plugin->getCore(), this); + btnScanFolder->setText(i18n("Cancel")); + progressBar->setVisible(true); + + connect( + m_thread, + &ScanForLostFilesThread::finished, + this, + [=]() { + btnScanFolder->setText(i18n("Scan")); + progressBar->setVisible(false); + m_thread->deleteLater(); + m_thread = nullptr; + }, + Qt::QueuedConnection); + + connect( + m_thread, + &ScanForLostFilesThread::filterReady, + this, + [=](QSet *filter) { + if (filter) { + m_proxy->setFilter(filter); + setupModels(); + } + }, + Qt::QueuedConnection); + + m_thread->start(); +} + +void ScanForLostFilesWidget::on_actionDelete_on_disk_triggered() +{ + QModelIndexList sel = treeView->selectionModel()->selectedRows(); + int n = sel.count(); + if (n == 1) { // single item can be a directory + if (m_model->fileInfo(m_proxy->mapToSource(sel.front())).isDir()) + n++; + } + + QString msg = i18np("You will lose all data in this file, are you sure you want to do this?", + "You will lose all data in these files, are you sure you want to do this?", + n); + + QList to_del; + if (KMessageBox::warningYesNo(nullptr, msg) == KMessageBox::Yes) { + for (const QModelIndex &m : sel) { + to_del.append(QUrl::fromLocalFile(m_model->filePath(m_proxy->mapToSource(m)))); + } + KIO::del(to_del); + } +} + +void ScanForLostFilesWidget::on_treeView_customContextMenuRequested(const QPoint &pos) +{ + actionOpen_file->setEnabled(treeView->currentIndex().isValid()); + actionDelete_on_disk->setEnabled(treeView->currentIndex().isValid() || treeView->selectionModel()->selectedRows().count()); + + m_menu->exec(treeView->mapToGlobal(pos)); +} + +} diff --git a/plugins/scanforlostfiles/scanforlostfileswidget.h b/plugins/scanforlostfiles/scanforlostfileswidget.h new file mode 100644 index 0000000..8d79452 --- /dev/null +++ b/plugins/scanforlostfiles/scanforlostfileswidget.h @@ -0,0 +1,54 @@ +/* + SPDX-FileCopyrightText: 2020 Alexander Trufanov + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KTSCANFORLOSTFILESWIDGET_H +#define KTSCANFORLOSTFILESWIDGET_H + +#include "scanforlostfilesplugin.h" +#include "ui_scanforlostfileswidget.h" +#include + +class QFileSystemModel; +class QMenu; + +namespace kt +{ +class FSProxyModel; +class ScanForLostFilesThread; + +/** + * ScanForLostFiles plugin widget + */ +class ScanForLostFilesWidget : public Activity, public Ui::ScanForLostFilesWidget +{ + Q_OBJECT + +public: + ScanForLostFilesWidget(ScanForLostFilesPlugin *plugin, QWidget *parent = nullptr); + ~ScanForLostFilesWidget() override; + +private Q_SLOTS: + void on_btnScanFolder_clicked(); + void on_btnExpandAll_clicked(); + void on_btnCollapseAll_clicked(); + void on_actionDelete_on_disk_triggered(); + void on_treeView_customContextMenuRequested(const QPoint &pos); + + void directoryLoaded(const QString &path); + +private: + void setupModels(); + +private: + ScanForLostFilesPlugin *m_plugin; + QFileSystemModel *m_model; + FSProxyModel *m_proxy; + QMenu *m_menu; + ScanForLostFilesThread *m_thread; +}; + +} + +#endif diff --git a/plugins/scanforlostfiles/scanforlostfileswidget.ui b/plugins/scanforlostfiles/scanforlostfileswidget.ui new file mode 100644 index 0000000..d0c0446 --- /dev/null +++ b/plugins/scanforlostfiles/scanforlostfileswidget.ui @@ -0,0 +1,159 @@ + + + ScanForLostFilesWidget + + + + 0 + 0 + 559 + 590 + + + + + + + Select folder: + + + + + + + + + + + + Expand All + + + + + + + Collapse All + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + 0 + + + 0 + + + false + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Scan + + + + + + + + + QAbstractItemView::ExtendedSelection + + + + + + + Options + + + + + + Open the torrents without asking any questions. + + + Show all files + + + + + + + + + + + .. + + + Open file + + + + + + .. + + + Delete on disk + + + Ctrl+Del + + + + + + .. + + + Copy to clipboard + + + Ctrl+C + + + + + + KUrlRequester + QWidget +
kurlrequester.h
+ 1 +
+
+ + +
diff --git a/plugins/scripting/CMakeLists.txt b/plugins/scripting/CMakeLists.txt new file mode 100644 index 0000000..ea8d3cf --- /dev/null +++ b/plugins/scripting/CMakeLists.txt @@ -0,0 +1,35 @@ +add_subdirectory(scripts) + +add_library(ktorrent_scripting MODULE) + +target_sources(ktorrent_scripting PRIVATE + api/scriptingmodule.cpp + api/scriptablegroup.cpp + scriptingplugin.cpp + scriptmanager.cpp + scriptmodel.cpp + scriptdelegate.cpp + script.cpp) + +ki18n_wrap_ui(ktorrent_scripting scriptproperties.ui) + +kcoreaddons_desktop_to_json(ktorrent_scripting ktorrent_scripting.desktop) + +target_link_libraries( + ktorrent_scripting + ktcore + Qt5::Core + KF5::Torrent + KF5::Archive + KF5::ConfigCore + KF5::CoreAddons + KF5::I18n + KF5::IconThemes + KF5::ItemViews + KF5::KIOWidgets + KF5::KrossCore + KF5::XmlGui +) + +install(TARGETS ktorrent_scripting DESTINATION ${KTORRENT_PLUGIN_INSTALL_DIR} ) +install(FILES ktorrent_scriptingui.rc DESTINATION ${KXMLGUI_INSTALL_DIR}/ktorrent ) diff --git a/plugins/scripting/api/scriptablegroup.cpp b/plugins/scripting/api/scriptablegroup.cpp new file mode 100644 index 0000000..c134e11 --- /dev/null +++ b/plugins/scripting/api/scriptablegroup.cpp @@ -0,0 +1,37 @@ +/* + SPDX-FileCopyrightText: 2008 Joris Guisson + SPDX-FileCopyrightText: 2008 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "scriptablegroup.h" +#include +#include +#include +#include + +using namespace bt; + +namespace kt +{ +ScriptableGroup::ScriptableGroup(const QString &name, const QString &icon, const QString &path, Kross::Object::Ptr script, DBus *api) + : kt::Group(name, MIXED_GROUP, path) + , script(script) + , api(api) +{ + setIconByName(icon); +} + +ScriptableGroup::~ScriptableGroup() +{ +} + +bool ScriptableGroup::isMember(bt::TorrentInterface *tor) +{ + QVariantList args; + args << tor->getInfoHash().toString(); + QVariant ret = script->callMethod(QStringLiteral("isMember"), args); + return ret.toBool(); +} + +} diff --git a/plugins/scripting/api/scriptablegroup.h b/plugins/scripting/api/scriptablegroup.h new file mode 100644 index 0000000..997d4ea --- /dev/null +++ b/plugins/scripting/api/scriptablegroup.h @@ -0,0 +1,34 @@ +/* + SPDX-FileCopyrightText: 2008 Joris Guisson + SPDX-FileCopyrightText: 2008 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KTAPISCRIPTABLEGROUP_H +#define KTAPISCRIPTABLEGROUP_H + +#include +#include + +namespace kt +{ +class DBus; + +/** + Group which uses objects in a script to determine if a torrent is a member of the group. +*/ +class ScriptableGroup : public kt::Group +{ + Kross::Object::Ptr script; + DBus *api; + +public: + ScriptableGroup(const QString &name, const QString &icon, const QString &path, Kross::Object::Ptr script, DBus *api); + ~ScriptableGroup() override; + + bool isMember(bt::TorrentInterface *tor) override; +}; + +} + +#endif diff --git a/plugins/scripting/api/scriptingmodule.cpp b/plugins/scripting/api/scriptingmodule.cpp new file mode 100644 index 0000000..2bb447a --- /dev/null +++ b/plugins/scripting/api/scriptingmodule.cpp @@ -0,0 +1,139 @@ +/* + SPDX-FileCopyrightText: 2008 Joris Guisson + SPDX-FileCopyrightText: 2008 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include +#include + +#include +#include + +#include "scriptablegroup.h" +#include "scriptingmodule.h" +#include +#include +#include + +namespace kt +{ +ScriptingModule::ScriptingModule(GUIInterface *gui, CoreInterface *core, QObject *parent) + : QObject(parent) + , gui(gui) + , core(core) +{ +} + +ScriptingModule::~ScriptingModule() +{ +} + +QString ScriptingModule::scriptsDir() const +{ + QStringList dirs = QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("ktorrent/scripts"), QStandardPaths::LocateDirectory); + if (dirs.count() == 0) + return QString(); + + QString ret = dirs.front(); + if (!ret.endsWith(bt::DirSeparator())) + ret += bt::DirSeparator(); + + return ret; +} + +QString ScriptingModule::scriptDir(const QString &script) const +{ + QStringList dirs = + QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("ktorrent/scripts/") + script, QStandardPaths::LocateDirectory); + if (dirs.count() == 0) + return QString(); + + QString ret = dirs.front(); + if (!ret.endsWith(bt::DirSeparator())) + ret += bt::DirSeparator(); + + return ret; +} + +QString ScriptingModule::readConfigEntry(const QString &group, const QString &name, const QString &default_value) +{ + KConfigGroup g = KSharedConfig::openConfig()->group(group); + return g.readEntry(name, default_value); +} + +bool ScriptingModule::readConfigEntryBool(const QString &group, const QString &name, bool default_value) +{ + KConfigGroup g = KSharedConfig::openConfig()->group(group); + return g.readEntry(name, default_value); +} + +int ScriptingModule::readConfigEntryInt(const QString &group, const QString &name, int default_value) +{ + KConfigGroup g = KSharedConfig::openConfig()->group(group); + return g.readEntry(name, default_value); +} + +float ScriptingModule::readConfigEntryFloat(const QString &group, const QString &name, float default_value) +{ + KConfigGroup g = KSharedConfig::openConfig()->group(group); + return g.readEntry(name, default_value); +} + +void ScriptingModule::writeConfigEntry(const QString &group, const QString &name, const QString &value) +{ + KConfigGroup g = KSharedConfig::openConfig()->group(group); + g.writeEntry(name, value); +} + +void ScriptingModule::writeConfigEntryBool(const QString &group, const QString &name, bool value) +{ + KConfigGroup g = KSharedConfig::openConfig()->group(group); + g.writeEntry(name, value); +} + +void ScriptingModule::writeConfigEntryInt(const QString &group, const QString &name, int value) +{ + KConfigGroup g = KSharedConfig::openConfig()->group(group); + g.writeEntry(name, value); +} + +void ScriptingModule::writeConfigEntryFloat(const QString &group, const QString &name, float value) +{ + KConfigGroup g = KSharedConfig::openConfig()->group(group); + g.writeEntry(name, value); +} + +void ScriptingModule::syncConfig(const QString &group) +{ + KConfigGroup g = KSharedConfig::openConfig()->group(group); + g.sync(); +} + +QObject *ScriptingModule::createTimer(bool single_shot) +{ + QTimer *t = new QTimer(this); + t->setSingleShot(single_shot); + return t; +} + +bool ScriptingModule::addGroup(const QString &name, const QString &icon, const QString &path, Kross::Object::Ptr obj) +{ + ScriptableGroup *g = new ScriptableGroup(name, icon, path, obj, core->getExternalInterface()); + kt::GroupManager *gman = core->getGroupManager(); + gman->addDefaultGroup(g); + sgroups.insert(name, g); + return true; +} + +void ScriptingModule::removeGroup(const QString &name) +{ + if (!sgroups.contains(name)) + return; + + kt::GroupManager *gman = core->getGroupManager(); + ScriptableGroup *g = sgroups[name]; + sgroups.remove(name); + gman->removeDefaultGroup(g); +} +} diff --git a/plugins/scripting/api/scriptingmodule.h b/plugins/scripting/api/scriptingmodule.h new file mode 100644 index 0000000..245194f --- /dev/null +++ b/plugins/scripting/api/scriptingmodule.h @@ -0,0 +1,67 @@ +/* + SPDX-FileCopyrightText: 2008 Joris Guisson + SPDX-FileCopyrightText: 2008 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#ifndef KTSCRIPTINGMODULE_H +#define KTSCRIPTINGMODULE_H + +#include "scriptablegroup.h" +#include +#include + +namespace kt +{ +class GUIInterface; +class CoreInterface; + +/** + Additional functions to be used in scripts +*/ +class ScriptingModule : public QObject +{ + Q_OBJECT +public: + ScriptingModule(GUIInterface *gui, CoreInterface *core, QObject *parent); + ~ScriptingModule() override; + +public Q_SLOTS: + /// Get the scripts directory + QString scriptsDir() const; + + /// Get the data directory of a script + QString scriptDir(const QString &script) const; + + /// Read a config entry + QString readConfigEntry(const QString &group, const QString &name, const QString &default_value); + int readConfigEntryInt(const QString &group, const QString &name, int default_value); + float readConfigEntryFloat(const QString &group, const QString &name, float default_value); + bool readConfigEntryBool(const QString &group, const QString &name, bool default_value); + + /// Write a config entry + void writeConfigEntry(const QString &group, const QString &name, const QString &value); + void writeConfigEntryInt(const QString &group, const QString &name, int value); + void writeConfigEntryFloat(const QString &group, const QString &name, float value); + void writeConfigEntryBool(const QString &group, const QString &name, bool value); + + /// Sync a group + void syncConfig(const QString &group); + + /// Create a timer + QObject *createTimer(bool single_shot); + + /// Add a new scriptable group + bool addGroup(const QString &name, const QString &icon, const QString &path, Kross::Object::Ptr obj); + + /// Remove a previously added group + void removeGroup(const QString &name); + +private: + GUIInterface *gui; + CoreInterface *core; + QMap sgroups; +}; + +} + +#endif diff --git a/plugins/scripting/ktorrent_scripting.desktop b/plugins/scripting/ktorrent_scripting.desktop new file mode 100644 index 0000000..1a95419 --- /dev/null +++ b/plugins/scripting/ktorrent_scripting.desktop @@ -0,0 +1,108 @@ +[Desktop Entry] +Name=Scripting +Name[bg]=Скриптиране +Name[bs]=Skriptovanje +Name[ca]=Crea scripts +Name[ca@valencia]=Crea scripts +Name[cs]=Skriptování +Name[da]=Scripting +Name[de]=Skripte +Name[el]=Σενάρια +Name[en_GB]=Scripting +Name[es]=Programación +Name[et]=Skriptid +Name[fi]=Skriptaus +Name[fr]=Langage de scripts +Name[ga]=Scriptiú +Name[gl]=Scripts +Name[hu]=Parancsfájlkezelés +Name[ia]=Scripting +Name[is]=Skriftun +Name[it]=Script +Name[ja]=スクリプティング +Name[kk]=Скриптті жазу +Name[km]=ការ​ស្គ្រីប +Name[ko]=스크립팅 +Name[lt]=Scenarijai +Name[lv]=Skriptēšana +Name[nb]=Skripting +Name[nds]=Skripten +Name[nl]=Scripting +Name[nn]=Skripting +Name[pl]=Skrypty +Name[pt]=Programação +Name[pt_BR]=Scripts +Name[ro]=Scriptare +Name[ru]=Сценарии +Name[si]=විධානාවලි +Name[sk]=Skriptovanie +Name[sl]=Skriptno izvajanje +Name[sq]=Skriptimi +Name[sr]=Скрипте +Name[sr@ijekavian]=Скрипте +Name[sr@ijekavianlatin]=Skripte +Name[sr@latin]=Skripte +Name[sv]=Skripthantering +Name[tr]=Betikleme +Name[ug]=قوليازما +Name[uk]=Запис сценаріїв +Name[x-test]=xxScriptingxx +Name[zh_CN]=脚本 +Name[zh_TW]=文稿 +Comment=Enables Kross scripting support +Comment[bg]=Включване на поддръжка на скриптиране на Kross +Comment[bs]=Omogućava podršku za skriptovanje Krosom +Comment[ca]=Habilita l'ús per a crear scripts del Kross +Comment[ca@valencia]=Habilita l'ús per a crear scripts del Kross +Comment[cs]=Podpora skriptování Kross +Comment[da]=Aktiverer understøttelse af Kross-scripting +Comment[de]=Aktiviert die Unterstützung für Kross-Skripte +Comment[el]=Ενεργοποίηση υποστήριξης σεναρίων Kross +Comment[en_GB]=Enables Kross scripting support +Comment[es]=Activa el uso de scripts con Kross +Comment[et]=Krossi skriptimise toetus +Comment[fi]=Ottaa käyttöön Kross-skriptaustuen +Comment[fr]=Active la prise en charge du langage de scripts Kross +Comment[ga]=Breiseán a chumasaíonn scriptiú Kross +Comment[gl]=Permite o uso de scripts escritos en Kross. +Comment[hu]=A Kross parancsfájlok támogatásának engedélyezése +Comment[is]=Virkja stuðning við Kross skriftun +Comment[it]=Abilita il supporto script Kross +Comment[ja]=Kross スクリプティングサポートを有効にします +Comment[kk]=Kross скриптін рұқсат ету +Comment[km]=អនុញ្ញាត​ការ​គាំទ្រ​ការ​ស្គ្រីប Kross +Comment[ko]=Kross 스크립팅 지원 사용 +Comment[lt]=Įgalina kryžminį scenarijų palaikymą +Comment[lv]=Kross skriptēšanas spraudnis +Comment[nb]=Skru på støtte for Kross skripting +Comment[nds]=Ünnerstütten för Kross-Skripten +Comment[nl]=Schakelt ondersteuning voor kross-scripting in +Comment[nn]=Slår på støtte for Kross-skripting +Comment[pl]=Włącza obsługę skryptową Kross +Comment[pt]=Activa o suporte de programação com o Kross +Comment[pt_BR]=Habilita suporte à scripts do Kross +Comment[ro]=Activează suportul pentru scripturi Kross +Comment[ru]=Модуль поддержки сценариев Kross в KTorrent +Comment[si]=Kross විධානාවලි සහාය සක්‍රීය කරයි +Comment[sk]=Povolí podporu Kross scripting +Comment[sl]=Omogoči podporo za pisanje skript Kross +Comment[sr]=Додаје подршку за скриптовање Кросом +Comment[sr@ijekavian]=Додаје подршку за скриптовање Кросом +Comment[sr@ijekavianlatin]=Dodaje podršku za skriptovanje Krossom +Comment[sr@latin]=Dodaje podršku za skriptovanje Krossom +Comment[sv]=Aktiverar stöd för skripthantering med Kross +Comment[tr]=Kross betikleme desteğini etkinleştirir +Comment[uk]=Уможливлює підтримку Kross +Comment[x-test]=xxEnables Kross scripting supportxx +Comment[zh_CN]=启用 Kross 脚本支持 +Comment[zh_TW]=Kross 文稿支援 +Type=Service +X-KDE-Library=ktscriptingplugin +X-KDE-PluginInfo-Author=Joris Guisson +X-KDE-PluginInfo-Email=joris.guisson@gmail.com +X-KDE-PluginInfo-Name=ScriptingPlugin +X-KDE-PluginInfo-Version=0.1 +X-KDE-PluginInfo-Website=http://kde.org/applications/internet/ktorrent/ +X-KDE-PluginInfo-License=GPL +X-KDE-PluginInfo-EnabledByDefault=false +Icon=text-x-script diff --git a/plugins/scripting/ktorrent_scriptingui.rc b/plugins/scripting/ktorrent_scriptingui.rc new file mode 100644 index 0000000..7ad609d --- /dev/null +++ b/plugins/scripting/ktorrent_scriptingui.rc @@ -0,0 +1,27 @@ + + + + + Scripting + + + + + + + + + + + Scripting + + + + + + + + + + + diff --git a/plugins/scripting/script.cpp b/plugins/scripting/script.cpp new file mode 100644 index 0000000..75a8332 --- /dev/null +++ b/plugins/scripting/script.cpp @@ -0,0 +1,158 @@ +/* + SPDX-FileCopyrightText: 2008 Joris Guisson + SPDX-FileCopyrightText: 2008 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include +#include + +#include +#include +#include +#include + +#include "script.h" +#include +#include + +using namespace bt; + +namespace kt +{ +Script::Script(QObject *parent) + : QObject(parent) + , action(nullptr) + , executing(false) + , can_be_removed(true) +{ +} + +Script::Script(const QString &file, QObject *parent) + : QObject(parent) + , file(file) + , action(nullptr) + , executing(false) + , can_be_removed(true) +{ +} + +Script::~Script() +{ + stop(); +} + +bool Script::loadFromDesktopFile(const QString &dir, const QString &desktop_file) +{ + KDesktopFile df(dir + desktop_file); + // check if everything is OK + if (df.readType().trimmed() != QStringLiteral("KTorrentScript")) + return false; + + info.name = df.readName(); + info.comment = df.readComment(); + info.icon = df.readIcon(); + + KConfigGroup g = df.group("Desktop Entry"); + info.author = g.readEntry("X-KTorrent-Script-Author", QString()); + info.email = g.readEntry("X-KTorrent-Script-Email", QString()); + info.website = g.readEntry("X-KTorrent-Script-Website", QString()); + info.license = g.readEntry("X-KTorrent-Script-License", QString()); + file = g.readEntry("X-KTorrent-Script-File", QString()); + if (file.isEmpty() || !bt::Exists(dir + file)) // the script file must exist + return false; + + file = dir + file; + return true; +} + +bool Script::executeable() const +{ + return bt::Exists(file) && !Kross::Manager::self().interpreternameForFile(file).isNull(); +} + +bool Script::execute() +{ + if (!bt::Exists(file) || action) + return false; + + QMimeDatabase db; + QMimeType mt = db.mimeTypeForFile(file); + QString name = QFileInfo(file).fileName(); + action = new Kross::Action(this, name); + action->setText(name); + action->setDescription(name); + action->setFile(file); + action->setIconName(mt.iconName()); + QString interpreter = Kross::Manager::self().interpreternameForFile(file); + if (interpreter.isNull()) { + delete action; + action = nullptr; + return false; + } else { + action->setInterpreter(interpreter); + Kross::Manager::self().actionCollection()->addAction(file, action); + action->trigger(); + executing = true; + return true; + } +} + +void Script::stop() +{ + if (!executing) + return; + + // Call unload function if the script has one + if (action->functionNames().contains(QStringLiteral("unload"))) { + QVariantList args; + action->callFunction(QStringLiteral("unload"), args); + } + + Kross::ActionCollection *col = Kross::Manager::self().actionCollection(); + col->removeAction(action->file()); + action->deleteLater(); + action = nullptr; + executing = false; +} + +QString Script::name() const +{ + if (!info.name.isEmpty()) + return info.name; + else if (action) + return action->name(); + else + return QFileInfo(file).fileName(); +} + +QString Script::iconName() const +{ + QMimeDatabase db; + if (!info.icon.isEmpty()) + return info.icon; + else if (action) + return action->iconName(); + else + return db.mimeTypeForFile(file).iconName(); +} + +bool Script::hasConfigure() const +{ + if (!action) + return false; + + QStringList functions = action->functionNames(); + return functions.contains(QStringLiteral("configure")); +} + +void Script::configure() +{ + if (!action) + return; + + QVariantList args; + action->callFunction(QStringLiteral("configure"), args); +} + +} diff --git a/plugins/scripting/script.h b/plugins/scripting/script.h new file mode 100644 index 0000000..4ba1d71 --- /dev/null +++ b/plugins/scripting/script.h @@ -0,0 +1,132 @@ +/* + SPDX-FileCopyrightText: 2008 Joris Guisson + SPDX-FileCopyrightText: 2008 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KTSCRIPT_H +#define KTSCRIPT_H + +#include + +namespace Kross +{ +class Action; +} + +namespace kt +{ +/** + Keeps track of a script +*/ +class Script : public QObject +{ + Q_OBJECT +public: + Script(QObject *parent); + Script(const QString &file, QObject *parent); + ~Script() override; + + struct MetaInfo { + QString name; + QString comment; + QString icon; + QString author; + QString email; + QString website; + QString license; + + bool valid() const + { + return !name.isEmpty() && !comment.isEmpty() && !icon.isEmpty() && !author.isEmpty() && !license.isEmpty(); + } + }; + + /** + * Load the script from a desktop file + * @param dir THe directory the desktop file is in + * @param desktop_file The desktop file itself (relative to dir) + * @return true upon success + */ + bool loadFromDesktopFile(const QString &dir, const QString &desktop_file); + + /** + * Load and execute the script + * @return true upon success + */ + bool execute(); + + /// Is the script executeable (i.e. is the interpreter not installed) + bool executeable() const; + + /** + * Stop the script + */ + void stop(); + + /// Is the script running + bool running() const + { + return executing; + } + + /// Get the name of the script + QString name() const; + + /// Get the icon name of the script + QString iconName() const; + + /// Get the file + QString scriptFile() const + { + return file; + } + + /// Get the package directory, this returns an empty string if the script is just a file + QString packageDirectory() const + { + return package_directory; + } + + /// Set the package directory + void setPackageDirectory(const QString &dir) + { + package_directory = dir; + } + + /// Get the meta info of the script + const MetaInfo &metaInfo() const + { + return info; + } + + /// Does the script has a configure function + bool hasConfigure() const; + + /// Call the configure function of the script + void configure(); + + /// Whether or not the script can be removed + bool removable() const + { + return can_be_removed; + } + + /// Set the script to be removable or not + void setRemovable(bool on) + { + can_be_removed = on; + } + +private: + QString file; + Kross::Action *action; + bool executing; + MetaInfo info; + bool can_be_removed; + QString package_directory; +}; + +} + +#endif diff --git a/plugins/scripting/scriptdelegate.cpp b/plugins/scripting/scriptdelegate.cpp new file mode 100644 index 0000000..68c91b9 --- /dev/null +++ b/plugins/scripting/scriptdelegate.cpp @@ -0,0 +1,178 @@ +/* + SPDX-FileCopyrightText: 2009 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include + +#include +#include +#include +#include + +#include "scriptdelegate.h" +#include "scriptmodel.h" + +#define MARGIN 5 + +namespace kt +{ +ScriptDelegate::ScriptDelegate(QAbstractItemView *parent) + : KWidgetItemDelegate(parent, parent) + , check_box(new QCheckBox) + , push_button(new QPushButton) +{ +} + +ScriptDelegate::~ScriptDelegate() +{ + delete check_box; + delete push_button; +} + +QFont ScriptDelegate::titleFont(const QFont &base) const +{ + QFont font(base); + font.setBold(true); + return font; +} + +void ScriptDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const +{ + if (!index.isValid()) + return; + + int x_offset = check_box->sizeHint().width(); + + painter->save(); + QApplication::style()->drawPrimitive(QStyle::PE_PanelItemViewItem, &option, painter, nullptr); + + int iconSize = option.rect.height() - MARGIN * 2; + + QString icon = index.model()->data(index, Qt::DecorationRole).toString(); + KIconLoader::States state = option.state & QStyle::State_Enabled ? KIconLoader::DefaultState : KIconLoader::DisabledState; + QPixmap pixmap = KIconLoader::global()->loadIcon(icon, KIconLoader::Desktop, iconSize, state); + + int x = MARGIN + option.rect.left() + x_offset; + painter->drawPixmap(QRect(x, MARGIN + option.rect.top(), iconSize, iconSize), pixmap, QRect(0, 0, iconSize, iconSize)); + + x = MARGIN * 2 + iconSize + option.rect.left() + x_offset; + QRect contentsRect(x, MARGIN + option.rect.top(), option.rect.width() - MARGIN * 3 - iconSize - x_offset, option.rect.height() - MARGIN * 2); + + int lessHorizontalSpace = MARGIN * 2 + push_button->sizeHint().width(); + contentsRect.setWidth(contentsRect.width() - lessHorizontalSpace); + + QPalette::ColorGroup cg = QPalette::Active; + if (!(option.state & QStyle::State_Enabled)) + cg = QPalette::Inactive; + + if (option.state & QStyle::State_Selected) + painter->setPen(option.palette.color(cg, QPalette::HighlightedText)); + else + painter->setPen(option.palette.color(cg, QPalette::WindowText)); + + painter->save(); + painter->save(); + QFont font = titleFont(option.font); + QFontMetrics fmTitle(font); + painter->setFont(font); + QString text = index.model()->data(index, Qt::DisplayRole).toString(); + painter->drawText(contentsRect, Qt::AlignLeft | Qt::AlignTop, fmTitle.elidedText(text, Qt::ElideRight, contentsRect.width())); + painter->restore(); + + QString comment = index.model()->data(index, ScriptModel::CommentRole).toString(); + painter->drawText(contentsRect, Qt::AlignLeft | Qt::AlignBottom, option.fontMetrics.elidedText(comment, Qt::ElideRight, contentsRect.width())); + + painter->restore(); + painter->restore(); +} + +QSize ScriptDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const +{ + QFont font = titleFont(option.font); + QFontMetrics fm(font); + + int w = std::max(fm.horizontalAdvance(index.model()->data(index, Qt::DisplayRole).toString()), + option.fontMetrics.horizontalAdvance(index.model()->data(index, ScriptModel::CommentRole).toString())); + int h = std::max(KIconLoader::SizeMedium + MARGIN * 2, fm.height() + option.fontMetrics.height() + MARGIN * 2); + return QSize(w + KIconLoader::SizeMedium, h); +} + +QList ScriptDelegate::createItemWidgets(const QModelIndex &index) const +{ + Q_UNUSED(index) + QList widgets; + + QCheckBox *enabled_check = new QCheckBox; + connect(enabled_check, &QCheckBox::clicked, this, &ScriptDelegate::toggled); + + QPushButton *about_button = new QPushButton; + about_button->setIcon(QIcon::fromTheme(QStringLiteral("dialog-information"))); + connect(about_button, &QPushButton::clicked, this, &ScriptDelegate::aboutClicked); + + QPushButton *configure_button = new QPushButton; + configure_button->setIcon(QIcon::fromTheme(QStringLiteral("configure"))); + connect(configure_button, &QPushButton::clicked, this, &ScriptDelegate::settingsClicked); + + QList blocked; + blocked << QEvent::MouseButtonPress << QEvent::MouseButtonRelease << QEvent::MouseButtonDblClick; + setBlockedEventTypes(enabled_check, blocked); + setBlockedEventTypes(about_button, blocked); + setBlockedEventTypes(configure_button, blocked); + + widgets << enabled_check << configure_button << about_button; + return widgets; +} + +void ScriptDelegate::updateItemWidgets(const QList widgets, const QStyleOptionViewItem &option, const QPersistentModelIndex &index) const +{ + QCheckBox *check_box = static_cast(widgets[0]); + check_box->resize(check_box->sizeHint()); + int x = MARGIN; + check_box->move(x, option.rect.height() / 2 - check_box->sizeHint().height() / 2); + + QPushButton *about_button = static_cast(widgets[2]); + QSize about_size_hint = about_button->sizeHint(); + about_button->resize(about_size_hint); + x = option.rect.width() - MARGIN - about_size_hint.width(); + about_button->move(x, option.rect.height() / 2 - about_size_hint.height() / 2); + + QPushButton *configure_button = static_cast(widgets[1]); + QSize configure_size_hint = configure_button->sizeHint(); + configure_button->resize(configure_size_hint); + x = option.rect.width() - MARGIN * 2 - configure_size_hint.width() - about_size_hint.width(); + configure_button->move(x, option.rect.height() / 2 - configure_size_hint.height() / 2); + + if (!index.isValid()) { + check_box->setVisible(false); + about_button->setVisible(false); + configure_button->setVisible(false); + } else { + check_box->setChecked(index.model()->data(index, Qt::CheckStateRole).toBool()); + check_box->setEnabled(true); + configure_button->setVisible(true); + configure_button->setEnabled(index.model()->data(index, ScriptModel::ConfigurableRole).toBool()); + } +} + +void ScriptDelegate::aboutClicked() +{ + QModelIndex index = focusedIndex(); + QAbstractItemModel *model = (QAbstractItemModel *)index.model(); + model->setData(index, 0, ScriptModel::AboutRole); +} + +void ScriptDelegate::settingsClicked() +{ + QModelIndex index = focusedIndex(); + QAbstractItemModel *model = (QAbstractItemModel *)index.model(); + model->setData(index, 0, ScriptModel::ConfigureRole); +} + +void ScriptDelegate::toggled(bool on) +{ + QModelIndex index = focusedIndex(); + QAbstractItemModel *model = (QAbstractItemModel *)index.model(); + model->setData(index, on, Qt::CheckStateRole); +} +} diff --git a/plugins/scripting/scriptdelegate.h b/plugins/scripting/scriptdelegate.h new file mode 100644 index 0000000..5fcfa6a --- /dev/null +++ b/plugins/scripting/scriptdelegate.h @@ -0,0 +1,44 @@ +/* + SPDX-FileCopyrightText: 2009 Joris Guisson + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef KT_SCRIPTDELEGATE_H +#define KT_SCRIPTDELEGATE_H + +#include +#include +#include + +#include + +namespace kt +{ +class ScriptDelegate : public KWidgetItemDelegate +{ + Q_OBJECT +public: + ScriptDelegate(QAbstractItemView *parent); + ~ScriptDelegate() override; + + void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override; + QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override; + QList createItemWidgets(const QModelIndex &index) const override; + void updateItemWidgets(const QList widgets, const QStyleOptionViewItem &option, const QPersistentModelIndex &index) const override; + +private: + QFont titleFont(const QFont &baseFont) const; + +private Q_SLOTS: + void toggled(bool on); + void aboutClicked(); + void settingsClicked(); + +private: + QCheckBox *check_box; + QPushButton *push_button; +}; + +} + +#endif // KT_SCRIPTDELEGATE_H diff --git a/plugins/scripting/scriptingplugin.cpp b/plugins/scripting/scriptingplugin.cpp new file mode 100644 index 0000000..fec1309 --- /dev/null +++ b/plugins/scripting/scriptingplugin.cpp @@ -0,0 +1,222 @@ +/* + SPDX-FileCopyrightText: 2008 Joris Guisson + SPDX-FileCopyrightText: 2008 Ivan Vasic + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "api/scriptingmodule.h" +#include "script.h" +#include "scriptingplugin.h" +#include "scriptmanager.h" +#include "scriptmodel.h" + +K_PLUGIN_FACTORY_WITH_JSON(ktorrent_scripting, "ktorrent_scripting.json", registerPlugin();) + +using namespace bt; + +namespace kt +{ +ScriptingPlugin::ScriptingPlugin(QObject *parent, const QVariantList &args) + : Plugin(parent) +{ + Q_UNUSED(args); +} + +ScriptingPlugin::~ScriptingPlugin() +{ +} + +void ScriptingPlugin::load() +{ + // make sure script dir exists + QString script_dir = kt::DataDir() + QStringLiteral("scripts") + bt::DirSeparator(); + if (!bt::Exists(script_dir)) + bt::MakeDir(script_dir, true); + + LogSystemManager::instance().registerSystem(i18n("Scripting"), SYS_SCR); + model = new ScriptModel(this); + // add the KTorrent object + Kross::Manager::self().addObject(getCore()->getExternalInterface(), QStringLiteral("KTorrent")); + Kross::Manager::self().addObject(new ScriptingModule(getGUI(), getCore(), this), QStringLiteral("KTScriptingPlugin")); + loadScripts(); + + Out(SYS_SCR | LOG_DEBUG) << "Supported interpreters : " << endl; + const QStringList interpreters = Kross::Manager::self().interpreters(); + for (const QString &s : interpreters) + Out(SYS_SCR | LOG_DEBUG) << s << endl; + + sman = new ScriptManager(model, nullptr); + connect(sman, &ScriptManager::addScript, this, &ScriptingPlugin::addScript); + connect(sman, &ScriptManager::removeScript, this, &ScriptingPlugin::removeScript); + connect(model, &ScriptModel::showPropertiesDialog, sman, qOverload