diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..cd470a6 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,89 @@ +project(mixcloud CXX) +cmake_minimum_required(VERSION 2.8.10) +set(CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake" "${CMAKE_MODULE_PATH}") + +# We require g++ 4.9, to avoid ABI breakage with earlier version. +set(cxx_version_required 4.9) +if("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU") + if (NOT CMAKE_CXX_COMPILER_VERSION MATCHES "^${cxx_version_required}") + message(FATAL_ERROR "g++ version must be ${cxx_version_required}!") + endif() +endif() + +# Set strict and naggy C++ compiler flags, and enable C++11 +add_definitions( + -fno-permissive + -std=c++11 + -pedantic + -Wall + -Wextra + -fPIC + -DQT_NO_KEYWORDS +) + +include(GNUInstallDirs) +find_package(PkgConfig) + +# We depend on Boost for string trimming +find_package( + Boost + REQUIRED +) + +# Search for our dependencies +pkg_check_modules( + SCOPE + libunity-scopes>=0.6.0 + net-cpp>=1.1.0 + REQUIRED +) + +find_package(Qt5Core REQUIRED) +include_directories(${Qt5Core_INCLUDE_DIRS}) + +# Add our dependencies to the include paths +include_directories( + "${CMAKE_SOURCE_DIR}/include" + ${Boost_INCLUDE_DIRS} + ${SCOPE_INCLUDE_DIRS} +) + +# Do not remove this line, its required for the correct functionality of the Ubuntu-SDK +set(UBUNTU_MANIFEST_PATH "manifest.json.in" CACHE INTERNAL "Tells QtCreator location and name of the manifest file") +set(UBUNTU_PROJECT_TYPE "Scope" CACHE INTERNAL "Tells QtCreator this is a Scope project") + +# Important project paths +set(CMAKE_INSTALL_PREFIX /) +set(SCOPE_INSTALL_DIR "/mixcloudscope") +set(GETTEXT_PACKAGE "mixcloud") + +# If we need to refer to the scope's name or package in code, these definitions will help +add_definitions(-DPACKAGE_NAME="com.ubuntu.developer.boghison.mixcloud") +add_definitions(-DSCOPE_NAME="com.ubuntu.developer.boghison.mixcloud_mixcloudscope") +add_definitions(-DGETTEXT_PACKAGE="${GETTEXT_PACKAGE}") + +#This command figures out the target architecture and puts it into the manifest file +execute_process( + COMMAND dpkg-architecture -qDEB_HOST_ARCH + OUTPUT_VARIABLE CLICK_ARCH + OUTPUT_STRIP_TRAILING_WHITESPACE +) + +configure_file(manifest.json.in ${CMAKE_CURRENT_BINARY_DIR}/manifest.json) + +# Install the click manifest +install(FILES ${CMAKE_CURRENT_BINARY_DIR}/manifest.json DESTINATION "/") +install(FILES "mixcloudscope.apparmor" DESTINATION "/") + +# Make this file show up in QtCreator +add_custom_target(hidden_files + ALL + SOURCES + manifest.json.in + "mixcloudscope.apparmor" +) + +# Add our main directories +add_subdirectory(src) +add_subdirectory(data) +add_subdirectory(po) diff --git a/CMakeLists.txt.user b/CMakeLists.txt.user new file mode 100644 index 0000000..86b00b1 --- /dev/null +++ b/CMakeLists.txt.user @@ -0,0 +1,337 @@ + + + + + + ProjectExplorer.Project.ActiveTarget + 0 + + + ProjectExplorer.Project.EditorSettings + + true + false + true + + Cpp + + CppGlobal + + + + QmlJS + + QmlJSGlobal + + + 2 + UTF-8 + false + 4 + false + 80 + true + true + 1 + true + false + 0 + true + 0 + 8 + true + 1 + true + true + true + false + + + + ProjectExplorer.Project.PluginSettings + + + + ProjectExplorer.Project.Target.0 + + Desktop + Desktop + {9f526637-a1a3-485f-b2b7-a75f980b8c4f} + 0 + 0 + 0 + + false + /home/boghison/Projects/build-MixCloud-Desktop-Default + + + + + false + false + true + Make + + CMakeProjectManager.MakeStep + + 1 + Build + + ProjectExplorer.BuildSteps.Build + + + + clean + + true + false + true + Make + + CMakeProjectManager.MakeStep + + 1 + Clean + + ProjectExplorer.BuildSteps.Clean + + 2 + false + + Default + Default + CMakeProjectManager.CMakeBuildConfiguration + + 1 + + + 0 + Deploy + + ProjectExplorer.BuildSteps.Deploy + + 1 + Deploy locally + + ProjectExplorer.DefaultDeployConfiguration + + 1 + + + + false + false + false + false + true + 0.01 + 10 + true + 1 + 25 + + 1 + true + false + true + valgrind + + 0 + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11 + 12 + 13 + 14 + + 2 + + + mixcloudscope + UbuntuProjectManager.UbuntuRunConfiguration.Scopemixcloudscope + 3768 + false + true + false + false + true + + 1 + + + + ProjectExplorer.Project.Target.1 + + Ubuntu Device (GCC armhf-ubuntu-sdk-14.10-utopic) + Ubuntu Device (GCC armhf-ubuntu-sdk-14.10-utopic) + {22bc7e68-9728-4455-b28b-2b16920f71c8} + 0 + 0 + 0 + + false + /home/boghison/Projects/build-MixCloud-Ubuntu_Device_GCC_armhf_ubuntu_sdk_14_10_utopic-Default + + + + + all + + false + false + true + Ubuntu SDK Make + + UbuntuProjectManager.UbuntuCMake.MakeStep + + 1 + Build + + ProjectExplorer.BuildSteps.Build + + + + clean + + true + false + true + Ubuntu SDK Make + + UbuntuProjectManager.UbuntuCMake.MakeStep + + 1 + Clean + + ProjectExplorer.BuildSteps.Clean + + 2 + false + + Default + Default + UbuntuProjectManager.UbuntuCMake.BuildConfiguration + + 1 + + + + true + UbuntuSDK Click build + + UbuntuProjectManager.ClickPackageStep + 1 + + + true + Upload files to Ubuntu Device + + UbuntuProjectManager.UploadStep + + /home/boghison/Projects/build-MixCloud-Ubuntu_Device_GCC_armhf_ubuntu_sdk_14_10_utopic-Default/com.ubuntu.developer.boghison.mixcloud_0.1_armhf.click + /usr/share/qtcreator/ubuntu/scripts/qtc_device_applaunch.py + + + 127.0.0.1 + 127.0.0.1 + + + /tmp + /tmp + + + /var/lib/schroot/chroots/click-ubuntu-sdk-14.10-armhf + /var/lib/schroot/chroots/click-ubuntu-sdk-14.10-armhf + + + 2014-11-17T16:46:12 + 2014-11-17T16:46:12 + + + 2 + Deploy + + ProjectExplorer.BuildSteps.Deploy + + 1 + Deploy to Ubuntu Device + + UbuntuProjectManager.DeployConfiguration + + 1 + + + + false + false + false + false + true + 0.01 + 10 + true + 1 + 25 + + 1 + true + false + true + valgrind + + 0 + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11 + 12 + 13 + 14 + + -1 + + + + false + %{buildDir} + Custom Executable + + ProjectExplorer.CustomExecutableRunConfiguration + 3768 + false + true + false + false + true + + 1 + + + + ProjectExplorer.Project.TargetCount + 2 + + + ProjectExplorer.Project.Updater.EnvironmentId + {933be325-2d5b-4358-a163-774b77eeb676} + + + ProjectExplorer.Project.Updater.FileVersion + 15 + + diff --git a/cmake/FindGMock.cmake b/cmake/FindGMock.cmake new file mode 100644 index 0000000..0fe55b7 --- /dev/null +++ b/cmake/FindGMock.cmake @@ -0,0 +1,13 @@ +# Build with system gmock and embedded gtest +set (GMOCK_INCLUDE_DIRS "/usr/include/gmock/include" CACHE PATH "gmock source include directory") +set (GMOCK_SOURCE_DIR "/usr/src/gmock" CACHE PATH "gmock source directory") +set (GTEST_INCLUDE_DIRS "${GMOCK_SOURCE_DIR}/gtest/include" CACHE PATH "gtest source include directory") + +add_subdirectory(${GMOCK_SOURCE_DIR} "${CMAKE_CURRENT_BINARY_DIR}/gmock") + +set(GTEST_LIBRARIES gtest) +set(GTEST_MAIN_LIBRARIES gtest_main) +set(GMOCK_LIBRARIES gmock gmock_main) + +set(GTEST_BOTH_LIBRARIES ${GTEST_LIBRARIES} ${GTEST_MAIN_LIBRARIES}) + diff --git a/cmake/FindXGettext.cmake b/cmake/FindXGettext.cmake new file mode 100644 index 0000000..0ab21a8 --- /dev/null +++ b/cmake/FindXGettext.cmake @@ -0,0 +1,131 @@ +# Copyright (C) 2013 Canonical Ltd +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License version 3 as +# published by the Free Software Foundation. +# +# 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 Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see . + +# This package provides macros that wrap the xgettext program. +# +# An example of common usage is: +# +# set( +# POT_FILE +# "${CMAKE_CURRENT_SOURCE_DIR}/${GETTEXT_PACKAGE}.pot" +# ) +# +# file( +# GLOB_RECURSE SRC_FILES +# RELATIVE ${CMAKE_SOURCE_DIR} +# ${SOURCE_DIR}/*.cpp +# ${SOURCE_DIR}/*.c +# ${SOURCE_DIR}/*.h +# ) +# +# xgettext_create_pot_file( +# ${POT_FILE} +# CPP +# QT +# INPUT ${SOURCES} +# WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} +# ADD_COMMENTS "TRANSLATORS" +# KEYWORDS "_" "N_" +# PACKAGE_NAME ${GETTEXT_PACKAGE} +# COPYRIGHT_HOLDER "Canonical Ltd." +# ) + +find_package(Gettext REQUIRED) + +find_program(XGETTEXT_EXECUTABLE xgettext) + +if(XGETTEXT_EXECUTABLE) + execute_process( + COMMAND ${XGETTEXT_EXECUTABLE} --version + OUTPUT_VARIABLE xgettext_version + ERROR_QUIET + OUTPUT_STRIP_TRAILING_WHITESPACE) + if (xgettext_version MATCHES "^xgettext \\(.*\\) [0-9]") + string( + REGEX REPLACE "^xgettext \\([^\\)]*\\) ([0-9\\.]+[^ \n]*).*" "\\1" + XGETTEXT_VERSION_STRING "${xgettext_version}" + ) + endif() + unset(xgettext_version) +endif() + +include(FindPackageHandleStandardArgs) + +find_package_handle_standard_args( + XGettext + REQUIRED_VARS XGETTEXT_EXECUTABLE + VERSION_VAR XGETTEXT_VERSION_STRING +) + +function(APPEND_EACH LISTNAME GLUE OUTPUT) + set(_tmp_list "") + foreach(VAL ${${LISTNAME}}) + list(APPEND _tmp_list "${GLUE}${VAL}") + endforeach(VAL ${${LISTNAME}}) + set(${OUTPUT} "${_tmp_list}" PARENT_SCOPE) +endfunction() + +function(XGETTEXT_CREATE_POT_FILE _potFile) + set(_options ALL QT CPP) + set(_oneValueArgs ADD_COMMENTS PACKAGE_NAME COPYRIGHT_HOLDER WORKING_DIRECTORY) + set(_multiValueArgs INPUT KEYWORDS) + + cmake_parse_arguments(_ARG "${_options}" "${_oneValueArgs}" "${_multiValueArgs}" ${ARGN}) + + set(_QT "") + if(_ARG_QT) + set(_QT "--qt") + endif() + + set(_CPP "") + if(_ARG_CPP) + set(_CPP "--c++") + endif() + + set(_KEYWORD "") + if(_ARG_KEYWORDS) + append_each(_ARG_KEYWORDS "--keyword=" _KEYWORD) + endif() + + set(_ADD_COMMENTS "") + if(_ARG_ADD_COMMENTS) + set(_ADD_COMMENTS --add-comments="${_ARG_ADD_COMMENTS}") + endif() + + set(_PACKAGE_NAME "") + if(_ARG_PACKAGE_NAME) + set(_PACKAGE_NAME --package-name="${_ARG_PACKAGE_NAME}") + endif() + + set(_COPYRIGHT_HOLDER "") + if(_ARG_COPYRIGHT_HOLDER) + set(_COPYRIGHT_HOLDER --copyright-holder="${_ARG_COPYRIGHT_HOLDER}") + endif() + + add_custom_command( + OUTPUT "${_potFile}" + COMMAND ${XGETTEXT_EXECUTABLE} --output="${_potFile}" ${_KEYWORD} ${_PACKAGE_NAME} ${_COPYRIGHT_HOLDER} ${_QT} ${_CPP} ${_ADD_COMMENTS} ${_ARG_INPUT} + WORKING_DIRECTORY ${_ARG_WORKING_DIRECTORY} + ) + + _GETTEXT_GET_UNIQUE_TARGET_NAME(_potFile _uniqueTargetName) + + if(_ARG_ALL) + add_custom_target(${_uniqueTargetName} ALL DEPENDS ${_potFile}) + else() + add_custom_target(${_uniqueTargetName} DEPENDS ${_potFile}) + endif() + +endfunction() + diff --git a/cmake/UseXGettext.cmake b/cmake/UseXGettext.cmake new file mode 100644 index 0000000..7562d91 --- /dev/null +++ b/cmake/UseXGettext.cmake @@ -0,0 +1,99 @@ +# Copyright (C) 2013 Canonical Ltd +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License version 3 as +# published by the Free Software Foundation. +# +# 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 Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see . + +# Example usage: +# +# include(UseXGettext) +# +# add_translations_directory(${GETTEXT_PACKAGE}) +# +# add_translations_catalog( +# GETTEXT_PACKAGE ${GETTEXT_PACKAGE} +# COPYRIGHT_HOLDER "Canonical Ltd." +# SOURCE_DIRECTORIES "${CMAKE_SOURCE_DIR}/src" +# ) + +cmake_minimum_required(VERSION 2.8.9) + +find_package(XGettext REQUIRED) + +macro(add_translations_directory GETTEXT_PACKAGE) + set( + _POT_FILE + "${CMAKE_CURRENT_SOURCE_DIR}/${GETTEXT_PACKAGE}.pot" + ) + + file( + GLOB _PO_FILES + ${CMAKE_CURRENT_SOURCE_DIR}/*.po + ) + + gettext_create_translations( + ${_POT_FILE} + ALL + ${_PO_FILES} + ) +endmacro(add_translations_directory) + +macro(add_translations_catalog) + set(_oneValueArgs GETTEXT_PACKAGE COPYRIGHT_HOLDER) + set(_multiValueArgs SOURCE_DIRECTORIES) + + cmake_parse_arguments(_ARG "" "${_oneValueArgs}" "${_multiValueArgs}" ${ARGN}) + + set(_GETTEXT_PACKAGE ${PROJECT}) + if(_ARG_GETTEXT_PACKAGE) + set(_GETTEXT_PACKAGE ${_ARG_GETTEXT_PACKAGE}) + endif() + + set( + _POT_FILE + "${CMAKE_CURRENT_SOURCE_DIR}/${_GETTEXT_PACKAGE}.pot" + ) + + add_custom_target (pot + COMMENT “Building translation catalog.” + DEPENDS ${_POT_FILE} + ) + + set(_SOURCES "") + + foreach(DIR ${_ARG_SOURCE_DIRECTORIES}) + file( + GLOB_RECURSE _DIR_SOURCES + RELATIVE ${CMAKE_SOURCE_DIR} + ${DIR}/*.cpp + ${DIR}/*.cc + ${DIR}/*.cxx + ${DIR}/*.vala + ${DIR}/*.c + ${DIR}/*.h + ) + set (_SOURCES ${_SOURCES} ${_DIR_SOURCES}) + endforeach() + + xgettext_create_pot_file( + ${_POT_FILE} + ALL + CPP + QT + INPUT ${_SOURCES} + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} + ADD_COMMENTS "TRANSLATORS" + KEYWORDS "_" "_:1,2" "N_" "N_:1,2" + PACKAGE_NAME ${_GETTEXT_PACKAGE} + COPYRIGHT_HOLDER ${_ARG_COPYRIGHT_HOLDER} + ) +endmacro() + diff --git a/data/CMakeLists.txt b/data/CMakeLists.txt new file mode 100644 index 0000000..1996497 --- /dev/null +++ b/data/CMakeLists.txt @@ -0,0 +1,22 @@ +configure_file( + "com.ubuntu.developer.boghison.mixcloud_mixcloudscope-settings.ini" + "${CMAKE_BINARY_DIR}/src/com.ubuntu.developer.boghison.mixcloud_mixcloudscope-settings.ini" +) +# Install the scope ini file +install( + FILES + "com.ubuntu.developer.boghison.mixcloud_mixcloudscope.ini" + "com.ubuntu.developer.boghison.mixcloud_mixcloudscope-settings.ini" + DESTINATION ${SCOPE_INSTALL_DIR} +) + +# Install the scope images +install( + FILES + "art.png" + "logo.svg" + "mixcloud.svg" + DESTINATION + "${SCOPE_INSTALL_DIR}" +) + diff --git a/data/art.png b/data/art.png new file mode 100644 index 0000000..ecf1cbd Binary files /dev/null and b/data/art.png differ diff --git a/data/com.ubuntu.developer.boghison.mixcloud_mixcloudscope-settings.ini b/data/com.ubuntu.developer.boghison.mixcloud_mixcloudscope-settings.ini new file mode 100644 index 0000000..7d46bf2 --- /dev/null +++ b/data/com.ubuntu.developer.boghison.mixcloud_mixcloudscope-settings.ini @@ -0,0 +1,9 @@ +[showNew] +type = boolean +defaultValue = true +displayName = Show New Cloudcasts + +[getExtra] +type = boolean +defaultValue = false +displayName = Use double requests to get extra metadata diff --git a/data/com.ubuntu.developer.boghison.mixcloud_mixcloudscope.ini b/data/com.ubuntu.developer.boghison.mixcloud_mixcloudscope.ini new file mode 100644 index 0000000..5bd22e6 --- /dev/null +++ b/data/com.ubuntu.developer.boghison.mixcloud_mixcloudscope.ini @@ -0,0 +1,13 @@ +[ScopeConfig] +DisplayName = MixCloud Scope +Description = This is a MixCloud scope +Art = art.png +Author = Firstname Lastname +Icon = icon.png + +[Appearance] +PageHeader.Logo = logo.svg +PageHeader.DividerColor = #33475f +ForegroundColor = #33475f +PreviewButtonColor = #33475f +BackgroundColor = #ffffff diff --git a/data/logo.svg b/data/logo.svg new file mode 100644 index 0000000..0d665d9 --- /dev/null +++ b/data/logo.svg @@ -0,0 +1,59 @@ + + + +image/svg+xml diff --git a/data/mixcloud.svg b/data/mixcloud.svg new file mode 100644 index 0000000..afe4285 --- /dev/null +++ b/data/mixcloud.svg @@ -0,0 +1,34 @@ + +image/svg+xml diff --git a/data/mixcloud.svg~ b/data/mixcloud.svg~ new file mode 100644 index 0000000..50ec4b6 --- /dev/null +++ b/data/mixcloud.svg~ @@ -0,0 +1,36 @@ + +image/svg+xml diff --git a/include/api/client.h b/include/api/client.h new file mode 100644 index 0000000..3b3eeee --- /dev/null +++ b/include/api/client.h @@ -0,0 +1,118 @@ +#ifndef API_CLIENT_H_ +#define API_CLIENT_H_ + +#include + +#include +#include +#include +#include +#include +#include + +#include + +namespace api { + +/** + * Provide a nice way to access the HTTP API. + * + * We don't want our scope's code to be mixed together with HTTP and JSON handling. + */ +class Client { +public: + + /** + * Information about a City + */ + struct User { + std::string name; + std::string thumbnail; + std::string url; + std::string username; + }; + struct CloudCast { + std::string name; + std::string thumbnail; + std::string url; + std::string audio_length; + int favorite_count; + int listener_count; + int repost_count; + User user; + std::string slug; + }; + + /** + * Temperature information for a day. + */ + typedef std::deque CloudCastList; + typedef std::deque UserList; + + struct CloudCasts { + CloudCastList cloudcast; + }; + struct Users { + UserList user; + }; + + int cardinality; + + /** + * Weather information for a day. + */ + + /** + * A list of weather information + */ + + /** + * Weather information about the current day + */ + + Client(Config::Ptr config); + + virtual ~Client() = default; + + /** + * Get the current weather for the specified location + */ + + virtual CloudCasts getHot(); + virtual CloudCasts getByQuery(const std::string &query); + virtual Users getUsers(const std::string &query); + virtual CloudCasts parseJson(QJsonDocument root); + virtual CloudCasts getNew(); + virtual std::vector getExtra(int type, const std::string &url); + /** + * Cancel any pending queries (this method can be called from a different thread) + */ + virtual void cancel(); + + virtual Config::Ptr config(); + +protected: + void get(const core::net::Uri::Path &path, + const core::net::Uri::QueryParameters ¶meters, + QJsonDocument &root); + /** + * Progress callback that allows the query to cancel pending HTTP requests. + */ + core::net::http::Request::Progress::Next progress_report( + const core::net::http::Request::Progress& progress); + + /** + * Hang onto the configuration information + */ + Config::Ptr config_; + + /** + * Thread-safe cancelled flag + */ + std::atomic cancelled_; +}; + +} + +#endif // API_CLIENT_H_ + diff --git a/include/api/config.h b/include/api/config.h new file mode 100644 index 0000000..2fba0bc --- /dev/null +++ b/include/api/config.h @@ -0,0 +1,27 @@ +#ifndef API_CONFIG_H_ +#define API_CONFIG_H_ + +#include +#include + +namespace api { + +struct Config { + typedef std::shared_ptr Ptr; + + /* + * The root of all API request URLs + */ + std::string installdir { "" }; + std::string apiroot { "http://api.mixcloud.com" }; + + /* + * The custom HTTP user agent string for this library + */ + std::string user_agent { "Ubuntu-Mixcloud-Scope" }; +}; + +} + +#endif /* API_CONFIG_H_ */ + diff --git a/include/scope/localization.h b/include/scope/localization.h new file mode 100644 index 0000000..8ab60bb --- /dev/null +++ b/include/scope/localization.h @@ -0,0 +1,23 @@ +#ifndef SCOPE_LOCALIZATION_H_ +#define SCOPE_LOCALIZATION_H_ + +#include +#include + +inline char * _(const char *__msgid) { + return dgettext(GETTEXT_PACKAGE, __msgid); +} + +inline std::string _(const char *__msgid1, const char *__msgid2, + unsigned long int __n) { + char buffer [256]; + if (snprintf ( buffer, 256, dngettext(GETTEXT_PACKAGE, __msgid1, __msgid2, __n), __n ) >= 0) { + return buffer; + } else { + return std::string(); + } +} + +#endif // SCOPE_LOCALIZATION_H_ + + diff --git a/include/scope/preview.h b/include/scope/preview.h new file mode 100644 index 0000000..cf36fe9 --- /dev/null +++ b/include/scope/preview.h @@ -0,0 +1,43 @@ +#ifndef SCOPE_PREVIEW_H_ +#define SCOPE_PREVIEW_H_ + +#include +#include +#include + + +namespace unity { +namespace scopes { +class Result; +} +} + +namespace scope { + +/** + * Represents an individual preview request. + * + * Each time a result is previewed in the UI a new Preview + * object is created. + */ +class Preview: public unity::scopes::PreviewQueryBase { +public: + //unity::scopes::Result myResult; + Preview(const unity::scopes::Result &result, + const unity::scopes::ActionMetadata &metadata); + + ~Preview() = default; + + void cancelled() override; + + /** + * Populates the reply object with preview information. + */ + void run(unity::scopes::PreviewReplyProxy const& reply) override; + api::Config::Ptr config_; +}; + +} + +#endif // SCOPE_PREVIEW_H_ + diff --git a/include/scope/query.h b/include/scope/query.h new file mode 100644 index 0000000..5d40dde --- /dev/null +++ b/include/scope/query.h @@ -0,0 +1,41 @@ +#ifndef SCOPE_QUERY_H_ +#define SCOPE_QUERY_H_ + +#include + +#include +#include + +namespace scope { + +/** + * Represents an individual query. + * + * A new Query object will be constructed for each query. It is + * given query information, metadata about the search, and + * some scope-specific configuration. + */ +class Query: public unity::scopes::SearchQueryBase { +public: + Query(const unity::scopes::CannedQuery &query, + const unity::scopes::SearchMetadata &metadata, api::Config::Ptr config); + + ~Query() = default; + + void cancelled() override; + + void run(const unity::scopes::SearchReplyProxy &reply) override; + +private: + std::string installdir; + api::Client client_; + void initScope(); + bool showNew; + bool getExtra; +}; + +} + +#endif // SCOPE_QUERY_H_ + + diff --git a/include/scope/scope.h b/include/scope/scope.h new file mode 100644 index 0000000..47b140b --- /dev/null +++ b/include/scope/scope.h @@ -0,0 +1,54 @@ +#ifndef SCOPE_SCOPE_H_ +#define SCOPE_SCOPE_H_ + +#include + +#include +#include +#include +#include +#include + +namespace scope { + +/** + * Defines the lifecycle of scope plugin, and acts as a factory + * for Query and Preview objects. + * + * Note that the #preview and #search methods are each called on + * different threads, so some form of interlocking is required + * if shared data structures are used. + */ +class Scope: public unity::scopes::ScopeBase { +public: + /** + * Called once at startup + */ + void start(std::string const&) override; + + /** + * Called at shutdown + */ + void stop() override; + + /** + * Called each time a new preview is requested + */ + unity::scopes::PreviewQueryBase::UPtr preview(const unity::scopes::Result&, + const unity::scopes::ActionMetadata&) override; + + /** + * Called each time a new query is requested + */ + unity::scopes::SearchQueryBase::UPtr search( + unity::scopes::CannedQuery const& q, + unity::scopes::SearchMetadata const&) override; + +protected: + api::Config::Ptr config_; +}; + +} + +#endif // SCOPE_SCOPE_H_ + diff --git a/manifest.json.in b/manifest.json.in new file mode 100644 index 0000000..82fae6f --- /dev/null +++ b/manifest.json.in @@ -0,0 +1,15 @@ +{ + "architecture": "@CLICK_ARCH@", + "description": "A scope for mixcloud", + "framework": "ubuntu-sdk-14.10-dev2", + "hooks": { + "mixcloudscope": { + "apparmor": "mixcloudscope.apparmor", + "scope": "mixcloudscope" + } + }, + "maintainer": "Bogdan Cuza ", + "name": "com.ubuntu.developer.boghison.mixcloud", + "title": "mixcloud", + "version": "0.1" +} diff --git a/mixcloudscope.apparmor b/mixcloudscope.apparmor new file mode 100644 index 0000000..63c28db --- /dev/null +++ b/mixcloudscope.apparmor @@ -0,0 +1,5 @@ +{ + "template": "ubuntu-scope-network", + "policy_groups": [], + "policy_version": 1.2 +} diff --git a/po/CMakeLists.txt b/po/CMakeLists.txt new file mode 100644 index 0000000..288b4b7 --- /dev/null +++ b/po/CMakeLists.txt @@ -0,0 +1,8 @@ +include(UseXGettext) + +add_translations_directory(${GETTEXT_PACKAGE}) + +add_translations_catalog( + GETTEXT_PACKAGE ${GETTEXT_PACKAGE} + SOURCE_DIRECTORIES "${CMAKE_SOURCE_DIR}/src" +) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt new file mode 100644 index 0000000..316d748 --- /dev/null +++ b/src/CMakeLists.txt @@ -0,0 +1,76 @@ + +# Put the ini file in the build directory next to the scope +# .so file so test tools can find both easily. +configure_file( + "${CMAKE_SOURCE_DIR}/data/com.ubuntu.developer.boghison.mixcloud_mixcloudscope.ini" + "${CMAKE_CURRENT_BINARY_DIR}/com.ubuntu.developer.boghison.mixcloud_mixcloudscope.ini" + @ONLY +) + +configure_file( + "${CMAKE_SOURCE_DIR}/data/logo.svg" + "${CMAKE_CURRENT_BINARY_DIR}/logo.svg" + @ONLY + COPYONLY +) + +# The sources to build the scope +set(SCOPE_SOURCES + api/client.cpp + scope/preview.cpp + scope/query.cpp + scope/scope.cpp +) + +# Find all the headers +file(GLOB_RECURSE + SCOPE_HEADERS + "${CMAKE_SOURCE_DIR}/include/*.h" +) + +# Build an object library for the scope code +add_library( + scope-static OBJECT + ${SCOPE_SOURCES} + ${SCOPE_HEADERS} +) + +# Ensure we export all the symbols +set_target_properties( + scope-static + PROPERTIES + LINK_FLAGS "-Wl,--export-all-symbols" +) + +# Build a shared library containing our scope code. +# This will be the actual plugin that is loaded. +add_library( + scope SHARED + $ +) + +# Link against the object library and our external library dependencies +target_link_libraries( + scope + ${SCOPE_LDFLAGS} + ${Boost_LIBRARIES} +) + +qt5_use_modules( + scope + Core +) + +# Set the correct library output name to conform to the securiry policy +set_target_properties( + scope + PROPERTIES + OUTPUT_NAME "com.ubuntu.developer.boghison.mixcloud_mixcloudscope" +) + +# Install the scope shared library +install( + TARGETS scope + LIBRARY DESTINATION ${SCOPE_INSTALL_DIR} +) + diff --git a/src/api/client.cpp b/src/api/client.cpp new file mode 100644 index 0000000..3d9db58 --- /dev/null +++ b/src/api/client.cpp @@ -0,0 +1,203 @@ +#include + +#include +#include +#include +#include +#include +#include +#include + + +namespace http = core::net::http; +namespace net = core::net; + +using namespace api; +using namespace std; + +Client::Client(Config::Ptr config) : + config_(config), cancelled_(false) { +} + +std::vector Client::getExtra(int type, const std::string &url){ + std::vector full; + QJsonDocument root; + get({url + "/"}, {}, root); + QVariantMap map = root.toVariant().toMap(); + switch(type){ + case 1: + full.push_back(map["description"].toString().toStdString()); + break; + case 2: + full.push_back(map["city"].toString().toStdString()); + full.push_back(map["following_count"].toString().toStdString()); + full.push_back(map["country"].toString().toStdString()); + full.push_back(map["created_time"].toString().toStdString()); + full.push_back(map["cloudcast_count"].toString().toStdString()); + full.push_back(map["favorite_count"].toString().toStdString()); + full.push_back(map["follower_count"].toString().toStdString()); + full.push_back(map["biog"].toString().toStdString()); + break; + } + return full; +} + +void Client::get(const net::Uri::Path &path, + const net::Uri::QueryParameters ¶meters, QJsonDocument &root) { + // Create a new HTTP client + auto client = http::make_client(); + + // Start building the request configuration + http::Request::Configuration configuration; + + // Build the URI from its components + net::Uri uri = net::make_uri(config_->apiroot, path, parameters); + configuration.uri = client->uri_to_string(uri); + // Give out a user agent string + configuration.header.add("User-Agent", config_->user_agent); + + // Build a HTTP request object from our configuration + auto request = client->head(configuration); + + try { + // Synchronously make the HTTP request + // We bind the cancellable callback to #progress_report + auto response = request->execute( + bind(&Client::progress_report, this, placeholders::_1)); + + // Check that we got a sensible HTTP status code + if (response.status != http::Status::ok) { + throw domain_error(response.body); + } + // Parse the JSON from the response + root = QJsonDocument::fromJson(response.body.c_str()); + } catch (net::Error &) { + } +} + + +Client::CloudCasts Client::parseJson(QJsonDocument root){ + QVariantMap variant = root.toVariant().toMap(); + CloudCasts result; + for (const QVariant &i : variant["data"].toList()){ + QVariantMap item = i.toMap(); + QVariantMap pictures = item["pictures"].toMap(); + QVariantMap user = item["user"].toMap(); + QVariantMap userPics = user["pictures"].toMap(); + Client::User myUser = User{ + user["name"].toString().toStdString(), + userPics["medium"].toString().toStdString(), + user["url"].toString().toStdString(), + user["username"].toString().toStdString() + }; + int audio_length = item["audio_length"].toInt(); + int favorite_count = item["favorite_count"].toInt(); + int listener_count = item["play_count"].toInt(); + int repost_count = item["repost_count"].toInt(); + string audiol_string; + int hours, minutes, seconds; + hours = floor((audio_length / 60 / 60) % 24); + minutes = floor((audio_length / 60) % 60); + seconds = floor(audio_length % 60); + if (hours != 0) { + audiol_string += std::to_string(hours) + "h"; + } + if (minutes != 0) { + audiol_string += std::to_string(minutes) + "m"; + } + if (seconds != 0) { + audiol_string += std::to_string(seconds) + "s"; + } + result.cloudcast.emplace_back( + CloudCast { + item["name"].toString().toStdString(), + pictures["large"].toString().toStdString(), + item["url"].toString().toStdString(), + audiol_string, + favorite_count, + listener_count, + repost_count, + myUser, + item["slug"].toString().toStdString() + }); + } + return result; +} + +Client::CloudCasts Client::getHot() { + QJsonDocument root; + int cardinality = Client::cardinality; + if (cardinality == 0) { + get({"popular/"}, {}, root); + } + else { + get({"popular/"}, {{"limit", std::to_string(cardinality)}}, root); + } + return parseJson(root); +} + +Client::CloudCasts Client::getNew(){ + QJsonDocument root; + int cardinality = Client::cardinality; + if (cardinality == 0) { + get({"new/"}, {}, root); + } + else { + get({"new/"}, {{"limit", std::to_string(cardinality)}}, root); + } + return parseJson(root); +} + +Client::CloudCasts Client::getByQuery(const string &query){ + QJsonDocument root; + int cardinality = Client::cardinality; + if (cardinality == 0) { + get({"search/"}, {{"q", query}, {"type", "cloudcast"}}, root); + } + else { + get({"search/"}, {{"q", query}, {"type", "cloudcast"}, {"limit", std::to_string(cardinality)}}, root); + } + return parseJson(root); +} + +Client::Users Client::getUsers(const string &query){ + QJsonDocument root; + int cardinality = Client::cardinality; + if (cardinality == 0){ + get({"search/"}, {{"q", query}, {"type", "user"}}, root); + } else { + get({"search/"}, {{"q", query}, {"type", "user"}, {"limit", std::to_string(cardinality)}}, root); + } + QVariantMap rootVariant = root.toVariant().toMap(); + Users result; + for (const QVariant &i : rootVariant["data"].toList()){ + QVariantMap item = i.toMap(); + string image = item["pictures"].toMap()["large"].toString().toStdString(); + result.user.emplace_back( + User { + item["name"].toString().toStdString(), + image, + item["url"].toString().toStdString(), + item["username"].toString().toStdString() + } + ); + } + return result; +} + +http::Request::Progress::Next Client::progress_report( + const http::Request::Progress&) { + + return cancelled_ ? + http::Request::Progress::Next::abort_operation : + http::Request::Progress::Next::continue_operation; +} + +void Client::cancel() { + cancelled_ = true; +} + +Config::Ptr Client::config() { + return config_; +} + diff --git a/src/scope/preview.cpp b/src/scope/preview.cpp new file mode 100644 index 0000000..ace953e --- /dev/null +++ b/src/scope/preview.cpp @@ -0,0 +1,143 @@ +#include + +#include +#include +#include +#include +#include +#include + +namespace sc = unity::scopes; + +using namespace std; +using namespace scope; + +Preview::Preview(const sc::Result &result, const sc::ActionMetadata &metadata) : + sc::PreviewQueryBase(result, metadata) { +} + +void Preview::cancelled() { +} + +void Preview::run(sc::PreviewReplyProxy const& reply) { + api::Client client_(Preview::config_); + sc::Result result = PreviewQueryBase::result(); + + if(result["type"].get_string() == "cloudcast"){ + sc::ColumnLayout layoutcol1(1), layoutcol2(2); + if (result["getExtra"].get_bool()){ + layoutcol1.add_column({"image", "header", "description", "expandable", "button"}); + layoutcol2.add_column({"image", "header", "button"}); + layoutcol2.add_column({"description", "audio_length", "favoritecount", "listencount", "repostcount"}); + } + else{ + layoutcol1.add_column({"image", "header", "expandable", "button"}); + layoutcol2.add_column({"image", "header", "button"}); + layoutcol2.add_column({"audio_length", "favoritecount", "listencount", "repostcount"}); + } + reply->register_layout({layoutcol1, layoutcol2}); + sc::PreviewWidget header("header", "header"); + header.add_attribute_mapping("title", "title"); + header.add_attribute_mapping("subtitle", "subtitle"); + sc::PreviewWidget button("button", "actions"); + sc::VariantBuilder builder; + builder.add_tuple({ + {"id", sc::Variant("open")}, + {"label",sc::Variant("Open")}, + {"uri", result["uri"]} + }); + sc::PreviewWidget image("image", "image"); + image.add_attribute_mapping("source", "art"); + button.add_attribute_value("actions", builder.end()); + sc::PreviewWidget expandable("expandable", "expandable"); + expandable.add_attribute_value("title", sc::Variant("Additional info")); + sc::PreviewWidget audio_length("audio_length", "text"); + audio_length.add_attribute_value("title", sc::Variant("Length")); + audio_length.add_attribute_value("text", result["audio_length"]); + sc::PreviewWidget listencount("listencount", "text"); + sc::PreviewWidget favoritecount("favoritecount", "text"); + sc::PreviewWidget repostcount("repostcount", "text"); + listencount.add_attribute_value("title", sc::Variant("Play Count")); + favoritecount.add_attribute_value("title", sc::Variant("Favorite Count")); + repostcount.add_attribute_value("title", sc::Variant("Repost Count")); + listencount.add_attribute_value("text", (result["listen"].get_int() == 0) ? sc::Variant("None") : result["listen"]); + favoritecount.add_attribute_value("text", (result["favorite"].get_int() == 0) ? sc::Variant("None") : result["favorite"]); + repostcount.add_attribute_value("text", (result["repost"].get_int() == 0) ? sc::Variant("None") : result["repost"]); + expandable.add_widget(audio_length); + expandable.add_widget(listencount); + expandable.add_widget(favoritecount); + expandable.add_widget(repostcount); + if (result["getExtra"].get_bool()){ + sc::PreviewWidget description("description", "text"); + description.add_attribute_value("title", sc::Variant("Description")); + std::vector full = client_.getExtra(1, result["username"].get_string() + "/" + result["slug"].get_string()); + description.add_attribute_value("text", sc::Variant(full[0])); + reply->push({image, header, description, button, expandable, audio_length, listencount, favoritecount, repostcount}); + } + else{ + reply->push({image, header, button, expandable, audio_length, listencount, favoritecount, repostcount}); + } + } + else { + if (result["getExtra"].get_bool()) { + sc::ColumnLayout layoutcol1(1), layoutcol2(2); + layoutcol1.add_column({"image", "header", "bio", "expandable", "button"}); + layoutcol2.add_column({"image", "header", "button"}); + layoutcol2.add_column({"bio", "location", "created_time", "cloudcast_count", "favorite_count", "follower_count", "following_count"}); + reply->register_layout({layoutcol1, layoutcol2}); + } + else { + sc::ColumnLayout layout(1); + layout.add_column({"image", "header", "button"}); + reply->register_layout({layout}); + } + sc::PreviewWidget header("header", "header"); + header.add_attribute_mapping("title", "title"); + header.add_attribute_mapping("subtitle", "subtitle"); + sc::PreviewWidget button("button", "actions"); + sc::VariantBuilder builder; + builder.add_tuple({ + {"id", sc::Variant("open")}, + {"label",sc::Variant("Open")}, + {"uri", result["uri"]} + }); + sc::PreviewWidget image("image", "image"); + image.add_attribute_mapping("source", "art"); + button.add_attribute_value("actions", builder.end()); + if (result["getExtra"].get_bool()) { + std::vector full = client_.getExtra(2, result["subtitle"].get_string()); + sc::PreviewWidget bio("bio", "text"), expandable("expandable", "expandable"), location("location", "text"), created_time("created_time", "text"), cloudcast_count("cloudcast_count", "text"), favorite_count("favorite_count", "text"), follower_count("follower_count", "text"), following_count("following_count", "text"); + bio.add_attribute_value("title", sc::Variant("Bio")); + bio.add_attribute_value("text", sc::Variant(full[7])); + std::string fullLocation = full[0] + ", " + full[2]; + //location.add_attribute_value("title", sc::Variant("Location")); + location.add_attribute_value("text", sc::Variant("Lives in " + fullLocation)); + QString dateString = QString::fromUtf8(full[3].data(), full[3].size()); + QDate date = QDate::fromString(dateString, Qt::ISODate); + QString fullDate = date.toString("d MMMM yyyy"); + //created_time.add_attribute_value("title", sc::Variant("Created on")); + created_time.add_attribute_value("text", sc::Variant("Created on " + fullDate.toStdString())); + //cloudcast_count.add_attribute_value("title", sc::Variant("Cloucast Count")); + cloudcast_count.add_attribute_value("text", sc::Variant(full[4] + " Cloudcasts")); + //favorite_count.add_attribute_value("title", sc::Variant("Favorite Count")); + favorite_count.add_attribute_value("text", sc::Variant(full[5] + " Favorites")); + //follower_count.add_attribute_value("title", sc::Variant("Follower Count")); + follower_count.add_attribute_value("text", sc::Variant(full[6] + " Followers")); + //following_count.add_attribute_value("title", sc::Variant("Following Count")); + following_count.add_attribute_value("text", sc::Variant("Following " + full[1])); + expandable.add_attribute_value("title", sc::Variant("Additional Info")); + expandable.add_widget(created_time); + expandable.add_widget(location); + expandable.add_widget(cloudcast_count); + expandable.add_widget(favorite_count); + expandable.add_widget(follower_count); + expandable.add_widget(following_count); + reply->push({image, header, button, expandable, bio, location, created_time, cloudcast_count, favorite_count, follower_count, following_count}); + } + else{ + reply->push({image, header, button}); + } + } + +} + diff --git a/src/scope/query.cpp b/src/scope/query.cpp new file mode 100644 index 0000000..449d9d3 --- /dev/null +++ b/src/scope/query.cpp @@ -0,0 +1,169 @@ +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace sc = unity::scopes; +namespace alg = boost::algorithm; + +using namespace std; +using namespace api; +using namespace scope; + + +/** + * Define the layout for the forecast results + * + * The icon size is small, and ask for the card layout + * itself to be horizontal. I.e. the text will be placed + * next to the image. + */ +/** + * Define the larger "current weather" layout. + * + * The icons are larger. + */ +const static string TRACKS_TEMPLATE = "{\"schema-version\":1,\"template\":{\"category-layout\":\"grid\",\"card-layout\":\"horizontal\",\"card-size\":\"medium\"},\"components\":{\"title\":\"title\",\"art\":{\"field\":\"art\"},\"subtitle\":\"subtitle\",\"attributes\":\"attributes\",\"emblem\":\"emblem\"}}"; +const static string USERS_TEMPLATE = "{\"schema-version\":1,\"components\":{\"title\":\"title\",\"art\":{\"field\":\"art\"}, \"subtitle\": \"subtitle\", \"attributes\": \"attributes\", \"emblem\": \"emblem\"}}"; +Query::Query(const sc::CannedQuery &query, const sc::SearchMetadata &metadata, + Config::Ptr config) : + sc::SearchQueryBase(query, metadata), client_(config) { + Query::installdir = config->installdir; +} + +void Query::cancelled() { + client_.cancel(); +} + +void Query::initScope() { + unity::scopes::VariantMap config = settings(); + showNew = config["showNew"].get_bool(); + getExtra = config["getExtra"].get_bool(); +} + + +void Query::run(sc::SearchReplyProxy const& reply) { + try { + initScope(); + const sc::CannedQuery &query(sc::SearchQueryBase::query()); + sc::SearchMetadata myData = sc::SearchQueryBase::search_metadata(); + client_.cardinality = myData.cardinality(); + string query_string = alg::trim_copy(query.query_string()); + string emblemPath = Query::installdir + "/mixcloud.svg"; + if (query_string.empty()){ + Client::CloudCasts hotCasts; + Client::CloudCasts newCasts; + hotCasts = client_.getHot(); + newCasts = client_.getNew(); + auto hot_category = reply->register_category("hotCasts", "Popular", "", sc::CategoryRenderer(TRACKS_TEMPLATE)); + auto new_category = reply->register_category("newCasts", "New", "", sc::CategoryRenderer(TRACKS_TEMPLATE)); + for (const auto &hotCast : hotCasts.cloudcast){ + sc::CategorisedResult hotRes(hot_category); + hotRes.set_uri(hotCast.url); + hotRes.set_title(hotCast.name); + hotRes.set_art(hotCast.thumbnail); + hotRes["subtitle"] = hotCast.user.name; + hotRes["username"] = hotCast.user.username; + hotRes["type"] = "cloudcast"; + hotRes["getExtra"] = getExtra; + hotRes["repost"] = hotCast.repost_count; + hotRes["slug"] = hotCast.slug; + hotRes["listen"] = hotCast.listener_count; + hotRes["favorite"] = hotCast.favorite_count; + hotRes["audio_length"] = hotCast.audio_length; + string myValue = "♥ " + std::to_string(hotCast.favorite_count); + sc::VariantBuilder builder; + builder.add_tuple({{"value", sc::Variant(hotCast.audio_length)}}); + builder.add_tuple({{"value", sc::Variant(myValue)}}); + hotRes["attributes"] = builder.end(); + hotRes["emblem"] = emblemPath; + if (!reply->push(hotRes)){ + return; + } + } + if (showNew){ + for (const auto &newCast : newCasts.cloudcast){ + sc::CategorisedResult newRes(new_category); + newRes.set_uri(newCast.url); + newRes.set_title(newCast.name); + newRes.set_art(newCast.thumbnail); + newRes["subtitle"] = newCast.user.name; + newRes["type"] = "cloudcast"; + newRes["slug"] = newCast.slug; + newRes["repost"] = newCast.repost_count; + newRes["username"] = newCast.user.username; + newRes["listen"] = newCast.listener_count; + newRes["favorite"] = newCast.favorite_count; + newRes["audio_length"] = newCast.audio_length; + newRes["getExtra"] = getExtra; + string myValue = "♪♫" + std::to_string(newCast.listener_count); + sc::VariantBuilder builder; + builder.add_tuple({{"value", sc::Variant(newCast.audio_length)}}); + builder.add_tuple({{"value", sc::Variant(myValue)}}); + newRes["attributes"] = builder.end(); + newRes["emblem"] = emblemPath; + if (!reply->push(newRes)){ + return; + } + } + } + } else { + Client::CloudCasts castResults; + Client::Users userResults; + castResults = client_.getByQuery(query_string); + userResults = client_.getUsers(query_string); + auto cast_category = reply->register_category("casts", "Cloudcasts", "", sc::CategoryRenderer(TRACKS_TEMPLATE)); + auto user_category = reply->register_category("users", "Users", "", sc::CategoryRenderer(USERS_TEMPLATE)); + for (const auto &cast : castResults.cloudcast){ + sc::CategorisedResult castRes(cast_category); + castRes.set_title(cast.name); + castRes.set_uri(cast.url); + castRes.set_art(cast.thumbnail); + castRes["subtitle"] = cast.user.name; + castRes["repost"] = cast.repost_count; + castRes["listen"] = cast.listener_count; + castRes["favorite"] = cast.favorite_count; + castRes["type"] = "cloudcast"; + castRes["username"] = cast.user.username; + castRes["slug"] = cast.slug; + castRes["getExtra"] = getExtra; + castRes["audio_length"] = cast.audio_length; + string myValue = "♥ " + std::to_string(cast.favorite_count); + sc::VariantBuilder builder; + builder.add_tuple({{"value", sc::Variant(cast.audio_length)}}); + builder.add_tuple({{"value", sc::Variant(myValue)}}); + castRes["attributes"] = builder.end(); + castRes["emblem"] = emblemPath; + if (!reply->push(castRes)){ + return; + } + } + for (const auto &user : userResults.user){ + sc::CategorisedResult userRes(user_category); + userRes.set_title(user.name); + userRes.set_uri(user.url); + userRes.set_art(user.thumbnail); + userRes["subtitle"] = user.username; + userRes["type"] = "user"; + userRes["getExtra"] = getExtra; + if (!reply->push(userRes)){ + return; + } + } + } + + } catch (domain_error &e) { + // Handle exceptions being thrown by the client API + cerr << e.what() << endl; + reply->error(current_exception()); + } +} + diff --git a/src/scope/scope.cpp b/src/scope/scope.cpp new file mode 100644 index 0000000..dbd999c --- /dev/null +++ b/src/scope/scope.cpp @@ -0,0 +1,64 @@ +#include +#include +#include +#include + +namespace sc = unity::scopes; +using namespace std; +using namespace api; +using namespace scope; + +void Scope::start(string const&) { + config_ = make_shared(); + config_->installdir = ScopeBase::scope_directory(); + + setlocale(LC_ALL, ""); + string translation_directory = ScopeBase::scope_directory() + + "/../share/locale/"; + bindtextdomain(GETTEXT_PACKAGE, translation_directory.c_str()); + + // Under test we set a different API root + char *apiroot = getenv("NETWORK_SCOPE_APIROOT"); + if (apiroot) { + config_->apiroot = apiroot; + } +} + +void Scope::stop() { +} + +sc::SearchQueryBase::UPtr Scope::search(const sc::CannedQuery &query, + const sc::SearchMetadata &metadata) { + // Boilerplate construction of Query + return sc::SearchQueryBase::UPtr(new Query(query, metadata, config_)); +} + +sc::PreviewQueryBase::UPtr Scope::preview(sc::Result const& result, + sc::ActionMetadata const& metadata) { + // Boilerplate construction of Preview + auto myPreview = new Preview(result, metadata); + myPreview->config_ = config_; + return sc::PreviewQueryBase::UPtr(myPreview); +} + +#define EXPORT __attribute__ ((visibility ("default"))) + +// These functions define the entry points for the scope plugin +extern "C" { + +EXPORT +unity::scopes::ScopeBase* +// cppcheck-suppress unusedFunction +UNITY_SCOPE_CREATE_FUNCTION() { + return new Scope(); +} + +EXPORT +void +// cppcheck-suppress unusedFunction +UNITY_SCOPE_DESTROY_FUNCTION(unity::scopes::ScopeBase* scope_base) { + delete scope_base; +} + +} +