diff --git a/.gitmodules b/.gitmodules index 9511f4c..1996bf3 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,9 +1,6 @@ [submodule "gpodder-core"] path = gpodder-core url = git://github.com/gpodder/gpodder-core.git -[submodule "gpodder-ui-qml"] - path = gpodder-ui-qml - url = git://github.com/gpodder/gpodder-ui-qml.git [submodule "podcastparser"] path = podcastparser url = git://github.com/gpodder/podcastparser.git diff --git a/gpodder-ui-qml b/gpodder-ui-qml deleted file mode 160000 index ed036ab..0000000 --- a/gpodder-ui-qml +++ /dev/null @@ -1 +0,0 @@ -Subproject commit ed036ab40776889f716d781ec08e7ce15604f404 diff --git a/gpodder-ui-qml/common/GPodderAutoFire.qml b/gpodder-ui-qml/common/GPodderAutoFire.qml new file mode 100644 index 0000000..8fdf5df --- /dev/null +++ b/gpodder-ui-qml/common/GPodderAutoFire.qml @@ -0,0 +1,45 @@ + +/** + * + * gPodder QML UI Reference Implementation + * Copyright (c) 2015, Thomas Perl + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH + * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, + * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM + * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR + * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR + * PERFORMANCE OF THIS SOFTWARE. + * + */ + +import QtQuick 2.0 + +Timer { + property int triggerCount: 0 + property int initialInterval: 1500 + property int autoFireInterval: 200 + + signal fired() + + interval: triggerCount > 1 ? autoFireInterval : initialInterval + + repeat: true + triggeredOnStart: true + + onRunningChanged: { + if (!running) { + triggerCount = 0 + } + } + + onTriggered: { + triggerCount += 1 + fired() + } +} diff --git a/gpodder-ui-qml/common/GPodderCore.qml b/gpodder-ui-qml/common/GPodderCore.qml new file mode 100644 index 0000000..7920d32 --- /dev/null +++ b/gpodder-ui-qml/common/GPodderCore.qml @@ -0,0 +1,97 @@ + +/** + * + * gPodder QML UI Reference Implementation + * Copyright (c) 2013, 2014, Thomas Perl + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH + * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, + * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM + * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR + * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR + * PERFORMANCE OF THIS SOFTWARE. + * + */ + +import QtQuick 2.0 +import io.thp.pyotherside 1.3 + + +Python { + id: py + + property string progname: 'gpodder' + property bool ready: false + property bool refreshing: false + + property string coreversion + property string uiversion + property string parserversion + + signal downloadProgress(int episode_id, real progress) + signal playbackProgress(int episode_id, real progress) + signal podcastListChanged() + signal updatingPodcast(int podcast_id) + signal updatedPodcast(var podcast) + signal episodeListChanged(int podcast_id) + signal updatedEpisode(var episode) + signal updateStats() + signal configChanged(string key, var value) + + Component.onCompleted: { + setHandler('hello', function (coreversion, uiversion, parserversion) { + py.coreversion = coreversion; + py.uiversion = uiversion; + py.parserversion = parserversion; + + console.log('gPodder Core ' + py.coreversion); + console.log('gPodder QML UI ' + py.uiversion); + console.log('Podcastparser ' + py.parserversion); + console.log('PyOtherSide ' + py.pluginVersion()); + console.log('Python ' + py.pythonVersion()); + }); + + setHandler('download-progress', py.downloadProgress); + setHandler('playback-progress', py.playbackProgress); + setHandler('podcast-list-changed', py.podcastListChanged); + setHandler('updating-podcast', py.updatingPodcast); + setHandler('updated-podcast', py.updatedPodcast); + setHandler('refreshing', function(v) { py.refreshing = v; }); + setHandler('episode-list-changed', py.episodeListChanged); + setHandler('updated-episode', py.updatedEpisode); + setHandler('update-stats', py.updateStats); + setHandler('config-changed', py.configChanged); + + addImportPath(Qt.resolvedUrl('../..')); + + // Load the Python side of things + importModule('main', function() { + py.call('main.initialize', [py.progname], function() { + py.ready = true; + }); + }); + } + + function setConfig(key, value) { + py.call('main.set_config_value', [key, value]); + } + + function getConfig(key, callback) { + py.call('main.get_config_value', [key], function (result) { + callback(result); + }); + } + + onReceived: { + console.log('unhandled message: ' + data); + } + + onError: { + console.log('Python failure: ' + traceback); + } +} diff --git a/gpodder-ui-qml/common/GPodderDirectorySearchModel.qml b/gpodder-ui-qml/common/GPodderDirectorySearchModel.qml new file mode 100644 index 0000000..b4a7bfa --- /dev/null +++ b/gpodder-ui-qml/common/GPodderDirectorySearchModel.qml @@ -0,0 +1,40 @@ + +/** + * + * gPodder QML UI Reference Implementation + * Copyright (c) 2013, 2014, Thomas Perl + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH + * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, + * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM + * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR + * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR + * PERFORMANCE OF THIS SOFTWARE. + * + */ + +import QtQuick 2.0 + +ListModel { + id: directorySearchModel + property string provider + + function search(query, callback) { + clear(); + + py.call('main.get_directory_entries', [directorySearchModel.provider, query], function (result) { + for (var i=0; i + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH + * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, + * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM + * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR + * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR + * PERFORMANCE OF THIS SOFTWARE. + * + */ + +import QtQuick 2.0 + +import 'util.js' as Util +import 'constants.js' as Constants + +ListModel { + id: episodeListModel + + property int podcast_id: -1 + + property bool cached: false + + property var queries: ({ + All: '', + Fresh: 'new or downloading', + Downloaded: 'downloaded or downloading', + UnplayedDownloads: 'downloaded and not played', + FinishedDownloads: 'downloaded and finished', + HideDeleted: 'not deleted', + Deleted: 'deleted', + ShortDownloads: 'downloaded and min > 0 and min < 10', + TextSearch: '/.*%s.*/i' + }) + + property var filters: ([ + { label: qsTr("All"), query: episodeListModel.queries.All, hasParameters:false }, + { label: qsTr("Fresh"), query: episodeListModel.queries.Fresh, hasParameters:false }, + { label: qsTr("Downloaded"), query: episodeListModel.queries.Downloaded, hasParameters:false }, + { label: qsTr("Unplayed downloads"), query: episodeListModel.queries.UnplayedDownloads, hasParameters:false }, + { label: qsTr("Finished downloads"), query: episodeListModel.queries.FinishedDownloads, hasParameters:false }, + { label: qsTr("Hide deleted"), query: episodeListModel.queries.HideDeleted, hasParameters:false }, + { label: qsTr("Deleted episodes"), query: episodeListModel.queries.Deleted, hasParameters:false }, + { label: qsTr("Short downloads (< 10 min)"), query: episodeListModel.queries.ShortDownloads, hasParameters:false }, + { label: qsTr("Includes Text: %s"), query: episodeListModel.queries.TextSearch, hasParameters:true, searchTerm: ""} + ]) + + property bool ready: false + property int currentFilterIndex: 0 + property string currentCustomQuery: queries.All + + Component.onCompleted: { + // Request filter, then load episodes + py.call('main.get_config_value', ['ui.qml.episode_list.filter_eql'], function (result) { + console.debug("got query from storage: '",result,"'") + setQueryFromUpdate(result); + }); + } + + function getFormattedLabel(i){ + if(i === undefined){ + i = currentFilterIndex + } + console.assert(i>=0&&i=0; i--) { + callback(get(i)); + } + } + + function setQueryFromIndex(index) { + setQueryEx(filters[index].query,true); + } + + function setQueryFromUpdate(query) { + setQueryEx(query, false); + } + + function setQuery(query) { + setQueryEx(query, true); + } + + function setQueryEx(query, update) { + console.info("changing query from '",currentCustomQuery,"' to '",query,"',") + if(query === currentCustomQuery && !filters[currentFilterIndex].hasParameters){ + console.debug("filter already selected, skipping..."); + return; + } + for (var i=0; i + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH + * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, + * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM + * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR + * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR + * PERFORMANCE OF THIS SOFTWARE. + * + */ + +import QtQuick 2.0 + +import 'util.js' as Util + +Connections { + target: py + + onDownloadProgress: { + Util.updateModelWith(episodeListModel, 'id', episode_id, + {'progress': progress}); + } + onPlaybackProgress: { + Util.updateModelWith(episodeListModel, 'id', episode_id, + {'playbackProgress': progress}); + } + onUpdatedEpisode: { + for (var i=0; i + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH + * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, + * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM + * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR + * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR + * PERFORMANCE OF THIS SOFTWARE. + * + */ + +import QtQuick 2.0 + +Item { + property bool emulatingAndroid: false + + property bool android: (typeof(gpodderAndroid) !== 'undefined') || emulatingAndroid + + property bool needsBackButton: !android + + property bool toolbarOnTop: true + property bool invertedToolbar: toolbarOnTop + property bool titleInToolbar: toolbarOnTop + + property bool floatingPlayButton: true + property bool hideDisabledMenu: true +} diff --git a/gpodder-ui-qml/common/GPodderPlayback.qml b/gpodder-ui-qml/common/GPodderPlayback.qml new file mode 100644 index 0000000..f39da99 --- /dev/null +++ b/gpodder-ui-qml/common/GPodderPlayback.qml @@ -0,0 +1,276 @@ + +/** + * + * gPodder QML UI Reference Implementation + * Copyright (c) 2013, 2014, Thomas Perl + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH + * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, + * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM + * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR + * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR + * PERFORMANCE OF THIS SOFTWARE. + * + */ + +import QtQuick 2.0 +import QtMultimedia 5.0 + +MediaPlayer { + id: player + + property int episode: 0 + property string episode_title: '' + property var episode_chapters: ([]) + property string podcast_title: '' + property string cover_art: '' + property string episode_art: '' + signal playerCreated() + + property var queue: ([]) + signal queueUpdated() + property bool isPlaying: playbackState == MediaPlayer.PlayingState + property bool isPaused: playbackState == MediaPlayer.PausedState + property bool isStopped: playbackState == MediaPlayer.StoppedState + + property bool inhibitPositionEvents: false + property bool seekAfterPlay: false + property int seekTargetSeconds: 0 + property int lastPosition: 0 + property int lastDuration: 0 + property int playedFrom: 0 + + property var androidConnections: Connections { + target: platform.android ? gpodderAndroid : null + + onAudioBecomingNoisy: { + if (playbackState === MediaPlayer.PlayingState) { + pause(); + } + } + } + + function togglePause() { + if (playbackState === MediaPlayer.PlayingState) { + pause(); + } else if (playbackState === MediaPlayer.PausedState) { + play(); + } + } + + function enqueueEpisode(episode_id, callback) { + py.call('main.show_episode', [episode_id], function (episode) { + if (episode_id != player.episode && !queue.some(function (queued) { + return queued.episode_id === episode_id; + })) { + queue.push({ + episode_id: episode_id, + title: episode.title, + }); + queueUpdated(); + } + + if (callback !== undefined) { + callback(); + } + }); + } + + function clearQueue() { + while (queue.length) { + queue.shift() + } + queueUpdated() + } + + function playbackEpisode(episode_id) { + if (episode == episode_id) { + // If the episode is already loaded, just start playing + play(); + return; + } + + // First, make sure we stop any seeking / position update events + sendPositionToCore(lastPosition); + player.inhibitPositionEvents = true; + player.stop(); + + py.call('main.play_episode', [episode_id], function (episode) { + if (episode.video) { + player.inhibitPositionEvents = false; + Qt.openUrlExternally(episode.source); + return; + } + + // Load media / prepare and start playback + var old_episode = player.episode; + player.episode = episode_id; + player.episode_title = episode.title; + player.episode_chapters = episode.chapters; + player.podcast_title = episode.podcast_title; + player.cover_art = episode.cover_art; + player.episode_art = episode.episode_art; + var source = episode.source; + if (source.indexOf('/') === 0) { + player.source = 'file://' + source; + } else { + player.source = source; + } + player.seekTargetSeconds = episode.position; + seekAfterPlay = true; + + // Notify interested parties that the player is now active + if (old_episode == 0) { + player.playerCreated(); + } + + player.play(); + }); + } + + function seekAndSync(target_position) { + sendPositionToCore(lastPosition); + seek(target_position); + playedFrom = target_position; + savePlaybackAfterStopTimer.restart(); + } + + onPlaybackStateChanged: { + if (playbackState == MediaPlayer.PlayingState) { + if (!seekAfterPlay) { + player.playedFrom = position; + } + } else { + sendPositionToCore(lastPosition); + savePlaybackAfterStopTimer.restart(); + } + } + + function flushToDisk() { + py.call('main.save_playback_state', []); + } + + property var durationChoices: ([5, 15, 30, 45, 60]) + + function startSleepTimer(seconds) { + sleepTimer.running = false; + sleepTimer.secondsRemaining = seconds; + sleepTimer.running = true; + } + + function stopSleepTimer() { + sleepTimer.running = false; + sleepTimer.secondsRemaining = 0; + } + + property bool sleepTimerRunning: sleepTimer.running + property int sleepTimerRemaining: sleepTimer.secondsRemaining + + property var sleepTimer: Timer { + property int secondsRemaining: 0 + + interval: 1000 + repeat: true + onTriggered: { + secondsRemaining -= 1; + + if (secondsRemaining <= 0) { + player.pause(); + running = false; + } + } + } + + property var nextInQueueTimer: Timer { + interval: 500 + + repeat: false + + onTriggered: { + if (queue.length > 0) { + playbackEpisode(queue.shift().episode_id); + player.queueUpdated(); + } + } + } + + function jumpToQueueIndex(index) { + playbackEpisode(removeQueueIndex(index).episode_id); + } + + function removeQueueIndex(index) { + var result = queue.splice(index, 1)[0]; + player.queueUpdated(); + return result; + } + + onStatusChanged: { + if (status === MediaPlayer.EndOfMedia) { + nextInQueueTimer.start(); + } + } + + property var savePlaybackPositionTimer: Timer { + // Save position every minute during playback + interval: 60 * 1000 + repeat: true + running: player.isPlaying + onTriggered: player.flushToDisk(); + } + + property var savePlaybackAfterStopTimer: Timer { + // Save position shortly after every seek and pause event + interval: 5 * 1000 + repeat: false + onTriggered: player.flushToDisk(); + } + + property var seekAfterPlayTimer: Timer { + interval: 100 + repeat: true + running: player.isPlaying && player.seekAfterPlay + + onTriggered: { + var targetPosition = player.seekTargetSeconds * 1000; + if (Math.abs(player.position - targetPosition) < 10 * interval) { + // We have seeked properly + player.inhibitPositionEvents = false; + player.seekAfterPlay = false; + } else { + // Try to seek to the target position + player.seek(targetPosition); + player.playedFrom = targetPosition; + } + } + } + + function sendPositionToCore(positionToSend) { + if (episode != 0 && !inhibitPositionEvents) { + var begin = playedFrom / 1000; + var end = positionToSend / 1000; + var duration = ((lastDuration > 0) ? lastDuration : 0) / 1000; + var diff = end - begin; + + // Only send playback events if they are 2 seconds or longer + // (all other events might just be seeking events or wrong ones) + if (diff >= 2) { + py.call('main.report_playback_event', [episode, begin, end, duration]); + } + } + } + + onPositionChanged: { + if (isPlaying && !inhibitPositionEvents) { + lastPosition = position; + lastDuration = duration; + + // Directly update the playback progress in the episode list + py.playbackProgress(episode, position / duration); + } + } +} diff --git a/gpodder-ui-qml/common/GPodderPodcastListModel.qml b/gpodder-ui-qml/common/GPodderPodcastListModel.qml new file mode 100644 index 0000000..216d8fb --- /dev/null +++ b/gpodder-ui-qml/common/GPodderPodcastListModel.qml @@ -0,0 +1,38 @@ + +/** + * + * gPodder QML UI Reference Implementation + * Copyright (c) 2013, 2014, Thomas Perl + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH + * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, + * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM + * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR + * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR + * PERFORMANCE OF THIS SOFTWARE. + * + */ + +import QtQuick 2.0 + +import 'util.js' as Util + +ListModel { + id: podcastListModel + property bool initialized: false + + function reload() { + initialized = false; + py.call('main.load_podcasts', [], function (podcasts) { + Util.updateModelFrom(podcastListModel, podcasts); + if(!initialized) { + initialized = true; + } + }); + } +} diff --git a/gpodder-ui-qml/common/GPodderPodcastListModelConnections.qml b/gpodder-ui-qml/common/GPodderPodcastListModelConnections.qml new file mode 100644 index 0000000..3f6d797 --- /dev/null +++ b/gpodder-ui-qml/common/GPodderPodcastListModelConnections.qml @@ -0,0 +1,49 @@ + +/** + * + * gPodder QML UI Reference Implementation + * Copyright (c) 2014, Thomas Perl + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH + * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, + * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM + * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR + * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR + * PERFORMANCE OF THIS SOFTWARE. + * + */ + +import QtQuick 2.0 + +Connections { + target: py + + onPodcastListChanged: { + podcastListModel.reload(); + } + + onUpdatingPodcast: { + for (var i=0; i + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH + * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, + * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM + * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR + * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR + * PERFORMANCE OF THIS SOFTWARE. + * + */ + +var layout = { + header: { + height: 100, /* page header height */ + }, + item: { + height: 80, /* podcast/episode item height */ + }, + coverart: 80, /* cover art size */ + padding: 10, /* padding of items left/right */ +}; + +var colors = { + download: '#7ac224', /* download green */ + select: '#7f5785', /* gpodder dark purple */ + fresh: '#815c86', /* gpodder purple */ + playback: '#729fcf', /* playback blue */ + destructive: '#cf424f', /* destructive actions */ + + toolbar: '#d0d0d0', + toolbarText: '#333333', + toolbarDisabled: '#666666', + + inverted: { + toolbar: '#815c86', + toolbarText: '#ffffff', + toolbarDisabled: '#aaffffff', + }, + + page: '#dddddd', + dialog: '#dddddd', + dialogBackground: '#aa000000', + text: '#333333', /* text color */ + dialogText: '#333333', + highlight: '#433b67', + dialogHighlight: '#433b67', + secondaryHighlight: '#605885', + area: '#cccccc', + dialogArea: '#d0d0d0', + toolbarArea: '#bbbbbb', + placeholder: '#666666', + + //page: '#000000', + //text: '#ffffff', /* text color */ + //highlight: Qt.lighter('#433b67', 1.2), + //secondaryHighlight: Qt.lighter('#605885', 1.2), + //area: '#333333', + //placeholder: '#aaaaaa', + + background: '#948db3', + secondaryBackground: '#d0cce1', +}; + +var font = 'Source Sans Pro'; + +var state = { + normal: 0, + downloaded: 1, + deleted: 2, +}; + diff --git a/gpodder-ui-qml/common/util.js b/gpodder-ui-qml/common/util.js new file mode 100644 index 0000000..3cb2d20 --- /dev/null +++ b/gpodder-ui-qml/common/util.js @@ -0,0 +1,87 @@ + +/** + * + * gPodder QML UI Reference Implementation + * Copyright (c) 2013, Thomas Perl + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH + * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, + * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM + * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR + * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR + * PERFORMANCE OF THIS SOFTWARE. + * + */ + +function updateModelFrom(model, data) { + for (var i=0; i data.length) { + model.remove(model.count-1); + } +} + +function updateModelWith(model, key, value, update) { + for (var row=0; row 0 ? (h < 10 ? '0' + h : h) + ':' : '' + var ms = (m < 10 ? '0' + m : m) + ':' + (s < 10 ? '0' + s : s) + + return hh + ms +} + +function formatPosition(position,duration) { + return formatDuration(position) + " / " + formatDuration(duration) +} + +// Call a Python function and disable item until the function returns +function disableUntilReturn(item, py, func, args) { + item.enabled = false; + py.call(func, args, function() { + item.enabled = true; + }); +} + +function format(s, d) { + return s.replace(/{([^}]*)}/g, function (m, k) { + return (k in d) ? d[k] : m; + }); +} + +function atMostOnce(callback) { + var called = false; + return function () { + if (!called) { + called = true; + callback(); + } + }; +} diff --git a/gpodder-ui-qml/makefile b/gpodder-ui-qml/makefile new file mode 100644 index 0000000..ad654d7 --- /dev/null +++ b/gpodder-ui-qml/makefile @@ -0,0 +1,21 @@ +PROJECT := gpodder-ui-qml +VERSION := 4.11.1 + +all: + @echo "" + @echo " make release ..... Build source release" + @echo "" + +release: dist/$(PROJECT)-$(VERSION).tar.gz + +dist/$(PROJECT)-$(VERSION).tar.gz: + mkdir -p dist + git archive --format=tar --prefix=$(PROJECT)-$(VERSION)/ $(VERSION) | gzip >$@ + +clean: + find . -name '__pycache__' -exec rm -rf {} + + +distclean: clean + rm -rf dist + +.PHONY: all release clean diff --git a/gpodder-ui-qml/setup.cfg b/gpodder-ui-qml/setup.cfg new file mode 100644 index 0000000..68859ad --- /dev/null +++ b/gpodder-ui-qml/setup.cfg @@ -0,0 +1,2 @@ +[pep8] +max-line-length = 120 diff --git a/qml/AllEpisodesPage.qml b/qml/AllEpisodesPage.qml index 7625bab..59deed4 100644 --- a/qml/AllEpisodesPage.qml +++ b/qml/AllEpisodesPage.qml @@ -30,11 +30,6 @@ Page { onStatusChanged: pgst.handlePageStatusChange(status) - Component.onCompleted: { - episodeListModel.setQuery(episodeListModel.queries.Downloaded); - episodeListModel.reload(); - } - BusyIndicator { visible: !episodeListModel.ready running: visible @@ -47,15 +42,29 @@ Page { PullDownMenu { EpisodeListFilterItem { id: filterItem; model: episodeListModel } + + MenuItem { + text: py.refreshing ? qsTr("Checking for new episodes...") : qsTr("Check for new episodes") + enabled: podcastListModel.count > 0 && !py.refreshing + onClicked: { + episodeListModel.ready = false; + py.call('main.check_for_episodes',[],function (){ + episodeListModel.reload(); + }); + } + } } VerticalScrollDecorator { flickable: filteredEpisodesList } header: PageHeader { - title: qsTr("Episodes: ") + filterItem.currentFilter + title: episodeListModel.ready, "Episodes: "+ episodeListModel.getFormattedLabel() + } + + model: GPodderEpisodeListModel { + id: episodeListModel } - model: GPodderEpisodeListModel { id: episodeListModel } GPodderEpisodeListModelConnections {} section.property: 'section' diff --git a/qml/EpisodeFilterDialog.qml b/qml/EpisodeFilterDialog.qml index 2bd3689..639e029 100644 --- a/qml/EpisodeFilterDialog.qml +++ b/qml/EpisodeFilterDialog.qml @@ -36,9 +36,10 @@ Page { } model: { + var filters = filterSelector.model.filters; var result = []; - for (var i in filterSelector.model.filters) { - result.push(filterSelector.model.filters[i].label); + for (var i in filters) { + result.push(filterSelector.model.getFormattedLabel(i)) } return result; } @@ -48,10 +49,26 @@ Page { highlighted: down || (index == filterSelector.selectedIndex) - onClicked: { - filterSelector.model.currentFilterIndex = index; - filterSelector.model.reload(); - pageStack.pop(); + onClicked: { + console.debug("Selected filter id ", index); + var filter = filterSelector.model.filters[index]; + if(filter.hasParameters){ + pageStack.push('TextInputDialog.qml', { + inputLabel: qsTr("Search for"), + initialValue: filter.searchTerm, + acceptText: qsTr("Search"), + acceptDestinationAction: PageStackAction.Pop, + acceptDestination: pageStack.previousPage(), + callback: function (searchTerm) { + console.debug("got search term:",searchTerm) + filter.searchTerm=searchTerm; + filterSelector.model.setQueryFromIndex(index); + } + }, true); + }else{ + filterSelector.model.setQueryFromIndex(index); + pageStack.pop(); + } } Label { diff --git a/qml/EpisodeListFilterItem.qml b/qml/EpisodeListFilterItem.qml index 02d85d9..043e2ca 100644 --- a/qml/EpisodeListFilterItem.qml +++ b/qml/EpisodeListFilterItem.qml @@ -28,8 +28,8 @@ MenuItem { id: episodeListFilterItem property var model - property string currentFilter: model.filters[model.currentFilterIndex].label + property string currentFilter: model.getFormattedLabel() - text: qsTr("Filter: ") + currentFilter + text: qsTr("Change Filter") onClicked: pageStack.push('EpisodeFilterDialog.qml', { model: model }); } diff --git a/qml/PodcastItem.qml b/qml/PodcastItem.qml index be2479a..a86c10f 100644 --- a/qml/PodcastItem.qml +++ b/qml/PodcastItem.qml @@ -53,7 +53,7 @@ ListItem { onClicked: { podcastItem.closeMenu(); var ctx = { py: py, id: id }; - pageStack.push('RenameDialog.qml', { + pageStack.push('TextInputDialog.qml', { activityName: qsTr("Rename podcast"), affirmativeAction: qsTr("Rename"), inputLabel: qsTr("Podcast name"), diff --git a/qml/PodcastsPage.qml b/qml/PodcastsPage.qml index dfabb23..9039628 100644 --- a/qml/PodcastsPage.qml +++ b/qml/PodcastsPage.qml @@ -43,7 +43,7 @@ Page { } MenuItem { - text: qsTr("Filter episodes") + text: qsTr("Episodelist") onClicked: pgst.loadPage('AllEpisodesPage.qml'); } @@ -83,12 +83,19 @@ Page { model: podcastListModel + BusyIndicator { + size: BusyIndicatorSize.Large + anchors.centerIn: parent + visible: !podcastListModel.initialized + running: visible + } + delegate: PodcastItem { onClicked: pgst.loadPage('EpisodesPage.qml', {'podcast_id': id, 'title': title}); } ViewPlaceholder { - enabled: podcastListModel.count === 0 && podcastListModel.firstRun + enabled: podcastListModel.count === 0 && podcastListModel.initialized text: qsTr("No subscriptions") } } diff --git a/qml/RenameDialog.qml b/qml/TextInputDialog.qml similarity index 78% rename from qml/RenameDialog.qml rename to qml/TextInputDialog.qml index e616ff8..fdb003c 100644 --- a/qml/RenameDialog.qml +++ b/qml/TextInputDialog.qml @@ -22,7 +22,7 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 Dialog { - id: rename + id: textInput allowedOrientations: Orientation.All property string activityName @@ -32,26 +32,25 @@ Dialog { property var callback canAccept: input.text != '' - onAccepted: rename.callback(input.text); + onAccepted: textInput.callback(input.text); Column { anchors.fill: parent DialogHeader { - title: rename.activityName - acceptText: rename.affirmativeAction + title: textInput.activityName + acceptText: textInput.affirmativeAction } TextField { id: input width: parent.width - label: rename.inputLabel + label: textInput.inputLabel placeholderText: label text: initialValue focus: enabled - enabled: rename.status == PageStatus.Active - //inputMethodHints: Qt.ImhUrlCharactersOnly | Qt.ImhNoPredictiveText | Qt.ImhNoAutoUppercase - EnterKey.onClicked: rename.accept() + enabled: textInput.status == PageStatus.Active + EnterKey.onClicked: textInput.accept() } } } diff --git a/translations/harbour-org.gpodder.sailfish-bg.ts b/translations/harbour-org.gpodder.sailfish-bg.ts index 7161f74..3ceea1d 100644 --- a/translations/harbour-org.gpodder.sailfish-bg.ts +++ b/translations/harbour-org.gpodder.sailfish-bg.ts @@ -32,14 +32,19 @@ AllEpisodesPage - - Episodes: - Епизоди: + + Checking for new episodes... + Проверява се за нови... - + + Check for new episodes + Проверяване за нови епизоди + + + No episodes found - Няма намерени епизоди + Няма намерени епизоди @@ -93,6 +98,16 @@ Filter episode list Филтър + + + Search for + + + + + Search + Търсене + EpisodeItem @@ -141,8 +156,8 @@ EpisodeListFilterItem - Filter: - Филтър: + Change Filter + @@ -171,45 +186,50 @@ GPodderEpisodeListModel - + All Всички - + Fresh Нови и неизтеглени - + Downloaded Изтеглени - + Unplayed downloads Непускани епизоди - + Finished downloads Изслушани епизоди - + Hide deleted Без изтритите - + Deleted episodes Изтрити епизоди - + Short downloads (< 10 min) Кратки епизоди (< 10 мин) + + + Includes Text: %s + + ImportOPML @@ -464,47 +484,47 @@ PodcastsPage - + Settings Настройки - - Filter episodes - Филтриране на епизодите + + Episodelist + - + Checking for new episodes... Проверява се за нови... - + Check for new episodes Проверяване за нови епизоди - + Add new podcast Добавяне на нов подкаст - + Discover new podcasts Откриване на нови подкасти - + Import/Export OPML Внос/износ на OPML - + Subscriptions Абонаменти - + No subscriptions Няма абонаменти diff --git a/translations/harbour-org.gpodder.sailfish-de.ts b/translations/harbour-org.gpodder.sailfish-de.ts index 34dfea1..6c785f5 100644 --- a/translations/harbour-org.gpodder.sailfish-de.ts +++ b/translations/harbour-org.gpodder.sailfish-de.ts @@ -32,14 +32,19 @@ AllEpisodesPage - - Episodes: - Episoden: + + Checking for new episodes... + Suche neue Episoden... - + + Check for new episodes + Auf neue Episoden prüfen + + + No episodes found - Keine Episoden gefunden + Keine Episoden gefunden @@ -93,6 +98,16 @@ Filter episode list Filtere Episodenliste + + + Search for + + + + + Search + Suche + EpisodeItem @@ -141,8 +156,8 @@ EpisodeListFilterItem - Filter: - Filter: + Change Filter + @@ -171,45 +186,50 @@ GPodderEpisodeListModel - + All Alle - + Fresh Neu - + Downloaded Heruntergeladen - + Unplayed downloads Heruntergeladen & Nicht abgespielt - + Finished downloads Heruntergeladen & Fertig - + Hide deleted Gelöschte ausblenden - + Deleted episodes Gelöscht - + Short downloads (< 10 min) Heruntergeladen & Länge < 10min + + + Includes Text: %s + + ImportOPML @@ -464,47 +484,47 @@ PodcastsPage - + Settings Einstellungen - - Filter episodes - Episoden filtern + + Episodelist + - + Checking for new episodes... Suche neue Episoden... - + Check for new episodes Auf neue Episoden prüfen - + Add new podcast Podcast hinzufügen - + Discover new podcasts Neue Podcasts entdecken - + Import/Export OPML OPML Import/Export - + Subscriptions Abonnierte Podcasts - + No subscriptions Keine abonnierten Podcasts diff --git a/translations/harbour-org.gpodder.sailfish-es.ts b/translations/harbour-org.gpodder.sailfish-es.ts index 71cc6e0..e301287 100644 --- a/translations/harbour-org.gpodder.sailfish-es.ts +++ b/translations/harbour-org.gpodder.sailfish-es.ts @@ -32,14 +32,19 @@ AllEpisodesPage - - Episodes: - Episodios: + + Checking for new episodes... + Buscando nuevos episodios... + + + + Check for new episodes + Buscar nuevos episodios - + No episodes found - No se han encontrado episodios + No se han encontrado episodios @@ -93,6 +98,16 @@ Filter episode list Lista de filtros + + + Search for + + + + + Search + Buscar + EpisodeItem @@ -141,8 +156,8 @@ EpisodeListFilterItem - Filter: - Filtro: + Change Filter + @@ -171,45 +186,50 @@ GPodderEpisodeListModel - + All Todos - + Fresh Nuevos - + Downloaded Descargados - + Unplayed downloads Descargas sin reproducir - + Finished downloads Descargas reproducidas - + Hide deleted Ocultar borrados - + Deleted episodes Episodios borrados - + Short downloads (< 10 min) Descargas cortas ( < 10 min) + + + Includes Text: %s + + ImportOPML @@ -464,47 +484,47 @@ PodcastsPage - + Settings Ajustes - - Filter episodes - Filtro de episodios + + Episodelist + - + Checking for new episodes... Buscando nuevos episodios... - + Check for new episodes Buscar nuevos episodios - + Add new podcast Añadir nuevo podcast - + Discover new podcasts Descubrir nuevos podcasts - + Import/Export OPML Importar/Exportar OPML - + Subscriptions Suscripciones - + No subscriptions No hay suscripciones diff --git a/translations/harbour-org.gpodder.sailfish-it.ts b/translations/harbour-org.gpodder.sailfish-it.ts index 0ae311e..4311d1f 100644 --- a/translations/harbour-org.gpodder.sailfish-it.ts +++ b/translations/harbour-org.gpodder.sailfish-it.ts @@ -32,14 +32,19 @@ AllEpisodesPage - - Episodes: - Episodi: + + Checking for new episodes... + Controllo nuovi episodi... - + + Check for new episodes + Controlla nuovi episodi + + + No episodes found - Nessun episodio + Nessun episodio @@ -93,6 +98,16 @@ Filter episode list Filtra lista episodi + + + Search for + + + + + Search + Cerca + EpisodeItem @@ -141,8 +156,8 @@ EpisodeListFilterItem - Filter: - Filtro: + Change Filter + @@ -171,45 +186,50 @@ GPodderEpisodeListModel - + All Tutti - + Fresh Nuovi - + Downloaded Scaricati - + Unplayed downloads Download non riprodotti - + Finished downloads Download completati - + Hide deleted Nascondi eliminati - + Deleted episodes Episodi eliminati - + Short downloads (< 10 min) Download corti (< 10 min) + + + Includes Text: %s + + ImportOPML @@ -464,47 +484,47 @@ PodcastsPage - + Settings Impostazioni - - Filter episodes - Filtra episodi + + Episodelist + - + Checking for new episodes... Controllo nuovi episodi... - + Check for new episodes Controlla nuovi episodi - + Add new podcast Aggiungi nuovo podcast - + Discover new podcasts Scopri nuovi podcast - + Import/Export OPML Importa/Esporta OPML - + Subscriptions iscrizioni - + No subscriptions Nessuna iscrizione diff --git a/translations/harbour-org.gpodder.sailfish-pl.ts b/translations/harbour-org.gpodder.sailfish-pl.ts index df4cb9a..61778b8 100644 --- a/translations/harbour-org.gpodder.sailfish-pl.ts +++ b/translations/harbour-org.gpodder.sailfish-pl.ts @@ -32,14 +32,19 @@ AllEpisodesPage - - Episodes: - Odcinki: + + Checking for new episodes... + Szukanie nowych odcinków... - + + Check for new episodes + Szukaj nowych odcinków + + + No episodes found - Brak odcinków + Brak odcinków @@ -93,6 +98,16 @@ Filter episode list Filtrowanie listy odcinków + + + Search for + + + + + Search + Szukaj + EpisodeItem @@ -141,8 +156,8 @@ EpisodeListFilterItem - Filter: - Filtr: + Change Filter + @@ -171,45 +186,50 @@ GPodderEpisodeListModel - + All Wszytskie - + Fresh Nowe - + Downloaded Pobrane - + Unplayed downloads Nieodtworzone pobrane - + Finished downloads Zakończone pobrania - + Hide deleted Ukryj usunięte - + Deleted episodes Usunięte odcinki - + Short downloads (< 10 min) Krótkie pobrane (< 10min) + + + Includes Text: %s + + ImportOPML @@ -464,47 +484,47 @@ PodcastsPage - + Settings Ustawienia - - Filter episodes - Filtr odcinków + + Episodelist + - + Checking for new episodes... Szukanie nowych odcinków... - + Check for new episodes Szukaj nowych odcinków - + Add new podcast Dodaj nowy podcast - + Discover new podcasts Odkryj nowe podcasty - + Import/Export OPML Import/Eksport OMPL - + Subscriptions Subskrypcje - + No subscriptions Brak subskrybcji diff --git a/translations/harbour-org.gpodder.sailfish-ru.ts b/translations/harbour-org.gpodder.sailfish-ru.ts index 6ace26a..e6564d2 100644 --- a/translations/harbour-org.gpodder.sailfish-ru.ts +++ b/translations/harbour-org.gpodder.sailfish-ru.ts @@ -32,14 +32,19 @@ AllEpisodesPage - - Episodes: - Выпуски: + + Checking for new episodes... + Проверка новых выпусков... - + + Check for new episodes + Проверить новые выпуски + + + No episodes found - Нет выпусков + Нет выпусков @@ -93,6 +98,16 @@ Filter episode list Фильтр списка эпизодов + + + Search for + + + + + Search + Искать + EpisodeItem @@ -141,8 +156,8 @@ EpisodeListFilterItem - Filter: - Фильтр: + Change Filter + @@ -171,45 +186,50 @@ GPodderEpisodeListModel - + All Все - + Fresh Последние - + Downloaded Скачанные - + Unplayed downloads Непрослушанные - + Finished downloads Завершенные закачки - + Hide deleted Скрыть удаленные - + Deleted episodes Удаленные выпуски - + Short downloads (< 10 min) Короткие (< 10 минут) + + + Includes Text: %s + + ImportOPML @@ -464,47 +484,47 @@ PodcastsPage - + Settings Настройка - - Filter episodes - Фильтр выпусков + + Episodelist + - + Checking for new episodes... Проверка новых выпусков... - + Check for new episodes Проверить новые выпуски - + Add new podcast Добавить подписку - + Discover new podcasts Поиск подкастов - + Import/Export OPML Импорт/экспорт OPML - + Subscriptions Подписки - + No subscriptions Нет подписок diff --git a/translations/harbour-org.gpodder.sailfish-sv.ts b/translations/harbour-org.gpodder.sailfish-sv.ts index fe5a47a..c4c95ed 100644 --- a/translations/harbour-org.gpodder.sailfish-sv.ts +++ b/translations/harbour-org.gpodder.sailfish-sv.ts @@ -32,14 +32,19 @@ AllEpisodesPage - - Episodes: - Avsnitt: + + Checking for new episodes... + Söker nya avsnitt... - + + Check for new episodes + Sök efter nya avsnitt + + + No episodes found - Inga avsnitt hittades + Inga avsnitt hittades @@ -93,6 +98,16 @@ Filter episode list Filtrera avsnittslistan + + + Search for + + + + + Search + Sök + EpisodeItem @@ -141,8 +156,8 @@ EpisodeListFilterItem - Filter: - Filter: + Change Filter + @@ -171,45 +186,50 @@ GPodderEpisodeListModel - + All Alla - + Fresh Nya - + Downloaded Nerladdat - + Unplayed downloads Ospelade nerladdningar - + Finished downloads Slutförda nerladdningar - + Hide deleted Dölj borttagna - + Deleted episodes Borttagna avsnitt - + Short downloads (< 10 min) Korta nerladdningar (<10 min) + + + Includes Text: %s + + ImportOPML @@ -464,47 +484,47 @@ PodcastsPage - + Settings Inställningar - - Filter episodes - Filtrera avsnitt + + Episodelist + - + Checking for new episodes... Söker nya avsnitt... - + Check for new episodes Sök efter nya avsnitt - + Add new podcast Lägg till ny podd - + Discover new podcasts Upptäck nya poddar - + Import/Export OPML Importera/Exportera OPML - + Subscriptions Prenumerationer - + No subscriptions Inga prenumerationer diff --git a/translations/harbour-org.gpodder.sailfish-zh_CN.ts b/translations/harbour-org.gpodder.sailfish-zh_CN.ts index 942d5ee..e585a20 100644 --- a/translations/harbour-org.gpodder.sailfish-zh_CN.ts +++ b/translations/harbour-org.gpodder.sailfish-zh_CN.ts @@ -32,14 +32,19 @@ AllEpisodesPage - - Episodes: - 剧集: + + Checking for new episodes... + 正在检测新剧集…… - + + Check for new episodes + 检测新剧集 + + + No episodes found - 没有找到剧集 + 没有找到剧集 @@ -93,6 +98,16 @@ Filter episode list 筛选剧集列表 + + + Search for + + + + + Search + 搜索 + EpisodeItem @@ -141,8 +156,8 @@ EpisodeListFilterItem - Filter: - 筛选: + Change Filter + @@ -171,45 +186,50 @@ GPodderEpisodeListModel - + All 全部 - + Fresh 新的 - + Downloaded 下载 - + Unplayed downloads 未播放的下载 - + Finished downloads 已完成的下载 - + Hide deleted 隐藏已删除 - + Deleted episodes 已删除剧集 - + Short downloads (< 10 min) 较短的下载(10分钟以内) + + + Includes Text: %s + + ImportOPML @@ -464,47 +484,47 @@ PodcastsPage - + Settings 设置 - - Filter episodes - 筛选剧集 + + Episodelist + - + Checking for new episodes... 正在检测新剧集…… - + Check for new episodes 检测新剧集 - + Add new podcast 添加新剧集 - + Discover new podcasts 发现新播客 - + Import/Export OPML 导入/导出 OPML - + Subscriptions 订阅 - + No subscriptions 无订阅 diff --git a/translations/harbour-org.gpodder.sailfish.ts b/translations/harbour-org.gpodder.sailfish.ts index 89cf9b6..5d12b7e 100644 --- a/translations/harbour-org.gpodder.sailfish.ts +++ b/translations/harbour-org.gpodder.sailfish.ts @@ -32,12 +32,17 @@ AllEpisodesPage - - Episodes: + + Checking for new episodes... + + + + + Check for new episodes - + No episodes found @@ -93,6 +98,16 @@ Filter episode list + + + Search for + + + + + Search + + EpisodeItem @@ -141,7 +156,7 @@ EpisodeListFilterItem - Filter: + Change Filter @@ -171,45 +186,50 @@ GPodderEpisodeListModel - + All - + Fresh - + Downloaded - + Unplayed downloads - + Finished downloads - + Hide deleted - + Deleted episodes - + Short downloads (< 10 min) + + + Includes Text: %s + + ImportOPML @@ -464,47 +484,47 @@ PodcastsPage - + Settings - - Filter episodes + + Episodelist - + Checking for new episodes... - + Check for new episodes - + Add new podcast - + Discover new podcasts - + Import/Export OPML - + Subscriptions - + No subscriptions