diff --git a/.github/workflows/02_get_artifact.yml b/.github/workflows/02_get_artifact.yml deleted file mode 100644 index 193aa4af..00000000 --- a/.github/workflows/02_get_artifact.yml +++ /dev/null @@ -1,31 +0,0 @@ -name: Get Artifact - -on: pull_request - -jobs: - get_artifact: - runs-on: ubuntu-latest - - steps: - - name: Grab last artifact - id: last_artifact - run: | - sleep 20 - runs=$(curl -s https://api.github.com/repos/sailfishos-patches/sailfish-qml/actions/workflows/3182534/runs) - check_suite_id=$(echo $runs | jq ".workflow_runs[0].check_suite_id") - run_id=$(echo $runs | jq ".workflow_runs[0].id") - artifact_id=$(curl -s https://api.github.com/repos/sailfishos-patches/sailfish-qml/actions/runs/${run_id}/artifacts | jq ".artifacts[0].id") - archive_download_url="https://github.com/sailfishos-patches/sailfish-qml/suites/${check_suite_id}/artifacts/${artifact_id}" - echo "::set-output name=archive_download_url::${archive_download_url}" - - - name: 'Comment PR' - uses: actions/github-script@v3 - with: - github-token: ${{secrets.GITHUB_TOKEN}} - script: | - github.issues.createComment({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - body: 'Hello @${{ github.event.pull_request.user.login }}\nYour patch is ready!\n\n[Download link](${{ steps.last_artifact.outputs.archive_download_url }})' - }) diff --git a/.github/workflows/01_make_patch.yml b/.github/workflows/make_patch.yml similarity index 69% rename from .github/workflows/01_make_patch.yml rename to .github/workflows/make_patch.yml index 8be59ca6..87827a1d 100644 --- a/.github/workflows/01_make_patch.yml +++ b/.github/workflows/make_patch.yml @@ -7,7 +7,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Create diff run: | @@ -17,19 +17,20 @@ jobs: cp patch/*.qml patch/*.png patch/*.js patch/*.svg patch/*.qm .github/patch_data |: - name: Upload build result - uses: actions/upload-artifact@v2 + id: artifact-upload-step + uses: actions/upload-artifact@v4 with: name: ${{ github.head_ref }} path: .github/patch_data - name: 'Comment PR' - uses: actions/github-script@v3 + uses: actions/github-script@v7 with: github-token: ${{secrets.GITHUB_TOKEN}} script: | - github.issues.createComment({ + github.rest.issues.createComment({ issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, - body: 'Hello @${{ github.event.pull_request.user.login }}\nYour patch is ready as Artifact inside of [Checks](https://github.com/sailfishos-patches/sailfish-qml/pull/${{ github.event.pull_request.number }}/checks)\n\nTrying to get direct download link in couple of seconds...' + body: 'Hello @${{ github.event.pull_request.user.login }}\nYour patch is ready!\n\n[Download link](${{ steps.artifact-upload-step.outputs.artifact-url }})' }) diff --git a/usr/lib/jolla-mediaplayer/plugins/jolla/AlbumPage.qml b/usr/lib/jolla-mediaplayer/plugins/jolla/AlbumPage.qml new file mode 100644 index 00000000..da9089f9 --- /dev/null +++ b/usr/lib/jolla-mediaplayer/plugins/jolla/AlbumPage.qml @@ -0,0 +1,151 @@ +// -*- qml -*- + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Sailfish.Media 1.0 +import com.jolla.mediaplayer 1.0 + +Page { + id: albumPage + + property var media + + Loader { + active: albumPage.isLandscape && coverArt.source != "" + anchors { + top: parent.top + bottom: parent.bottom + left: parent.horizontalCenter + right: parent.right + } + + Component.onCompleted: setSource("CoverArtHolder.qml", { "view": view, "coverArt": coverArt }) + } + + CoverArt { + id: coverArt + + // re-evaluate when state changes + source: (albumArtProvider.extracting || true) ? albumArtProvider.albumArt(media.title, media.author) : "" + } + + MediaPlayerListView { + id: view + + model: GriloTrackerModel { + query: { + //: placeholder string for albums without a known name + //% "Unknown album" + var unknownAlbum = qsTrId("mediaplayer-la-unknown-album") + + //: placeholder string to be shown for media without a known artist + //% "Unknown artist" + var unknownArtist = qsTrId("mediaplayer-la-unknown-artist") + + return AudioTrackerHelpers.getSongsQuery(albumHeader.searchText, + {"unknownArtist": unknownArtist, + "unknownAlbum": unknownAlbum, + "authorId": media.get("tracker-urn"), + "albumId": media.id}) + } + } + + contentWidth: albumPage.width + + PullDownMenu { + MenuItem { + //: Shuffle all menu entry in album page + //% "Shuffle all" + text: qsTrId("mediaplayer-me-album-shuffle-all") + onClicked: AudioPlayer.shuffleAndPlay(view.model, view.count) + } + + NowPlayingMenuItem { } + + MenuItem { + //: Search menu entry + //% "Search" + text: qsTrId("mediaplayer-me-search") + onClicked: albumHeader.enableSearch() + enabled: view.count > 0 || albumHeader.searchText !== '' + } + } + + ViewPlaceholder { + //: Placeholder text for an empty search view + //% "No items found" + text: qsTrId("mediaplayer-la-empty-search") + enabled: view.count === 0 && !busyIndicator.running + } + + PageBusyIndicator { + id: busyIndicator + + running: view.model.fetching + } + + Component { + id: addPageComponent + AddToPlaylistPage { } + } + + header: SearchPageHeader { + id: albumHeader + width: albumPage.isLandscape ? view.contentWidth : view.width + + //: header for the page showing the songs that don't belong to a known album + //% "Unknown album" + title: media.title !== "" ? media.title : qsTrId("mediaplayer-la-unknown-album") + + //: All songs search field placeholder text + //% "Search song" + placeholderText: qsTrId("mediaplayer-tf-album-search") + + coverArt: albumPage.isLandscape ? null : coverArt + } + + delegate: MediaListDelegate { + id: delegate + + property var itemMedia: media + + formatFilter: albumHeader.searchText + menu: menuComponent + onClicked: AudioPlayer.play(view.model, index) + + ListView.onAdd: AddAnimation { target: delegate } + ListView.onRemove: animateRemoval() + + function remove() { + AudioPlayer.remove(itemMedia, delegate, playlists) + } + + Component { + id: menuComponent + + ContextMenu { + width: view.contentWidth + + MenuItem { + //: Add to playlist context menu item in album page + //% "Add to playlist" + text: qsTrId("mediaplayer-me-album-add-to-playlist") + onClicked: pageStack.animatorPush(addPageComponent, {media: itemMedia}) + } + MenuItem { + //: Add to playing queue context menu item in album page + //% "Add to playing queue" + text: qsTrId("mediaplayer-me-album-add-to-playing-queue") + onClicked: AudioPlayer.addToQueue(itemMedia) + } + MenuItem { + //: Delete item + //% "Delete" + text: qsTrId("mediaplayer-me-all-songs-delete") + onClicked: remove() + } + } + } + } + } +} diff --git a/usr/lib/jolla-mediaplayer/plugins/jolla/AlbumsPage.qml b/usr/lib/jolla-mediaplayer/plugins/jolla/AlbumsPage.qml new file mode 100644 index 00000000..e7310fb0 --- /dev/null +++ b/usr/lib/jolla-mediaplayer/plugins/jolla/AlbumsPage.qml @@ -0,0 +1,104 @@ +// -*- qml -*- + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import com.jolla.mediaplayer 1.0 +import Nemo.Thumbnailer 1.0 + +Page { + id: albumsPage + + property var model + property string searchText + + TrackerQueriesBuilder { + id: queriesBuilder + } + + MediaPlayerListView { + id: view + + property string query: queriesBuilder.getAlbumsQuery(albumsHeader.searchText) + + model: albumsPage.model + + Binding { + target: albumsPage.model.source + property: "query" + value: view.query + } + + PullDownMenu { + NowPlayingMenuItem { } + + MenuItem { + //: Search menu entry + //% "Search" + text: qsTrId("mediaplayer-me-search") + onClicked: albumsHeader.enableSearch() + enabled: view.count > 0 || albumsHeader.searchText !== '' + } + } + + ViewPlaceholder { + text: { + if (albumsHeader.searchText !== '') { + //: Placeholder text for an empty search view + //% "No items found" + return qsTrId("mediaplayer-la-empty-search") + } else { + //: Placeholder text for an empty view + //% "Get some media" + return qsTrId("mediaplayer-la-get-some-media") + } + } + enabled: view.count === 0 && !busyIndicator.running + } + + PageBusyIndicator { + id: busyIndicator + + running: albumsPage.model.source.fetching + } + + header: SearchPageHeader { + id: albumsHeader + width: parent.width + + //: title for the Albums page + //% "Albums" + title: qsTrId("mediaplayer-he-albums") + + //: Albums search field placeholder text + //% "Search album" + placeholderText: qsTrId("mediaplayer-tf-albums-search") + + searchText: albumsPage.searchText + Component.onCompleted: if (searchText !== '') enableSearch() + } + + delegate: MediaContainerListDelegate { + id: delegate + + contentHeight: albumArt.height + leftPadding: albumArt.width + Theme.paddingLarge + formatFilter: albumsHeader.searchText + title: media.title + subtitle: media.author + titleFont.pixelSize: Theme.fontSizeLarge + subtitleFont.pixelSize: Theme.fontSizeMedium + onClicked: pageStack.animatorPush(Qt.resolvedUrl("AlbumPage.qml"), {media: media}) + + ListView.onAdd: AddAnimation { target: delegate } + ListView.onRemove: animateRemoval() + + AlbumArt { + id: albumArt + + // re-evaluate when state changes + source: (albumArtProvider.extracting || true) ? albumArtProvider.albumThumbnail(title, subtitle) : "" + highlighted: delegate.highlighted + } + } + } +} diff --git a/usr/lib/jolla-mediaplayer/plugins/jolla/AllSongsPage.qml b/usr/lib/jolla-mediaplayer/plugins/jolla/AllSongsPage.qml new file mode 100644 index 00000000..23378a2b --- /dev/null +++ b/usr/lib/jolla-mediaplayer/plugins/jolla/AllSongsPage.qml @@ -0,0 +1,133 @@ +// -*- qml -*- + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import com.jolla.mediaplayer 1.0 + +Page { + id: allSongsPage + + property var model + property string searchText + + TrackerQueriesBuilder { + id: queriesBuilder + } + + MediaPlayerListView { + id: view + + property string query: queriesBuilder.getSongsQuery(allSongsHeader.searchText) + + model: allSongsPage.model + + Binding { + target: allSongsPage.model.source + property: "query" + value: view.query + } + + PullDownMenu { + MenuItem { + //: Shuffle all menu entry in all songs page + //% "Shuffle all" + text: qsTrId("mediaplayer-me-all-songs-shuffle-all") + onClicked: AudioPlayer.shuffleAndPlay(view.model, view.count) + } + + NowPlayingMenuItem { } + + MenuItem { + //: Search menu entry + //% "Search" + text: qsTrId("mediaplayer-me-search") + onClicked: allSongsHeader.enableSearch() + enabled: view.count > 0 || allSongsHeader.searchText !== '' + } + } + + Component { + id: addPageComponent + AddToPlaylistPage { } + } + + ViewPlaceholder { + text: { + if (allSongsHeader.searchText !== '') { + //: Placeholder text for an empty search view + //% "No items found" + return qsTrId("mediaplayer-la-empty-search") + } else { + //: Placeholder text for an empty view + //% "Get some media" + return qsTrId("mediaplayer-la-get-some-media") + } + } + enabled: view.count === 0 && !busyIndicator.running + } + + PageBusyIndicator { + id: busyIndicator + + running: allSongsPage.model.source.fetching + } + + header: SearchPageHeader { + id: allSongsHeader + width: parent.width + + //: Title for the all songs page + //% "All songs" + title: qsTrId("mediaplayer-he-all-songs") + + //: All songs search field placeholder text + //% "Search song" + placeholderText: qsTrId("mediaplayer-tf-songs-search") + + searchText: allSongsPage.searchText + Component.onCompleted: if (searchText !== '') enableSearch() + } + + delegate: MediaListDelegate { + id: delegate + + property var itemMedia: media + + formatFilter: allSongsHeader.searchText + + function remove() { + AudioPlayer.remove(itemMedia, delegate, playlists) + } + + menu: menuComponent + onClicked: AudioPlayer.play(view.model, index) + + ListView.onAdd: AddAnimation { target: delegate } + ListView.onRemove: animateRemoval() + + Component { + id: menuComponent + ContextMenu { + MenuItem { + //: Add to playlist context menu item in all songs page + //% "Add to playlist" + text: qsTrId("mediaplayer-me-all-songs-add-to-playlist") + onClicked: pageStack.animatorPush(addPageComponent, {media: itemMedia}) + } + MenuItem { + //: Add to playing queue context menu item in all songs page + //% "Add to playing queue" + text: qsTrId("mediaplayer-me-all-songs-add-to-playing-queue") + onClicked: AudioPlayer.addToQueue(itemMedia) + } + MenuItem { + //: Delete item + //% "Delete" + text: qsTrId("mediaplayer-me-all-songs-delete") + onClicked: remove() + } + } + } + } + } +} diff --git a/usr/lib/jolla-mediaplayer/plugins/jolla/ArtistAllSongsPage.qml b/usr/lib/jolla-mediaplayer/plugins/jolla/ArtistAllSongsPage.qml new file mode 100644 index 00000000..d966f1a1 --- /dev/null +++ b/usr/lib/jolla-mediaplayer/plugins/jolla/ArtistAllSongsPage.qml @@ -0,0 +1,123 @@ +// -*- qml -*- + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Sailfish.Media 1.0 +import com.jolla.mediaplayer 1.0 + +Page { + property var media + + MediaPlayerListView { + id: view + + model: GriloTrackerModel { + query: { + //: placeholder string for albums without a known name + //% "Unknown album" + var unknownAlbum = qsTrId("mediaplayer-la-unknown-album") + + //: placeholder string to be shown for media without a known artist + //% "Unknown artist" + var unknownArtist = qsTrId("mediaplayer-la-unknown-artist") + + return AudioTrackerHelpers.getSongsQuery(artistHeader.searchText, + {"unknownArtist": unknownArtist, + "unknownAlbum": unknownAlbum, + "authorId": media.id}) + } + } + + PullDownMenu { + MenuItem { + //: Shuffle all menu entry in artist page + //% "Shuffle all" + text: qsTrId("mediaplayer-me-artist-shuffle-all") + onClicked: AudioPlayer.shuffleAndPlay(view.model, view.count) + } + + NowPlayingMenuItem { } + + MenuItem { + //: Search menu entry + //% "Search" + text: qsTrId("mediaplayer-me-search") + onClicked: artistHeader.enableSearch() + enabled: view.count > 0 || artistHeader.searchText !== '' + } + } + + ViewPlaceholder { + //: Placeholder text for an empty search view + //% "No items found" + text: qsTrId("mediaplayer-la-empty-search") + enabled: view.count === 0 && !busyIndicator.running + } + + PageBusyIndicator { + id: busyIndicator + + running: view.model.source.fetching + } + + Component { + id: addPageComponent + AddToPlaylistPage { } + } + + header: SearchPageHeader { + id: artistHeader + width: parent.width + + //: placeholder text if we don't know the artist name + //% "Unknown artist" + title: media.title != "" ? media.title : qsTrId("mediaplayer-la-unknown-artist") + + //: Artist all songs search field placeholder text + //% "Search song" + placeholderText: qsTrId("mediaplayer-tf-songs-search") + } + + delegate: MediaListDelegate { + id: delegate + + property var itemMedia: media + + formatFilter: artistHeader.searchText + + function remove() { + AudioPlayer.remove(itemMedia, delegate, playlists) + } + + menu: menuComponent + onClicked: AudioPlayer.play(view.model, index) + + ListView.onAdd: AddAnimation { target: delegate } + ListView.onRemove: animateRemoval() + + Component { + id: menuComponent + ContextMenu { + MenuItem { + //: Add to playlist context menu item in artist page + //% "Add to playlist" + text: qsTrId("mediaplayer-me-artist-add-to-playlist") + onClicked: pageStack.animatorPush(addPageComponent, {media: itemMedia}) + } + MenuItem { + //: Add to playing queue context menu item in artist page + //% "Add to playing queue" + text: qsTrId("mediaplayer-me-artist-add-to-playing-queue") + onClicked: AudioPlayer.addToQueue(itemMedia) + } + MenuItem { + //: Delete item + //% "Delete" + text: qsTrId("mediaplayer-me-all-songs-delete") + onClicked: remove() + } + } + } + } + } +} diff --git a/usr/lib/jolla-mediaplayer/plugins/jolla/ArtistPage.qml b/usr/lib/jolla-mediaplayer/plugins/jolla/ArtistPage.qml new file mode 100644 index 00000000..aa213ce3 --- /dev/null +++ b/usr/lib/jolla-mediaplayer/plugins/jolla/ArtistPage.qml @@ -0,0 +1,115 @@ +// -*- qml -*- + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Sailfish.Media 1.0 +import com.jolla.mediaplayer 1.0 + +Page { + property var media + + MediaPlayerListView { + id: view + + model: GriloTrackerModel { + query: { + //: placeholder string to be shown for media without a known artist + //% "Unknown artist" + var unknownArtist = qsTrId("mediaplayer-la-unknown-artist") + + //: placeholder string for albums without a known name + //% "Unknown album" + var unknownAlbum = qsTrId("mediaplayer-la-unknown-album") + + //: string for albums with multiple artists + //% "Multiple artists" + var multipleArtists = qsTrId("mediaplayer-la-multiple-authors") + + return AudioTrackerHelpers.getAlbumsQuery(albumsHeader.searchText, + {"unknownArtist": unknownArtist, + "unknownAlbum": unknownAlbum, + "multipleArtists": multipleArtists, + "authorId": media.id}) + } + } + + PullDownMenu { + MenuItem { + //: List all songs of this artist + //% "List all songs" + text: qsTrId("mediaplayer-me-show-all-artist-songs") + onClicked: pageStack.animatorPush(Qt.resolvedUrl("ArtistAllSongsPage.qml"), {media: media}) + } + + NowPlayingMenuItem { } + + MenuItem { + //: Search menu entry + //% "Search" + text: qsTrId("mediaplayer-me-search") + onClicked: albumsHeader.enableSearch() + enabled: view.count > 0 || albumsHeader.searchText !== '' + } + } + + ViewPlaceholder { + text: { + if (albumsHeader.searchText !== '') { + //: Placeholder text for an empty search view + //% "No items found" + return qsTrId("mediaplayer-la-empty-search") + } else { + //: Placeholder text for an empty view + //% "Get some media" + return qsTrId("mediaplayer-la-get-some-media") + } + } + enabled: view.count === 0 && !busyIndicator.running + } + + PageBusyIndicator { + id: busyIndicator + + running: view.model.fetching + } + + header: SearchPageHeader { + id: albumsHeader + width: parent.width + + // TODO: Shouldn't we place here the artist name, instead? + + //: title for the Artist page + //% "Albums" + title: qsTrId("mediaplayer-he-albums") + + //: Albums search field placeholder text + //% "Search album" + placeholderText: qsTrId("mediaplayer-tf-albums-search") + } + + delegate: MediaContainerListDelegate { + id: delegate + + contentHeight: albumArt.height + leftPadding: albumArt.width + Theme.paddingLarge + formatFilter: albumsHeader.searchText + title: media.title + subtitle: media.author + titleFont.pixelSize: Theme.fontSizeLarge + subtitleFont.pixelSize: Theme.fontSizeMedium + onClicked: pageStack.animatorPush(Qt.resolvedUrl("AlbumPage.qml"), {media: media}) + + ListView.onAdd: AddAnimation { target: delegate } + ListView.onRemove: animateRemoval() + + AlbumArt { + id: albumArt + + // re-evaluate when state changes + source: (albumArtProvider.extracting || true) ? albumArtProvider.albumThumbnail(title, subtitle) : "" + highlighted: delegate.highlighted + } + } + } +} diff --git a/usr/lib/jolla-mediaplayer/plugins/jolla/ArtistsPage.qml b/usr/lib/jolla-mediaplayer/plugins/jolla/ArtistsPage.qml new file mode 100644 index 00000000..6a098d1d --- /dev/null +++ b/usr/lib/jolla-mediaplayer/plugins/jolla/ArtistsPage.qml @@ -0,0 +1,113 @@ +// -*- qml -*- + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import com.jolla.mediaplayer 1.0 + +Page { + id: artistsPage + + property var model + property string searchText + + function formatDuration(duration) { + var secs = parseInt(duration) + var mins = Math.floor(secs / 60) + var hours = Math.floor(mins / 60) + var minutes = mins - (hours * 60) + + //: duration in hours of the songs belonging to an artist + //% "%n hours" + var hourString = qsTrId("mediaplayer-la-artist-songs-duration-hours", hours) + + //: duration in minutes of the songs belonging to an artist + //% "%n minutes" + var minuteString = qsTrId("mediaplayer-la-artist-songs-duration-minutes", minutes) + + //: the duration shown below the artist name in the artists page, + //: %1 is hour string ("N hours"), %2 is minute string + //% "%1, %2" + return hours > 0 + ? qsTrId("mediaplayer-la-artist-songs-duration-hours-minutes").arg(hourString).arg(minuteString) + : minuteString + } + + TrackerQueriesBuilder { + id: queriesBuilder + } + + MediaPlayerListView { + id: view + + model: artistsPage.model + property string query: queriesBuilder.getArtistsQuery(artistsHeader.searchText) + + Binding { + target: artistsPage.model.source + property: "query" + value: view.query + } + + delegate: MediaContainerListDelegate { + id: delegate + + formatFilter: artistsHeader.searchText + title: media.title + subtitle: artistsPage.formatDuration(media.childCount) + onClicked: pageStack.animatorPush(Qt.resolvedUrl("ArtistPage.qml"), {media: media}) + + ListView.onAdd: AddAnimation { target: delegate } + ListView.onRemove: animateRemoval() + } + + + PullDownMenu { + NowPlayingMenuItem { } + + MenuItem { + //: Search menu entry + //% "Search" + text: qsTrId("mediaplayer-me-search") + onClicked: artistsHeader.enableSearch() + enabled: view.count > 0 || artistsHeader.searchText !== '' + } + } + + ViewPlaceholder { + text: { + if (artistsHeader.searchText !== '') { + //: Placeholder text for an empty search view + //% "No items found" + return qsTrId("mediaplayer-la-empty-search") + } else { + //: Placeholder text for an empty view + //% "Get some media" + return qsTrId("mediaplayer-la-get-some-media") + } + } + enabled: view.count === 0 && !busyIndicator.running + } + + PageBusyIndicator { + id: busyIndicator + + running: artistsPage.model.source.fetching + } + + header: SearchPageHeader { + id: artistsHeader + width: parent.width + + //: Title for the Artists page + //% "Artists" + title: qsTrId("mediaplayer-he-artists") + + //: Artists search field placeholder text + //% "Search artist" + placeholderText: qsTrId("mediaplayer-tf-artists-search") + + searchText: artistsPage.searchText + Component.onCompleted: if (searchText !== '') enableSearch() + } + } +} diff --git a/usr/lib/jolla-mediaplayer/plugins/jolla/CoverArtHolder.qml b/usr/lib/jolla-mediaplayer/plugins/jolla/CoverArtHolder.qml new file mode 100644 index 00000000..bf16e27c --- /dev/null +++ b/usr/lib/jolla-mediaplayer/plugins/jolla/CoverArtHolder.qml @@ -0,0 +1,42 @@ +// -*- qml -*- + +import QtQuick 2.0 +import Sailfish.Silica 1.0 + +Item { + id: root + + property Item view + property Item coverArt + + Component.onCompleted: { + coverArt.parent = coverHolder + view.contentWidth = Qt.binding(function() { return view.width - width }) + } + + Component.onDestruction: view.contentWidth = Qt.binding(function() { return view.width }) + + Rectangle { + anchors { + top: coverHolder.top + bottom: parent.bottom + left: coverHolder.left + right: coverHolder.right + } + color: Theme.highlightBackgroundColor + opacity: Theme.opacityHigh + } + + Item { + id: coverHolder + + opacity: coverArt.status === Image.Ready ? 1.0 : 0.0 + + Behavior on opacity { FadeAnimation {} } + + x: root.view.contentWidth - width + y: (root.view.contentItem.y - view.headerItem.height) > 0 ? root.view.contentItem.y - root.view.headerItem.height : 0 + width: parent.width + height: width + } +} diff --git a/usr/lib/jolla-mediaplayer/plugins/jolla/TrackerQueriesBuilder.qml b/usr/lib/jolla-mediaplayer/plugins/jolla/TrackerQueriesBuilder.qml new file mode 100644 index 00000000..51f15836 --- /dev/null +++ b/usr/lib/jolla-mediaplayer/plugins/jolla/TrackerQueriesBuilder.qml @@ -0,0 +1,47 @@ +// -*- qml -*- + +import QtQuick 2.0 +import com.jolla.mediaplayer 1.0 + +QtObject { + id: trackerQueriesBuilder + + function getSongsQuery(searchText) { + //: placeholder string for albums without a known name + //% "Unknown album" + var unknownAlbum = qsTrId("mediaplayer-la-unknown-album") + + //: placeholder string to be shown for media without a known artist + //% "Unknown artist" + var unknownArtist = qsTrId("mediaplayer-la-unknown-artist") + + return AudioTrackerHelpers.getSongsQuery(searchText, {"unknownArtist": unknownArtist, "unknownAlbum": unknownAlbum}) + } + + function getAlbumsQuery(searchText) { + //: placeholder string to be shown for media without a known artist + //% "Unknown artist" + var unknownArtist = qsTrId("mediaplayer-la-unknown-artist") + + //: placeholder string for albums without a known name + //% "Unknown album" + var unknownAlbum = qsTrId("mediaplayer-la-unknown-album") + + //: string for albums with multiple artists + //% "Multiple artists" + var multipleArtists = qsTrId("mediaplayer-la-multiple-authors") + + return AudioTrackerHelpers.getAlbumsQuery(searchText, + {"unknownArtist": unknownArtist, + "unknownAlbum": unknownAlbum, + "multipleArtists": multipleArtists}) + } + + function getArtistsQuery(searchText) { + //: placeholder string to be shown for media without a known artist + //% "Unknown artist" + var unknownArtist = qsTrId("mediaplayer-la-unknown-artist") + + return AudioTrackerHelpers.getArtistsQuery(searchText, {"unknownArtist": unknownArtist}) + } +} diff --git a/usr/lib/maliit/plugins/jolla-keyboard.qml b/usr/lib/maliit/plugins/jolla-keyboard.qml index a94524a2..481dc69d 100644 --- a/usr/lib/maliit/plugins/jolla-keyboard.qml +++ b/usr/lib/maliit/plugins/jolla-keyboard.qml @@ -33,12 +33,12 @@ import QtQuick 2.0 import com.jolla 1.0 import QtFeedback 5.0 import com.meego.maliitquick 1.0 -import org.nemomobile.configuration 1.0 +import Nemo.Configuration 1.0 import com.jolla.keyboard 1.0 import Sailfish.Silica 1.0 import Sailfish.Silica.Background 1.0 import com.jolla.keyboard.translations 1.0 -import org.nemomobile.dbus 2.0 +import Nemo.DBus 2.0 import org.nemomobile.systemsettings 1.0 SilicaControl { @@ -50,9 +50,9 @@ SilicaControl { height: MInputMethodQuick.screenHeight property bool portraitRotated: width > height - property bool portraitLayout: portraitRotated ? - (MInputMethodQuick.appOrientation == 90 || MInputMethodQuick.appOrientation == 270) : - (MInputMethodQuick.appOrientation == 0 || MInputMethodQuick.appOrientation == 180) + property bool portraitLayout: portraitRotated + ? (MInputMethodQuick.appOrientation == 90 || MInputMethodQuick.appOrientation == 270) + : (MInputMethodQuick.appOrientation == 0 || MInputMethodQuick.appOrientation == 180) property alias activeIndex: keyboard.currentIndex property alias layoutModel: _layoutModel @@ -93,7 +93,7 @@ SilicaControl { if (!MInputMethodQuick.active) return - var x = 0, y = 0, width = 0, height = 0; + var x = 0, y = 0, width = 0, height = 0 var angle = MInputMethodQuick.appOrientation var layoutHeight = keyboard.currentLayoutHeight @@ -337,6 +337,9 @@ SilicaControl { property alias splitEnabled: splitConfig.value property alias pasteInputHandler: pasteInputHandler + // has extra padding to avoid covering screen cutouts or roundings, can exist in landscape + readonly property bool hasHorizontalPadding: !portraitMode && geometry.keyboardWidthLandscape < screen.height + x: (canvas.width - width) / 2 y: (canvas.height - height) / 2 @@ -354,11 +357,11 @@ SilicaControl { portraitMode: portraitLayout layout: { if (mode === "common") { - return currentItem ? currentItem.item : null + return currentItem ? currentItem.loadedLayout : null } else if (mode === "number") { - return number_portrait.visible ? number_portrait : number_landscape.item + return numberPortrait.visible ? numberPortrait : numberLandscape.item } else { - return phone_portrait.visible ? phone_portrait : phone_landscape.item + return phonePortrait.visible ? phonePortrait : phoneLandscape.item } } layoutChangeAllowed: mode === "common" @@ -505,7 +508,8 @@ SilicaControl { } NumberLayoutPortrait { - id: number_portrait + id: numberPortrait + x: (keyboard.width - width) / 2 y: keyboard.height - height width: geometry.isLargeScreen ? 0.6 * geometry.keyboardWidthPortrait @@ -514,7 +518,7 @@ SilicaControl { } Loader { - id: number_landscape + id: numberLandscape sourceComponent: (keyboard.mode === "number" && !geometry.isLargeScreen) ? landscapeNumberComponent : undefined } @@ -523,12 +527,15 @@ SilicaControl { id: landscapeNumberComponent NumberLayoutLandscape { y: keyboard.height - height - visible: keyboard.mode === "number" && !number_portrait.visible + x: Math.round((keyboard.width - width) / 2) + width: geometry.keyboardWidthLandscape + visible: keyboard.mode === "number" && !numberPortrait.visible } } PhoneNumberLayoutPortrait { - id: phone_portrait + id: phonePortrait + x: (keyboard.width - width) / 2 y: keyboard.height - height width: geometry.isLargeScreen ? 0.6 * geometry.keyboardWidthPortrait @@ -537,7 +544,7 @@ SilicaControl { } Loader { - id: phone_landscape + id: phoneLandscape sourceComponent: (keyboard.mode === "phone" && !geometry.isLargeScreen) ? phoneLandscapeComponent : undefined } @@ -546,7 +553,9 @@ SilicaControl { id: phoneLandscapeComponent PhoneNumberLayoutLandscape { y: keyboard.height - height - visible: keyboard.mode === "phone" && !phone_portrait.visible + x: Math.round((keyboard.width - width) / 2) + width: geometry.keyboardWidthLandscape + visible: keyboard.mode === "phone" && !phonePortrait.visible } } @@ -572,6 +581,7 @@ SilicaControl { KeyboardLayoutSwitchHint { id: switchHint + y: keyboard.height - height width: keyboard.width height: keyboard.currentLayoutHeight @@ -668,6 +678,7 @@ SilicaControl { Timer { id: areaUpdater + interval: 1 onTriggered: canvas.updateIMArea() } diff --git a/usr/lib/mozembedlite/chrome/embedlite/content/ContextMenuHandler.js b/usr/lib/mozembedlite/chrome/embedlite/content/ContextMenuHandler.js index 13c6eb13..2b64ba60 100644 --- a/usr/lib/mozembedlite/chrome/embedlite/content/ContextMenuHandler.js +++ b/usr/lib/mozembedlite/chrome/embedlite/content/ContextMenuHandler.js @@ -94,6 +94,7 @@ var ContextMenuHandler = { if (popupNode instanceof Ci.nsIImageLoadingContent && popupNode.currentURI) { state.types.push("image"); state.label = state.mediaURL = popupNode.currentURI.spec; + state.linkTitle = popupNode.textContent || popupNode.title; imageUrl = state.mediaURL; this._target = popupNode; diff --git a/usr/lib/mozembedlite/chrome/embedlite/content/SelectionHandler.js b/usr/lib/mozembedlite/chrome/embedlite/content/SelectionHandler.js index 77a1831f..39b7a794 100644 --- a/usr/lib/mozembedlite/chrome/embedlite/content/SelectionHandler.js +++ b/usr/lib/mozembedlite/chrome/embedlite/content/SelectionHandler.js @@ -302,6 +302,10 @@ function SelectionHandler() { * content values. */ this._onSelectionCopy = function _onSelectionCopy(aMsg) { + if (!this._cache || !this._cache.selection) { + return; + } + let tap = { xPos: aMsg.xPos, yPos: aMsg.yPos, @@ -365,7 +369,7 @@ function SelectionHandler() { this._onFail = function _onFail(aDbgMessage) { if (aDbgMessage && aDbgMessage.length > 0) Logger.debug(aDbgMessage); - this.sendAsync("Content:SelectionFail"); + this.sendAsync("Content:SelectionFail", {}); this._clearSelection(); this.closeSelection(); } diff --git a/usr/lib/qt5/qml/Amber/QrFilter/qmldir b/usr/lib/qt5/qml/Amber/QrFilter/qmldir index 8cc630d4..c49e6c15 100644 --- a/usr/lib/qt5/qml/Amber/QrFilter/qmldir +++ b/usr/lib/qt5/qml/Amber/QrFilter/qmldir @@ -1,2 +1,3 @@ module Amber.QrFilter plugin qrfilter +typeinfo plugins.qmltypes diff --git a/usr/lib/qt5/qml/Amber/Web/Authorization/OAuth10a.qml b/usr/lib/qt5/qml/Amber/Web/Authorization/OAuth10a.qml new file mode 100644 index 00000000..c7440e04 --- /dev/null +++ b/usr/lib/qt5/qml/Amber/Web/Authorization/OAuth10a.qml @@ -0,0 +1,88 @@ +/**************************************************************************** +** +** BSD 3-Clause License. See LICENSE for full text. +** Copyright (c) 2021 Open Mobile Platform LLC. +** All rights reserved. +** +****************************************************************************/ + +import QtQml 2.0 +import Amber.Web.Authorization 1.0 +import "httpcontent.js" as OAuthHttpContent + +QtObject { + id: root + + property alias timeout: listener.timeout + + property alias userAgent: oauth1.userAgent + property alias redirectUri: oauth1.redirectUri // oauth_callback + + property alias requestTokenEndpoint: oauth1.requestTokenEndpoint + property alias authorizeTokenEndpoint: oauth1.authorizeTokenEndpoint + property alias accessTokenEndpoint: oauth1.accessTokenEndpoint + + property alias consumerKey: oauth1.consumerKey + property alias consumerSecret: oauth1.consumerSecret + + property alias customParameters: oauth1.customParameters + + signal receivedTemporaryToken(string oauthToken, string oauthTokenSecret) + signal receivedTokenAuthorization(string oauthToken, string oauthVerifier) + signal receivedAccessToken(string oauthToken, string oauthTokenSecret) + signal errorOccurred(var error) + + function requestTemporaryToken() { + oauth1.requestTemporaryToken() + } + + function authorizationUrl(oauthToken) { + if (redirectUri.length === 0 + || redirectUri == listener.uri) { + listener.startListening() + } + return oauth1.generateAuthorizationUrl(oauthToken) + } + + function authorizeInBrowser(oauthToken, oauthTokenSecret) { + oauth1.temporaryToken = oauthToken + oauth1.temporaryTokenSecret = oauthTokenSecret + Qt.openUrlExternally(authorizationUrl(oauthToken)) + } + + property OAuth1 oauth: OAuth1 { + id: oauth1 + property var temporaryToken + property var temporaryTokenSecret + flowType: OAuth2.AuthorizationCodeFlow + redirectUri: listener.uri + onErrorChanged: { listener.stopListening(); root.errorOccurred(error) } + onReceivedTemporaryToken: { root.receivedTemporaryToken(oauthToken, oauthTokenSecret) } + onReceivedAccessToken: { listener.stopListening(); root.receivedAccessToken(oauthToken, oauthTokenSecret) } + } + + property RedirectListener redirectListener: RedirectListener { + id: listener + timeout: 60 * 5 + httpContent: OAuthHttpContent.data + onFailed: root.errorOccurred({ "code": Error.NetworkError, + "message": "Unable to listen for redirect", + "httpCode": 0 }) + onTimedOut: root.errorOccurred({ "code": Error.TimedOutError, + "message": "Timed out waiting for redirect", + "httpCode": 0 }) + onReceivedRedirect: { + var data = oauth1.parseRedirectUri(redirectUri) + if (data.oauth_verifier && data.oauth_verifier.length) { + // give the client a chance to update customParameters etc + root.receivedTokenAuthorization(data.oauth_token, data.oauth_verifier) + oauth1.requestAccessToken(oauth1.temporaryToken, oauth1.temporaryTokenSecret, data.oauth_verifier) + } else { + stopListening() + root.errorOccurred({ "code": Error.ParseError, + "message": "Unable to parse oauth_verifier from redirect: " + redirectUri, + "httpCode": 0 }) + } + } + } +} diff --git a/usr/lib/qt5/qml/Amber/Web/Authorization/OAuth2Ac.qml b/usr/lib/qt5/qml/Amber/Web/Authorization/OAuth2Ac.qml new file mode 100644 index 00000000..0e361db5 --- /dev/null +++ b/usr/lib/qt5/qml/Amber/Web/Authorization/OAuth2Ac.qml @@ -0,0 +1,91 @@ +/**************************************************************************** +** +** BSD 3-Clause License. See LICENSE for full text. +** Copyright (c) 2021 Open Mobile Platform LLC. +** All rights reserved. +** +****************************************************************************/ + +import QtQml 2.0 +import Amber.Web.Authorization 1.0 +import "httpcontent.js" as OAuthHttpContent + +QtObject { + id: root + + property alias timeout: listener.timeout + + property alias userAgent: oauth2.userAgent + property alias redirectUri: oauth2.redirectUri + + property alias authorizationEndpoint: oauth2.authorizationEndpoint + property alias tokenEndpoint: oauth2.tokenEndpoint + property alias refreshEndpoint: oauth2.refreshEndpoint + + property alias clientId: oauth2.clientId + property alias clientSecret: oauth2.clientSecret + property var scopes + property string scopesSeparator: ' ' + property alias state: oauth2.state + + property alias customParameters: oauth2.customParameters + + // this signal is emitted after receiving the code, prior to requesting token. + signal receivedAuthorizationCode() + // this signal is emitted after receiving the token + signal receivedAccessToken(var token) + + // this signal is emitted on error + signal errorOccurred(var error) + + function authorizationUrl() { + if (redirectUri.length === 0 + || redirectUri == listener.uri) { + listener.startListening() + } + return oauth2.generateAuthorizationUrl() + } + + function authorizeInBrowser() { + Qt.openUrlExternally(authorizationUrl()) + } + + function refreshAccessToken(refreshToken) { + oauth2.refreshAccessToken(refreshToken) + } + + property OAuth2 oauth: OAuth2 { + id: oauth2 + flowType: OAuth2.AuthorizationCodeFlow + redirectUri: listener.uri + scope: generateScope(root.scopes, root.scopesSeparator) + state: generateState() + onErrorChanged: { listener.stopListening(); root.errorOccurred(error) } + onReceivedAccessToken: { listener.stopListening(); root.receivedAccessToken(token) } + } + + property RedirectListener redirectListener: RedirectListener { + id: listener + timeout: 60 * 5 + httpContent: OAuthHttpContent.data + onFailed: root.errorOccurred({ "code": Error.NetworkError, + "message": "Unable to listen for redirect", + "httpCode": 0 }) + onTimedOut: root.errorOccurred({ "code": Error.TimedOutError, + "message": "Timed out waiting for redirect", + "httpCode": 0 }) + onReceivedRedirect: { + var data = oauth2.parseRedirectUri(redirectUri) + if (data.code && data.code.length) { + // give the client a chance to update customParameters etc + root.receivedAuthorizationCode() + oauth2.requestAccessToken(data.code, data.state) + } else { + stopListening() + root.errorOccurred({ "code": Error.ParseError, + "message": "Unable to parse authorizatoin code from redirect: " + redirectUri, + "httpCode": 0 }) + } + } + } +} diff --git a/usr/lib/qt5/qml/Amber/Web/Authorization/OAuth2AcPkce.qml b/usr/lib/qt5/qml/Amber/Web/Authorization/OAuth2AcPkce.qml new file mode 100644 index 00000000..34afe2ed --- /dev/null +++ b/usr/lib/qt5/qml/Amber/Web/Authorization/OAuth2AcPkce.qml @@ -0,0 +1,95 @@ +/**************************************************************************** +** +** BSD 3-Clause License. See LICENSE for full text. +** Copyright (c) 2021 Open Mobile Platform LLC. +** All rights reserved. +** +****************************************************************************/ + +import QtQml 2.0 +import Amber.Web.Authorization 1.0 +import "httpcontent.js" as OAuthHttpContent + +QtObject { + id: root + + property alias timeout: listener.timeout + + property alias userAgent: oauth2.userAgent + property alias redirectUri: oauth2.redirectUri + + property alias authorizationEndpoint: oauth2.authorizationEndpoint + property alias tokenEndpoint: oauth2.tokenEndpoint + property alias refreshEndpoint: oauth2.refreshEndpoint + + property alias clientId: oauth2.clientId + property alias clientSecret: oauth2.clientSecret // shouldn't be required, but some services do... + property var scopes + property string scopesSeparator: ' ' + property alias state: oauth2.state + property alias codeVerifier: oauth2.codeVerifier + property alias codeChallenge: oauth2.codeChallenge + property alias codeChallengeMethod: oauth2.codeChallengeMethod + + property alias customParameters: oauth2.customParameters + + // this signal is emitted after receiving the code, prior to requesting token. + signal receivedAuthorizationCode() + // this signal is emitted after receiving the token + signal receivedAccessToken(var token) + + // this signal is emitted on error + signal errorOccurred(var error) + + function authorizationUrl() { + if (redirectUri.length === 0 + || redirectUri == listener.uri) { + listener.startListening() + } + return oauth2.generateAuthorizationUrl() + } + + function authorizeInBrowser() { + Qt.openUrlExternally(authorizationUrl()) + } + + function refreshAccessToken(refreshToken) { + oauth2.refreshAccessToken(refreshToken) + } + + property OAuth2 oauth: OAuth2 { + id: oauth2 + flowType: OAuth2.AuthorizationCodeWithPkceFlow + redirectUri: listener.uri + scope: generateScope(root.scopes, root.scopesSeparator) + state: generateState() + codeVerifier: generateCodeVerifier() + onErrorChanged: { listener.stopListening(); root.errorOccurred(error) } + onReceivedAccessToken: { listener.stopListening(); root.receivedAccessToken(token) } + } + + property RedirectListener redirectListener: RedirectListener { + id: listener + timeout: 60 * 5 + httpContent: OAuthHttpContent.data + onFailed: root.errorOccurred({ "code": Error.NetworkError, + "message": "Unable to listen for redirect", + "httpCode": 0 }) + onTimedOut: root.errorOccurred({ "code": Error.TimedOutError, + "message": "Timed out waiting for redirect", + "httpCode": 0 }) + onReceivedRedirect: { + var data = oauth2.parseRedirectUri(redirectUri) + if (data.code && data.code.length) { + // give the client a chance to update customParameters etc + root.receivedAuthorizationCode() + oauth2.requestAccessToken(data.code, data.state) + } else { + stopListening() + root.errorOccurred({ "code": Error.ParseError, + "message": "Unable to parse authorization code from redirect: " + redirectUri, + "httpCode": 0 }) + } + } + } +} diff --git a/usr/lib/qt5/qml/Amber/Web/Authorization/OAuth2Implicit.qml b/usr/lib/qt5/qml/Amber/Web/Authorization/OAuth2Implicit.qml new file mode 100644 index 00000000..215921f9 --- /dev/null +++ b/usr/lib/qt5/qml/Amber/Web/Authorization/OAuth2Implicit.qml @@ -0,0 +1,86 @@ +/**************************************************************************** +** +** BSD 3-Clause License. See LICENSE for full text. +** Copyright (c) 2021 Open Mobile Platform LLC. +** All rights reserved. +** +****************************************************************************/ + +import QtQml 2.0 +import Amber.Web.Authorization 1.0 +import "httpcontent.js" as OAuthHttpContent + +QtObject { + id: root + + property alias timeout: listener.timeout + + property alias userAgent: oauth2.userAgent + property alias redirectUri: oauth2.redirectUri + + property alias authorizationEndpoint: oauth2.authorizationEndpoint + property alias tokenEndpoint: oauth2.tokenEndpoint + property alias refreshEndpoint: oauth2.refreshEndpoint + + property alias clientId: oauth2.clientId + property alias clientSecret: oauth2.clientSecret // shouldn't be required, but some services do... + property var scopes + property string scopesSeparator: ' ' + property alias state: oauth2.state + + property alias customParameters: oauth2.customParameters + + // this signal is emitted after receiving the token + signal receivedAccessToken(var token) + + // this signal is emitted on error + signal errorOccurred(var error) + + function authorizationUrl() { + if (redirectUri.length === 0 + || redirectUri == listener.uri) { + listener.startListening() + } + return oauth2.generateAuthorizationUrl() + } + + function authorizeInBrowser() { + Qt.openUrlExternally(authorizationUrl()) + } + + function refreshAccessToken(refreshToken) { + oauth2.refreshAccessToken(refreshToken) + } + + property OAuth2 oauth: OAuth2 { + id: oauth2 + flowType: OAuth2.ImplicitFlow + scope: generateScope(root.scopes, root.scopesSeparator) + state: generateState() + onErrorChanged: { listener.stopListening(); root.errorOccurred(error) } + onReceivedAccessToken: { listener.stopListening(); root.receivedAccessToken(token) } + } + + property RedirectListener redirectListener: RedirectListener { + id: listener + timeout: 60 * 5 + httpContent: OAuthHttpContent.data + onFailed: root.errorOccurred({ "code": Error.NetworkError, + "message": "Unable to listen for redirect", + "httpCode": 0 }) + onTimedOut: root.errorOccurred({ "code": Error.TimedOutError, + "message": "Timed out waiting for redirect", + "httpCode": 0 }) + onReceivedRedirect: { + var data = oauth2.parseRedirectUri(redirectUri) + if (data.access_token && data.access_token.length) { + root.receivedAccessToken(data) + } else { + stopListening() + root.errorOccurred({ "code": Error.ParseError, + "message": "Unable to parse access token from redirect: " + redirectUri, + "httpCode": 0 }) + } + } + } +} diff --git a/usr/lib/qt5/qml/Amber/Web/Authorization/httpcontent.js b/usr/lib/qt5/qml/Amber/Web/Authorization/httpcontent.js new file mode 100644 index 00000000..d242a57d --- /dev/null +++ b/usr/lib/qt5/qml/Amber/Web/Authorization/httpcontent.js @@ -0,0 +1,26 @@ +/**************************************************************************** +** +** BSD 3-Clause License. See LICENSE for full text. +** Copyright (c) 2021 Open Mobile Platform LLC. +** All rights reserved. +** +****************************************************************************/ + +//: The default message which is displayed in the browser application after the redirect is complete +//% "Please close this browser window." +var translatedMessage = qsTrId("amber_web_authorization-la-close_external_browser") +var data = "HTTP/1.1 200 OK\r\n" + + "Content-Type: text/html\r\n" + + "Connection: close\r\n\r\n" + + "\r\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "

\n" + + translatedMessage + "\n" + + "

\n" + + "\n" + + "\r\n\r\n"; diff --git a/usr/lib/qt5/qml/Amber/Web/Authorization/qmldir b/usr/lib/qt5/qml/Amber/Web/Authorization/qmldir new file mode 100644 index 00000000..16bc4515 --- /dev/null +++ b/usr/lib/qt5/qml/Amber/Web/Authorization/qmldir @@ -0,0 +1,7 @@ +module Amber.Web.Authorization +plugin amberwebauthorizationplugin +typeinfo plugins.qmltypes +OAuth2Ac 1.0 OAuth2Ac.qml +OAuth2AcPkce 1.0 OAuth2AcPkce.qml +OAuth2Implicit 1.0 OAuth2Implicit.qml +OAuth10a 1.0 OAuth10a.qml diff --git a/usr/lib/qt5/qml/Connman/qmldir b/usr/lib/qt5/qml/Connman/qmldir new file mode 100644 index 00000000..8ac03596 --- /dev/null +++ b/usr/lib/qt5/qml/Connman/qmldir @@ -0,0 +1,3 @@ +module Connman +plugin ConnmanQtDeclarative +typeinfo plugins.qmltypes diff --git a/usr/lib/qt5/qml/QOfono/qmldir b/usr/lib/qt5/qml/QOfono/qmldir new file mode 100644 index 00000000..4c6cb487 --- /dev/null +++ b/usr/lib/qt5/qml/QOfono/qmldir @@ -0,0 +1,3 @@ +module QOfono +plugin QOfonoQtDeclarative +typeinfo plugins.qmltypes diff --git a/usr/lib/qt5/qml/Qt/labs/settings/qmldir b/usr/lib/qt5/qml/Qt/labs/settings/qmldir new file mode 100644 index 00000000..93d8e671 --- /dev/null +++ b/usr/lib/qt5/qml/Qt/labs/settings/qmldir @@ -0,0 +1,4 @@ +module Qt.labs.settings +plugin qmlsettingsplugin +classname QmlSettingsPlugin +typeinfo plugins.qmltypes diff --git a/usr/lib/qt5/qml/Qt3D/Shapes/Capsule.qml b/usr/lib/qt5/qml/Qt3D/Shapes/Capsule.qml new file mode 100644 index 00000000..6bf4d659 --- /dev/null +++ b/usr/lib/qt5/qml/Qt3D/Shapes/Capsule.qml @@ -0,0 +1,55 @@ +/**************************************************************************** +** +** Copyright (C) 2012 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** This file is part of the Qt3D module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +import QtQuick 2.0 +import Qt3D 2.0 +import Qt3D.Shapes 2.0 + +Item3D { + id: capsule + property alias name: capsuleMesh.objectName + property alias radius: capsuleMesh.radius + property alias length: capsuleMesh.length + property alias levelOfDetail: capsuleMesh.levelOfDetail + mesh: CapsuleMesh { + id: capsuleMesh + } +} diff --git a/usr/lib/qt5/qml/Qt3D/Shapes/Cube.qml b/usr/lib/qt5/qml/Qt3D/Shapes/Cube.qml new file mode 100644 index 00000000..9781d562 --- /dev/null +++ b/usr/lib/qt5/qml/Qt3D/Shapes/Cube.qml @@ -0,0 +1,48 @@ +/**************************************************************************** +** +** Copyright (C) 2012 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** This file is part of the Qt3D module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +import QtQuick 2.0 +import Qt3D 2.0 + +Item3D { + id: cube + mesh: Mesh { source: "cube.obj" } +} diff --git a/usr/lib/qt5/qml/Qt3D/Shapes/Cylinder.qml b/usr/lib/qt5/qml/Qt3D/Shapes/Cylinder.qml new file mode 100644 index 00000000..7260daef --- /dev/null +++ b/usr/lib/qt5/qml/Qt3D/Shapes/Cylinder.qml @@ -0,0 +1,55 @@ +/**************************************************************************** +** +** Copyright (C) 2012 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** This file is part of the Qt3D module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +import QtQuick 2.0 +import Qt3D 2.0 +import Qt3D.Shapes 2.0 + +Item3D { + id: cylinder + property alias name: cylinderMesh.objectName + property alias radius: cylinderMesh.radius + property alias length: cylinderMesh.length + property alias levelOfDetail: cylinderMesh.levelOfDetail + mesh: CylinderMesh { + id: cylinderMesh + } +} diff --git a/usr/lib/qt5/qml/Qt3D/Shapes/Quad.qml b/usr/lib/qt5/qml/Qt3D/Shapes/Quad.qml new file mode 100644 index 00000000..adf9bf26 --- /dev/null +++ b/usr/lib/qt5/qml/Qt3D/Shapes/Quad.qml @@ -0,0 +1,48 @@ +/**************************************************************************** +** +** Copyright (C) 2012 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** This file is part of the Qt3D module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +import QtQuick 2.0 +import Qt3D 2.0 + +Item3D { + id: quad + mesh: Mesh { source: "quad.obj" } +} diff --git a/usr/lib/qt5/qml/Qt3D/Shapes/Sphere.qml b/usr/lib/qt5/qml/Qt3D/Shapes/Sphere.qml new file mode 100644 index 00000000..b30b55cd --- /dev/null +++ b/usr/lib/qt5/qml/Qt3D/Shapes/Sphere.qml @@ -0,0 +1,55 @@ +/**************************************************************************** +** +** Copyright (C) 2012 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** This file is part of the Qt3D module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +import QtQuick 2.0 +import Qt3D 2.0 +import Qt3D.Shapes 2.0 + +Item3D { + id: sphere + property alias name: sphereMesh.objectName + property alias radius: sphereMesh.radius + property alias levelOfDetail: sphereMesh.levelOfDetail + property alias axis: sphereMesh.axis + mesh: SphereMesh { + id: sphereMesh + } +} diff --git a/usr/lib/qt5/qml/Qt3D/Shapes/Teapot.qml b/usr/lib/qt5/qml/Qt3D/Shapes/Teapot.qml new file mode 100644 index 00000000..378fa874 --- /dev/null +++ b/usr/lib/qt5/qml/Qt3D/Shapes/Teapot.qml @@ -0,0 +1,48 @@ +/**************************************************************************** +** +** Copyright (C) 2012 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** This file is part of the Qt3D module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +import QtQuick 2.0 +import Qt3D 2.0 + +Item3D { + id: teapot + mesh: Mesh { source: "teapot.bez" } +} diff --git a/usr/lib/qt5/qml/Qt3D/Shapes/qmldir b/usr/lib/qt5/qml/Qt3D/Shapes/qmldir new file mode 100644 index 00000000..15e17342 --- /dev/null +++ b/usr/lib/qt5/qml/Qt3D/Shapes/qmldir @@ -0,0 +1,7 @@ +module Qt3D.Shapes +Cube 2.0 Cube.qml +Teapot 2.0 Teapot.qml +Quad 2.0 Quad.qml +Sphere 2.0 Sphere.qml +Capsule 2.0 Capsule.qml +Cylinder 2.0 Cylinder.qml diff --git a/usr/lib/qt5/qml/Qt3D/qmldir b/usr/lib/qt5/qml/Qt3D/qmldir new file mode 100644 index 00000000..3e4ef110 --- /dev/null +++ b/usr/lib/qt5/qml/Qt3D/qmldir @@ -0,0 +1,3 @@ +module Qt3D +plugin qthreedqmlplugin +typeinfo plugins.qmltypes diff --git a/usr/lib/qt5/qml/QtContacts/qmldir b/usr/lib/qt5/qml/QtContacts/qmldir new file mode 100644 index 00000000..19d01b90 --- /dev/null +++ b/usr/lib/qt5/qml/QtContacts/qmldir @@ -0,0 +1,3 @@ +module QtContacts +plugin declarative_contacts +typeinfo plugins.qmltypes diff --git a/usr/lib/qt5/qml/QtLocation/qmldir b/usr/lib/qt5/qml/QtLocation/qmldir new file mode 100644 index 00000000..37ecf66c --- /dev/null +++ b/usr/lib/qt5/qml/QtLocation/qmldir @@ -0,0 +1,4 @@ +module QtLocation +plugin declarative_location +classname QtLocationDeclarativeModule +typeinfo plugins.qmltypes diff --git a/usr/lib/qt5/qml/QtOrganizer/qmldir b/usr/lib/qt5/qml/QtOrganizer/qmldir new file mode 100644 index 00000000..e9a279df --- /dev/null +++ b/usr/lib/qt5/qml/QtOrganizer/qmldir @@ -0,0 +1,3 @@ +module QtOrganizer +plugin declarative_organizer +typeinfo plugins.qmltypes diff --git a/usr/lib/qt5/qml/QtQml/StateMachine/qmldir b/usr/lib/qt5/qml/QtQml/StateMachine/qmldir new file mode 100644 index 00000000..8bc38312 --- /dev/null +++ b/usr/lib/qt5/qml/QtQml/StateMachine/qmldir @@ -0,0 +1,4 @@ +module QtQml.StateMachine +plugin qtqmlstatemachine +classname QtQmlStateMachinePlugin +typeinfo plugins.qmltypes diff --git a/usr/lib/qt5/qml/QtQml/qmldir b/usr/lib/qt5/qml/QtQml/qmldir new file mode 100644 index 00000000..8175ebb1 --- /dev/null +++ b/usr/lib/qt5/qml/QtQml/qmldir @@ -0,0 +1,2 @@ +module QtQml +typeinfo plugins.qmltypes diff --git a/usr/lib/qt5/qml/QtSparql/qmldir b/usr/lib/qt5/qml/QtSparql/qmldir new file mode 100644 index 00000000..1c80cb54 --- /dev/null +++ b/usr/lib/qt5/qml/QtSparql/qmldir @@ -0,0 +1,2 @@ +module QtSparql +plugin sparqlplugin diff --git a/usr/lib/qt5/qml/QtWebKit/experimental/qmldir b/usr/lib/qt5/qml/QtWebKit/experimental/qmldir deleted file mode 100644 index dfbc65cb..00000000 --- a/usr/lib/qt5/qml/QtWebKit/experimental/qmldir +++ /dev/null @@ -1,2 +0,0 @@ -module QtWebKit.experimental -plugin qmlwebkitexperimentalplugin diff --git a/usr/lib/qt5/qml/QtWebKit/qmldir b/usr/lib/qt5/qml/QtWebKit/qmldir deleted file mode 100644 index b590a78b..00000000 --- a/usr/lib/qt5/qml/QtWebKit/qmldir +++ /dev/null @@ -1,5 +0,0 @@ -module QtWebKit -plugin qmlwebkitplugin -classname WebKitQmlPlugin -typeinfo plugins.qmltypes - diff --git a/usr/lib/qt5/qml/Sailfish/Accounts/AccountIcon.qml b/usr/lib/qt5/qml/Sailfish/Accounts/AccountIcon.qml index 3b716789..c3a04657 100644 --- a/usr/lib/qt5/qml/Sailfish/Accounts/AccountIcon.qml +++ b/usr/lib/qt5/qml/Sailfish/Accounts/AccountIcon.qml @@ -1,3 +1,39 @@ +/**************************************************************************************** +** Copyright (c) 2013 - 2023 Jolla Ltd. +** +** All rights reserved. +** +** This file is part of Sailfish Accounts components package. +** +** You may use this file under the terms of BSD license as follows: +** +** Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are met: +** +** 1. Redistributions of source code must retain the above copyright notice, this +** list of conditions and the following disclaimer. +** +** 2. Redistributions in binary form must reproduce the above copyright notice, +** this list of conditions and the following disclaimer in the documentation +** and/or other materials provided with the distribution. +** +** 3. Neither the name of the copyright holder nor the names of its +** contributors may be used to endorse or promote products derived from +** this software without specific prior written permission. +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +** AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +** IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +** DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +** FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +** DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +** SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +** CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +** OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +** +****************************************************************************************/ + import QtQuick 2.0 import Sailfish.Silica 1.0 diff --git a/usr/lib/qt5/qml/Sailfish/Accounts/AccountProviderPicker.qml b/usr/lib/qt5/qml/Sailfish/Accounts/AccountProviderPicker.qml index 226a7b10..68162e55 100644 --- a/usr/lib/qt5/qml/Sailfish/Accounts/AccountProviderPicker.qml +++ b/usr/lib/qt5/qml/Sailfish/Accounts/AccountProviderPicker.qml @@ -1,9 +1,37 @@ /**************************************************************************************** -** -** Copyright (c) 2013 - 2019 Jolla Ltd. +** Copyright (c) 2013 - 2023 Jolla Ltd. ** Copyright (c) 2020 Open Mobile Platform LLC. ** -** License: Proprietary +** All rights reserved. +** +** This file is part of Sailfish Accounts components package. +** +** You may use this file under the terms of BSD license as follows: +** +** Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are met: +** +** 1. Redistributions of source code must retain the above copyright notice, this +** list of conditions and the following disclaimer. +** +** 2. Redistributions in binary form must reproduce the above copyright notice, +** this list of conditions and the following disclaimer in the documentation +** and/or other materials provided with the distribution. +** +** 3. Neither the name of the copyright holder nor the names of its +** contributors may be used to endorse or promote products derived from +** this software without specific prior written permission. +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +** AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +** IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +** DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +** FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +** DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +** SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +** CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +** OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ** ****************************************************************************************/ diff --git a/usr/lib/qt5/qml/Sailfish/Accounts/AccountProviderPickerDelegate.qml b/usr/lib/qt5/qml/Sailfish/Accounts/AccountProviderPickerDelegate.qml index 2219cdd6..413e354c 100644 --- a/usr/lib/qt5/qml/Sailfish/Accounts/AccountProviderPickerDelegate.qml +++ b/usr/lib/qt5/qml/Sailfish/Accounts/AccountProviderPickerDelegate.qml @@ -1,3 +1,39 @@ +/**************************************************************************************** +** Copyright (c) 2014 - 2023 Jolla Ltd. +** +** All rights reserved. +** +** This file is part of Sailfish Accounts components package. +** +** You may use this file under the terms of BSD license as follows: +** +** Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are met: +** +** 1. Redistributions of source code must retain the above copyright notice, this +** list of conditions and the following disclaimer. +** +** 2. Redistributions in binary form must reproduce the above copyright notice, +** this list of conditions and the following disclaimer in the documentation +** and/or other materials provided with the distribution. +** +** 3. Neither the name of the copyright holder nor the names of its +** contributors may be used to endorse or promote products derived from +** this software without specific prior written permission. +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +** AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +** IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +** DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +** FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +** DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +** SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +** CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +** OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +** +****************************************************************************************/ + import QtQuick 2.0 import Sailfish.Silica 1.0 diff --git a/usr/lib/qt5/qml/Sailfish/Accounts/AccountsFlowView.qml b/usr/lib/qt5/qml/Sailfish/Accounts/AccountsFlowView.qml index c62640dc..d1eb1762 100644 --- a/usr/lib/qt5/qml/Sailfish/Accounts/AccountsFlowView.qml +++ b/usr/lib/qt5/qml/Sailfish/Accounts/AccountsFlowView.qml @@ -1,3 +1,39 @@ +/**************************************************************************************** +** Copyright (c) 2015 - 2023 Jolla Ltd. +** +** All rights reserved. +** +** This file is part of Sailfish Accounts components package. +** +** You may use this file under the terms of BSD license as follows: +** +** Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are met: +** +** 1. Redistributions of source code must retain the above copyright notice, this +** list of conditions and the following disclaimer. +** +** 2. Redistributions in binary form must reproduce the above copyright notice, +** this list of conditions and the following disclaimer in the documentation +** and/or other materials provided with the distribution. +** +** 3. Neither the name of the copyright holder nor the names of its +** contributors may be used to endorse or promote products derived from +** this software without specific prior written permission. +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +** AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +** IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +** DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +** FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +** DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +** SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +** CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +** OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +** +****************************************************************************************/ + import QtQuick 2.0 import Sailfish.Silica 1.0 import Sailfish.Accounts 1.0 diff --git a/usr/lib/qt5/qml/Sailfish/Accounts/AccountsListDelegate.qml b/usr/lib/qt5/qml/Sailfish/Accounts/AccountsListDelegate.qml index cd00f47d..49e455f7 100644 --- a/usr/lib/qt5/qml/Sailfish/Accounts/AccountsListDelegate.qml +++ b/usr/lib/qt5/qml/Sailfish/Accounts/AccountsListDelegate.qml @@ -1,3 +1,39 @@ +/**************************************************************************************** +** Copyright (c) 2015 - 2023 Jolla Ltd. +** +** All rights reserved. +** +** This file is part of Sailfish Accounts components package. +** +** You may use this file under the terms of BSD license as follows: +** +** Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are met: +** +** 1. Redistributions of source code must retain the above copyright notice, this +** list of conditions and the following disclaimer. +** +** 2. Redistributions in binary form must reproduce the above copyright notice, +** this list of conditions and the following disclaimer in the documentation +** and/or other materials provided with the distribution. +** +** 3. Neither the name of the copyright holder nor the names of its +** contributors may be used to endorse or promote products derived from +** this software without specific prior written permission. +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +** AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +** IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +** DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +** FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +** DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +** SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +** CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +** OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +** +****************************************************************************************/ + import QtQuick 2.0 import Sailfish.Silica 1.0 import Sailfish.Accounts 1.0 @@ -94,7 +130,7 @@ ListItem { AccountIcon { id: icon x: Theme.horizontalPageMargin - anchors.verticalCenter: column.verticalCenter + y: Math.max(Theme.paddingSmall, -height / 2 + column.y + column.height / 2) source: model.accountIcon } BusyIndicator { diff --git a/usr/lib/qt5/qml/Sailfish/Accounts/AccountsListView.qml b/usr/lib/qt5/qml/Sailfish/Accounts/AccountsListView.qml index a6105a12..b1467d7b 100644 --- a/usr/lib/qt5/qml/Sailfish/Accounts/AccountsListView.qml +++ b/usr/lib/qt5/qml/Sailfish/Accounts/AccountsListView.qml @@ -1,3 +1,39 @@ +/**************************************************************************************** +** Copyright (c) 2013 - 2023 Jolla Ltd. +** +** All rights reserved. +** +** This file is part of Sailfish Accounts components package. +** +** You may use this file under the terms of BSD license as follows: +** +** Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are met: +** +** 1. Redistributions of source code must retain the above copyright notice, this +** list of conditions and the following disclaimer. +** +** 2. Redistributions in binary form must reproduce the above copyright notice, +** this list of conditions and the following disclaimer in the documentation +** and/or other materials provided with the distribution. +** +** 3. Neither the name of the copyright holder nor the names of its +** contributors may be used to endorse or promote products derived from +** this software without specific prior written permission. +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +** AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +** IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +** DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +** FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +** DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +** SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +** CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +** OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +** +****************************************************************************************/ + import QtQuick 2.0 import Sailfish.Silica 1.0 import Sailfish.Accounts 1.0 diff --git a/usr/lib/qt5/qml/Sailfish/Accounts/qmldir b/usr/lib/qt5/qml/Sailfish/Accounts/qmldir index 21eaf075..6451334d 100644 --- a/usr/lib/qt5/qml/Sailfish/Accounts/qmldir +++ b/usr/lib/qt5/qml/Sailfish/Accounts/qmldir @@ -1,5 +1,6 @@ module Sailfish.Accounts plugin sailfishaccountsplugin +typeinfo plugins.qmltypes internal AccountProviderPickerDelegate AccountProviderPickerDelegate.qml AccountProviderPicker 1.0 AccountProviderPicker.qml AccountsListView 1.0 AccountsListView.qml diff --git a/usr/lib/qt5/qml/Sailfish/Ambience/qmldir b/usr/lib/qt5/qml/Sailfish/Ambience/qmldir index a7346ec5..ac75bf29 100644 --- a/usr/lib/qt5/qml/Sailfish/Ambience/qmldir +++ b/usr/lib/qt5/qml/Sailfish/Ambience/qmldir @@ -1,5 +1,6 @@ module Sailfish.Ambience plugin declarativeambience-qt5 +typeinfo plugins.qmltypes AmbienceColorPicker 1.0 AmbienceColorPicker.qml AmbienceInfo 1.0 AmbienceInfo.qml PhotoInfo 1.0 PhotoInfo.qml diff --git a/usr/lib/qt5/qml/Sailfish/Bluetooth/BluetoothDeviceColumnView.qml b/usr/lib/qt5/qml/Sailfish/Bluetooth/BluetoothDeviceColumnView.qml index 750588d9..0ded4216 100644 --- a/usr/lib/qt5/qml/Sailfish/Bluetooth/BluetoothDeviceColumnView.qml +++ b/usr/lib/qt5/qml/Sailfish/Bluetooth/BluetoothDeviceColumnView.qml @@ -3,6 +3,9 @@ import Sailfish.Silica 1.0 import Sailfish.Bluetooth 1.0 import org.kde.bluezqt 1.0 as BluezQt +/*! + \inqmlmodule Sailfish.Bluetooth +*/ ColumnView { id: columnView @@ -16,14 +19,14 @@ ColumnView { signal removeDeviceClicked(string address) property var _connectingDevices: [] - property string _selectedDevice: "" + property string _selectedDevice //width: parent.width itemHeight: Theme.itemSizeSmall function addConnectingDevice(addr) { addr = addr.toUpperCase() - for (var i=0; i<_connectingDevices.length; i++) { + for (var i = 0; i < _connectingDevices.length; i++) { if (_connectingDevices[i].toUpperCase() == addr) { return } @@ -36,7 +39,7 @@ ColumnView { function removeConnectingDevice(addr) { addr = addr.toUpperCase() var devices = _connectingDevices - for (var i=0; i 0 + readonly property bool _showPairedDevicesHeader: showPairedDevices && showPairedDevicesHeader + && !_showDiscoveryProgress && pairedDevices.count > 0 property QtObject _devicePendingPairing property bool _autoStartDiscoveryTriggered @@ -80,6 +87,9 @@ Column { } } + /*! + \internal + */ function _deviceClicked(address, paired) { _devicePendingPairing = null selectedDevice = address @@ -104,6 +114,13 @@ Column { } } + function _deviceSettings(address) { + var device = root._bluetoothManager.deviceForAddress(address) + if (device) { + pageStack.animatorPush(Qt.resolvedUrl("PairedDeviceSettings.qml"), {"bluetoothDevice": device}) + } + } + width: parent.width Item { @@ -114,6 +131,7 @@ Column { SectionHeader { id: pairedDevicesHeader + height: discoveryProgressBar.height opacity: root._showPairedDevicesHeader ? 1.0 : 0 @@ -138,15 +156,9 @@ Column { } } - function _deviceSettings(address) { - var device = root._bluetoothManager.deviceForAddress(address) - if (device) { - pageStack.animatorPush(Qt.resolvedUrl("PairedDeviceSettings.qml"), {"bluetoothDevice": device}) - } - } - BluetoothDeviceColumnView { id: pairedDevices + filters: BluezQt.DevicesModelPrivate.PairedDevices excludedDevices: root.excludedDevices visible: root.showPairedDevices ? 1.0 : 0 @@ -179,6 +191,7 @@ Column { BluetoothDeviceColumnView { id: nearbyDevices + filters: BluezQt.DevicesModelPrivate.UnpairedDevices excludedDevices: root.excludedDevices highlightSelectedDevice: root.highlightSelectedDevice diff --git a/usr/lib/qt5/qml/Sailfish/Bluetooth/BluetoothDevicePickerDialog.qml b/usr/lib/qt5/qml/Sailfish/Bluetooth/BluetoothDevicePickerDialog.qml index d03e30b2..f1f87f50 100644 --- a/usr/lib/qt5/qml/Sailfish/Bluetooth/BluetoothDevicePickerDialog.qml +++ b/usr/lib/qt5/qml/Sailfish/Bluetooth/BluetoothDevicePickerDialog.qml @@ -10,9 +10,12 @@ import Sailfish.Silica 1.0 import Sailfish.Bluetooth 1.0 import Sailfish.Policy 1.0 import com.jolla.settings.system 1.0 -import org.nemomobile.notifications 1.0 +import Nemo.Notifications 1.0 import org.kde.bluezqt 1.0 as BluezQt +/*! + \inqmlmodule Sailfish.Bluetooth 1.0 +*/ Dialog { id: root @@ -22,11 +25,20 @@ Dialog { property alias preferredProfileHint: picker.preferredProfileHint property alias showPairedDevices: picker.showPairedDevices + /*! + \internal + */ readonly property bool _adapterPoweredOn: BluezQt.Manager.usableAdapter && BluezQt.Manager.usableAdapter.powered + /*! + \internal + */ readonly property bool _bluetoothToggleActive: AccessPolicy.bluetoothToggleEnabled || _adapterPoweredOn + /*! + \internal + */ function _tryAccept(address) { if (!_adapterPoweredOn) { adapterOffNotification.publish() diff --git a/usr/lib/qt5/qml/Sailfish/Bluetooth/BluetoothDeviceTypeComboBox.qml b/usr/lib/qt5/qml/Sailfish/Bluetooth/BluetoothDeviceTypeComboBox.qml index 5825ae99..1e57ba7c 100644 --- a/usr/lib/qt5/qml/Sailfish/Bluetooth/BluetoothDeviceTypeComboBox.qml +++ b/usr/lib/qt5/qml/Sailfish/Bluetooth/BluetoothDeviceTypeComboBox.qml @@ -1,8 +1,11 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 import Sailfish.Bluetooth 1.0 -import org.nemomobile.configuration 1.0 +import Nemo.Configuration 1.0 +/*! + \inqmlmodule Sailfish.Bluetooth 1.0 +*/ ComboBox { id: root @@ -13,6 +16,9 @@ ComboBox { visible: deviceTypesModel.count > 0 value: "" + /*! + \internal + */ function _loadIndex(index) { if (index >= 0 && index < deviceTypesModel.count) { var data = deviceTypesModel.get(index) diff --git a/usr/lib/qt5/qml/Sailfish/Bluetooth/BluetoothViewPlaceholder.qml b/usr/lib/qt5/qml/Sailfish/Bluetooth/BluetoothViewPlaceholder.qml index 13c23013..a6a86c81 100644 --- a/usr/lib/qt5/qml/Sailfish/Bluetooth/BluetoothViewPlaceholder.qml +++ b/usr/lib/qt5/qml/Sailfish/Bluetooth/BluetoothViewPlaceholder.qml @@ -2,7 +2,13 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 import org.kde.bluezqt 1.0 as BluezQt +/*! + \inqmlmodule Sailfish.Bluetooth 1.0 +*/ ViewPlaceholder { + /*! + \internal + */ property QtObject _bluetoothManager : BluezQt.Manager enabled: _bluetoothManager.adapters.length == 0 diff --git a/usr/lib/qt5/qml/Sailfish/Bluetooth/PairedDeviceSettings.qml b/usr/lib/qt5/qml/Sailfish/Bluetooth/PairedDeviceSettings.qml index 1ca90104..e0699c90 100644 --- a/usr/lib/qt5/qml/Sailfish/Bluetooth/PairedDeviceSettings.qml +++ b/usr/lib/qt5/qml/Sailfish/Bluetooth/PairedDeviceSettings.qml @@ -3,6 +3,9 @@ import Sailfish.Silica 1.0 import Sailfish.Bluetooth 1.0 import org.kde.bluezqt 1.0 as BluezQt +/*! + \inqmlmodule Sailfish.Bluetooth 1.0 +*/ Page { id: root diff --git a/usr/lib/qt5/qml/Sailfish/Bluetooth/TrustBluetoothDeviceSwitch.qml b/usr/lib/qt5/qml/Sailfish/Bluetooth/TrustBluetoothDeviceSwitch.qml index 9ffaac4f..7ef7e0a4 100644 --- a/usr/lib/qt5/qml/Sailfish/Bluetooth/TrustBluetoothDeviceSwitch.qml +++ b/usr/lib/qt5/qml/Sailfish/Bluetooth/TrustBluetoothDeviceSwitch.qml @@ -1,6 +1,9 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 +/*! + \inqmlmodule Sailfish.Bluetooth 1.0 +*/ TextSwitch { //: Whether a Bluetooth device may connect to this device without user confirmation //% "Always allow connections from this device" diff --git a/usr/lib/qt5/qml/Sailfish/Calculator/qmldir b/usr/lib/qt5/qml/Sailfish/Calculator/qmldir new file mode 100644 index 00000000..16b9ca28 --- /dev/null +++ b/usr/lib/qt5/qml/Sailfish/Calculator/qmldir @@ -0,0 +1,2 @@ +module Sailfish.Calculator +plugin sailfishcalculatorplugin diff --git a/usr/lib/qt5/qml/Sailfish/Calendar/CalendarAttendeeDelegate.qml b/usr/lib/qt5/qml/Sailfish/Calendar/CalendarAttendeeDelegate.qml index c46cc95a..a70d34f3 100644 --- a/usr/lib/qt5/qml/Sailfish/Calendar/CalendarAttendeeDelegate.qml +++ b/usr/lib/qt5/qml/Sailfish/Calendar/CalendarAttendeeDelegate.qml @@ -11,6 +11,7 @@ BackgroundItem { property string secondaryText property int participationStatus property int leftMargin: Theme.horizontalPageMargin + property int rightMargin: Theme.horizontalPageMargin height: extraText.text !== "" ? Theme.itemSizeMedium : Theme.itemSizeExtraSmall @@ -26,8 +27,8 @@ BackgroundItem { y: (root.height - height - (extraText.text !== "" ? extraText.height : 0)) / 2 width: statusIcon.status === Image.Ready - ? statusIcon.x - Theme.paddingMedium - 2*x - : root.width - 2*x + ? statusIcon.x - Theme.paddingMedium - x + : root.width - x - root.rightMargin truncationMode: TruncationMode.Fade text: root.name.length > 0 ? root.name : root.email @@ -42,7 +43,7 @@ BackgroundItem { font.pixelSize: Theme.fontSizeSmallBase color: highlighted ? palette.secondaryHighlightColor : palette.secondaryColor truncationMode: TruncationMode.Fade - width: parent.width - x + width: parent.width - x - root.rightMargin text: root.secondaryText.length > 0 ? root.secondaryText : root.name.length > 0 ? root.email : "" @@ -51,7 +52,7 @@ BackgroundItem { Icon { id: statusIcon - x: root.width - width + x: root.width - width - root.rightMargin y: nameLabel.y + (nameLabel.height - height) / 2 source: { diff --git a/usr/lib/qt5/qml/Sailfish/Calendar/CalendarEventDate.qml b/usr/lib/qt5/qml/Sailfish/Calendar/CalendarEventDate.qml index e1c67a74..d8c94ecd 100644 --- a/usr/lib/qt5/qml/Sailfish/Calendar/CalendarEventDate.qml +++ b/usr/lib/qt5/qml/Sailfish/Calendar/CalendarEventDate.qml @@ -7,6 +7,7 @@ Row { property date eventDate property bool showTime property bool timeContinued + property bool cancelled property bool useTwoLines: !fitsOneLine readonly property bool fitsOneLine: metrics.width < (maximumWidth - (timeText.visible ? (timeText.width + spacing) : 0)) property alias font: timeText.font @@ -43,6 +44,7 @@ Row { Text { visible: useTwoLines font.pixelSize: Theme.fontSizeSmall + font.strikeout: root.cancelled color: root.color text: Format.formatDate(eventDate, Format.WeekdayNameStandalone) } @@ -78,6 +80,7 @@ Row { anchors.bottom: parent.bottom visible: showTime font.pixelSize: Theme.fontSizeMedium + font.strikeout: root.cancelled color: root.color text: Format.formatDate(eventDate, Formatter.TimeValue) + (timeContinued ? " -" : "") } diff --git a/usr/lib/qt5/qml/Sailfish/Calendar/CalendarEventListDelegate.qml b/usr/lib/qt5/qml/Sailfish/Calendar/CalendarEventListDelegate.qml index a029a8aa..b7cc2ef9 100644 --- a/usr/lib/qt5/qml/Sailfish/Calendar/CalendarEventListDelegate.qml +++ b/usr/lib/qt5/qml/Sailfish/Calendar/CalendarEventListDelegate.qml @@ -35,13 +35,14 @@ ListItem { startTime: model.occurrence.startTime endTime: model.occurrence.endTime font.pixelSize: Theme.fontSizeLarge + font.strikeout: model.event.status == CalendarEvent.StatusCancelled color: root.highlighted ? Theme.secondaryHighlightColor : Theme.secondaryColor } Label { id: displayLabel width: root.width - 2*Theme.paddingMedium - Theme.paddingSmall - Theme.horizontalPageMargin + Theme.paddingMedium - text: model.event.displayLabel + text: CalendarTexts.ensureEventTitle(model.event.displayLabel) font.pixelSize: Theme.fontSizeMedium truncationMode: TruncationMode.Fade color: root.highlighted ? Theme.highlightColor : Theme.primaryColor diff --git a/usr/lib/qt5/qml/Sailfish/Calendar/CalendarEventView.qml b/usr/lib/qt5/qml/Sailfish/Calendar/CalendarEventView.qml index 06810e52..62dd58c8 100644 --- a/usr/lib/qt5/qml/Sailfish/Calendar/CalendarEventView.qml +++ b/usr/lib/qt5/qml/Sailfish/Calendar/CalendarEventView.qml @@ -10,7 +10,7 @@ import Sailfish.Silica 1.0 import Sailfish.TextLinking 1.0 import org.nemomobile.calendar 1.0 import Sailfish.Calendar 1.0 as Calendar -import org.nemomobile.notifications 1.0 as SystemNotifications +import Nemo.Notifications 1.0 as SystemNotifications Column { id: root @@ -58,7 +58,7 @@ Column { font.pixelSize: Theme.fontSizeLarge maximumLineCount: 5 wrapMode: Text.Wrap - text: root.event ? root.event.displayLabel : "" + text: Calendar.CalendarTexts.ensureEventTitle(root.event ? event.displayLabel : "") truncationMode: TruncationMode.Fade } } @@ -93,6 +93,7 @@ Column { showTime: parent.multiDay && (root.event && !root.event.allDay) timeContinued: parent.multiDay useTwoLines: timeColumn.twoLineDates + cancelled: root.event && root.event.status == CalendarEvent.StatusCancelled } CalendarEventDate { @@ -102,11 +103,13 @@ Column { eventDate: root.occurrence ? root.occurrence.endTime : new Date(-1) showTime: root.event && !root.event.allDay useTwoLines: timeColumn.twoLineDates + cancelled: root.event && root.event.status == CalendarEvent.StatusCancelled } Text { color: Theme.highlightColor font.pixelSize: Theme.fontSizeMedium + font.strikeout: root.event && root.event.status == CalendarEvent.StatusCancelled visible: !parent.multiDay //% "All day" text: root.event && root.occurrence ? (root.event.allDay ? qsTrId("sailfish_calendar-la-all_day") @@ -116,6 +119,24 @@ Column { ) : "" } + + Text { + color: Theme.highlightColor + font.pixelSize: Theme.fontSizeMedium + visible: recurrenceIcon.visible + && root.event && !isNaN(root.event.recurEndDate.getTime()) + //: %1 is a localized date string, giving the end of the recurring series. + //% "Until %1" + text: root.event ? qsTrId("sailfish_calendar-la-recurrence_end").arg(Qt.formatDate(root.event.recurEndDate)) : "" + } + + Text { + color: Theme.highlightColor + font.pixelSize: Theme.fontSizeMedium + visible: root.event && root.event.status == CalendarEvent.StatusCancelled + //% "The event is cancelled." + text: qsTrId("sailfish_calendar-la-event-cancelled") + } } Image { id: recurrenceIcon @@ -146,7 +167,8 @@ Column { //: %1 gets replaced with reminder time, e.g. "15 minutes before" //% "Reminder %1" return qsTrId("sailfish_calendar-view-reminder") - .arg(Calendar.CommonCalendarTranslations.getReminderText(root.event.reminder)) + .arg(Calendar.CalendarTexts.getReminderText(root.event.reminder, + root.event.allDay ? root.event.startTime : undefined)) } else if (root.event && !isNaN(root.event.reminderDateTime.getTime())) { //: %1 is replaced by the date in format like Monday 2nd November 2020 //: %2 is replaced by the time. @@ -169,35 +191,38 @@ Column { } } - Column { - width: parent.width - spacing: Theme.paddingMedium - - Item { - visible: root.event && root.event.location !== "" - width: parent.width - 2*Theme.horizontalPageMargin - height: Math.max(locationIcon.height, locationText.height) - x: Theme.horizontalPageMargin + BackgroundItem { + // locationRow + visible: root.event && root.event.location !== "" + width: parent.width - 2*Theme.horizontalPageMargin + height: Math.max(locationIcon.height, locationText.height) + x: Theme.horizontalPageMargin + onClicked: Qt.openUrlExternally("geo:?q=" + encodeURIComponent(locationText.text)) - Image { - id: locationIcon - source: "image://theme/icon-m-location" - } + Image { + id: locationIcon + source: "image://theme/icon-m-location" + } - Label { - id: locationText + Label { + id: locationText - width: parent.width - locationIcon.width - Theme.paddingMedium - height: contentHeight - x: locationIcon.width + Theme.paddingMedium - anchors.top: lineCount > 1 ? parent.top : undefined - anchors.verticalCenter: lineCount > 1 ? undefined : locationIcon.verticalCenter - color: Theme.highlightColor - font.pixelSize: Theme.fontSizeSmall - wrapMode: Text.Wrap - text: root.event ? root.event.location : "" - } + width: parent.width - locationIcon.width - Theme.paddingMedium + height: contentHeight + x: locationIcon.width + Theme.paddingMedium + anchors.top: lineCount > 1 ? parent.top : undefined + anchors.verticalCenter: lineCount > 1 ? undefined : locationIcon.verticalCenter + color: Theme.highlightColor + font.pixelSize: Theme.fontSizeSmall + wrapMode: Text.Wrap + text: root.event ? root.event.location : "" } + } + + Column { + // attendeeColumn + width: parent.width + spacing: Theme.paddingMedium Loader { active: cancellation @@ -351,6 +376,31 @@ Column { } } } + } + + Column { + width: parent.width + spacing: Theme.paddingMedium + + SectionHeader { + visible: syncWarning.visible + //% "Sync status" + text: qsTrId("sailfish_calendar-he-event_sync_status") + } + + SyncWarningItem { + id: syncWarning + width: parent.width - 2 * Theme.horizontalPageMargin + x: Theme.horizontalPageMargin + visible: syncFailure != CalendarEvent.NoSyncFailure + syncFailure: root.event ? root.event.syncFailure : CalendarEvent.NoSyncFailure + color: Theme.errorColor + } + + SyncFailureResolver { + event: root.event + visible: syncWarning.visible + } SectionHeader { visible: descriptionText.visible && descriptionText.text != "" @@ -384,14 +434,5 @@ Column { targetUid: (root.event && root.event.calendarUid) ? root.event.calendarUid : "" } } - - SyncWarningItem { - width: parent.width - 2 * Theme.horizontalPageMargin - x: Theme.horizontalPageMargin - syncFailure: root.event ? root.event.syncFailure : CalendarEvent.NoSyncFailure - visible: syncFailure != CalendarEvent.NoSyncFailure - withDetails: true - color: Theme.highlightColor - } } diff --git a/usr/lib/qt5/qml/Sailfish/Calendar/CalendarEventViewPage.qml b/usr/lib/qt5/qml/Sailfish/Calendar/CalendarEventViewPage.qml index abf0219b..75e1866e 100644 --- a/usr/lib/qt5/qml/Sailfish/Calendar/CalendarEventViewPage.qml +++ b/usr/lib/qt5/qml/Sailfish/Calendar/CalendarEventViewPage.qml @@ -14,8 +14,7 @@ import Nemo.DBus 2.0 Page { id: root - property alias uniqueId: query.uniqueId - property alias recurrenceId: query.recurrenceId + property alias instanceId: query.instanceId property alias startTime: query.startTimeString property alias cancellation: eventDetails.cancellation @@ -54,7 +53,7 @@ Page { //% "Show in Calendar" text: qsTrId("sailfish_calendar-me-show_event_in_calendar") onClicked: { - calendarDBusInterface.call("viewEvent", [root.uniqueId, root.recurrenceId, root.startTime]) + calendarDBusInterface.call("viewEventByIdentifier", [root.instanceId, root.startTime]) } } } @@ -67,7 +66,7 @@ Page { PageHeader { width: parent.width - title: query.event ? query.event.displayLabel : "" + title: Calendar.CalendarTexts.ensureEventTitle(query.event ? query.event.displayLabel : "") wrapMode: Text.Wrap } diff --git a/usr/lib/qt5/qml/Sailfish/Calendar/CalendarSelector.qml b/usr/lib/qt5/qml/Sailfish/Calendar/CalendarSelector.qml index 4f296fb0..9774428e 100644 --- a/usr/lib/qt5/qml/Sailfish/Calendar/CalendarSelector.qml +++ b/usr/lib/qt5/qml/Sailfish/Calendar/CalendarSelector.qml @@ -51,7 +51,7 @@ BackgroundItem { visible: source != "" } Label { - text: root.localCalendar ? CommonCalendarTranslations.getLocalCalendarName() + text: root.localCalendar ? CalendarTexts.getLocalCalendarName() : root.name color: Theme.highlightColor diff --git a/usr/lib/qt5/qml/Sailfish/Calendar/CommonCalendarTranslations.js b/usr/lib/qt5/qml/Sailfish/Calendar/CalendarTexts.js similarity index 62% rename from usr/lib/qt5/qml/Sailfish/Calendar/CommonCalendarTranslations.js rename to usr/lib/qt5/qml/Sailfish/Calendar/CalendarTexts.js index 0dedc0c3..c1bc0cd6 100644 --- a/usr/lib/qt5/qml/Sailfish/Calendar/CommonCalendarTranslations.js +++ b/usr/lib/qt5/qml/Sailfish/Calendar/CalendarTexts.js @@ -1,18 +1,48 @@ .pragma library .import org.nemomobile.calendar 1.0 as NemoCalendar +.import Sailfish.Silica 1.0 as Silica -function getReminderText(reminder) { +function getReminderText(reminder, date) { if (reminder < 0) { //% "Never" return qsTrId("sailfish_calendar-reminder-never") } else if (reminder == 0) { //% "At time of event" return qsTrId("sailfish_calendar-reminder-at_time_of_event") + } else if (date !== undefined) { + return allDayReminderTranslationText(reminder, date) } else { return customReminderTranslationText(reminder) } } +function allDayReminderTranslationText(reminder, date) { + var dt = new Date(date.getTime() - reminder * 1000) + var days = NemoCalendar.QtDate.daysTo(dt, date) + var timeStr = Silica.Format.formatDate(dt, Silica.Format.TimeValue) + + //: e.g. '5 days', count of days prior to event start. Fragment of "at HH:MM, before". + //% "%n days" + var daysStr = qsTrId("sailfish_calendar-reminder-allday_n_days", days) + //: e.g. '2 weeks', count of full weeks prior to event start. Fragment of "at HH:MM, before". + //% "%n weeks" + var weeksStr = qsTrId("sailfish_calendar-reminder-allday_n_weeks", days / 7) + + if (days > 1 && days % 7 == 0) { + //: Reminder will be triggered at a given time (%1), some full weeks (%2) before the event. + //% "at %1, %2 before" + return qsTrId("sailfish_calendar-reminder-allday_weeks_before").arg(timeStr).arg(weeksStr) + } else if (days > 1) { + //: Reminder will be triggered at a given time (%1), some days (%2) before the event. + //% "at %1, %2 before" + return qsTrId("sailfish_calendar-reminder-allday_days_before").arg(timeStr).arg(daysStr) + } else { + //: Reminder will be triggered at a given time (%1) the day before the event. + //% "at %1, the day before" + return qsTrId("sailfish_calendar-reminder-allday_before").arg(timeStr) + } +} + function customReminderTranslationText(reminder) { var secondsPerHour = 60 * 60 var secondsPerDay = 24 * secondsPerHour @@ -70,3 +100,13 @@ function getLocalCalendarName() { //% "Personal" return qsTrId("sailfish_calendar-la-personal_calendar_name") } + +function ensureEventTitle(title) { + if (!title || title.trim() == "") { + //: Fallback text for empty event title + //% "(unnamed)" + return qsTrId("sailfish_calendar-la-unnamed_event_title") + } + + return title +} diff --git a/usr/lib/qt5/qml/Sailfish/Calendar/CalendarWidget.qml b/usr/lib/qt5/qml/Sailfish/Calendar/CalendarWidget.qml index 80076123..5e37422b 100644 --- a/usr/lib/qt5/qml/Sailfish/Calendar/CalendarWidget.qml +++ b/usr/lib/qt5/qml/Sailfish/Calendar/CalendarWidget.qml @@ -9,8 +9,8 @@ import QtQuick 2.6 import Sailfish.Silica 1.0 import Sailfish.Calendar 1.0 as Calendar // QTBUG-27645 import org.nemomobile.calendar.lightweight 1.0 -import org.nemomobile.dbus 2.0 -import org.nemomobile.time 1.0 +import Nemo.DBus 2.0 +import Nemo.Time 1.0 Column { id: calendarWidget @@ -51,8 +51,8 @@ Column { } } - function showEvent(uid, recurrenceId, startDate) { - dbusInterface.call("viewEvent", [uid, recurrenceId, Qt.formatDateTime(startDate, Qt.ISODate)]) + function showEvent(instanceId, startDate) { + dbusInterface.call("viewEventByIdentifier", [instanceId, Qt.formatDateTime(startDate, Qt.ISODate)]) } function showCalendar(dateString) { @@ -183,7 +183,7 @@ Column { calendarWidget.requestUnlock() actionPending = true } else { - showEvent(uid, recurrenceId, startTime) + showEvent(instanceId, startTime) } } @@ -191,7 +191,7 @@ Column { target: calendarWidget onCheckPendingAction: { if (actionPending) { - calendarWidget.showEvent(uid, recurrenceId, startTime) + calendarWidget.showEvent(instanceId, startTime) actionPending = false } } diff --git a/usr/lib/qt5/qml/Sailfish/Calendar/CalendarWidgetDelegate.qml b/usr/lib/qt5/qml/Sailfish/Calendar/CalendarWidgetDelegate.qml index 84d95d91..63372e7b 100644 --- a/usr/lib/qt5/qml/Sailfish/Calendar/CalendarWidgetDelegate.qml +++ b/usr/lib/qt5/qml/Sailfish/Calendar/CalendarWidgetDelegate.qml @@ -8,6 +8,7 @@ import QtQuick 2.4 import Sailfish.Silica 1.0 import org.nemomobile.calendar.lightweight 1.0 +import Sailfish.Calendar 1.0 BackgroundItem { id: delegate @@ -43,6 +44,7 @@ BackgroundItem { width: Math.max(maxTimeLabelWidth, implicitWidth) color: highlighted ? Theme.secondaryHighlightColor : Theme.secondaryColor font.pixelSize: delegate.pixelSize + font.strikeout: cancelled //% "All day" text: allDay ? qsTrId("sailfish_calendar-la-all_day") : Format.formatDate(startTime, Formatter.TimeValue) @@ -60,7 +62,7 @@ BackgroundItem { width: parent.width - timeLabel.width - colorBar.width - 2*parent.spacing color: highlighted ? Theme.highlightColor : Theme.primaryColor - text: displayLabel + text: CalendarTexts.ensureEventTitle(displayLabel) truncationMode: TruncationMode.Fade font.pixelSize: delegate.pixelSize } diff --git a/usr/lib/qt5/qml/Sailfish/Calendar/SyncFailureResolver.qml b/usr/lib/qt5/qml/Sailfish/Calendar/SyncFailureResolver.qml new file mode 100644 index 00000000..7480c1b3 --- /dev/null +++ b/usr/lib/qt5/qml/Sailfish/Calendar/SyncFailureResolver.qml @@ -0,0 +1,101 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import org.nemomobile.calendar 1.0 + +Column { + id: root + property QtObject event + property int _syncFailure: event ? event.syncFailure : CalendarEvent.NoSyncFailure + + onEventChanged: combo.select(event ? event.syncFailureResolution : combo.value) + + width: parent.width + Label { + x: Theme.horizontalPageMargin + width: parent.width - 2 * x + font.pixelSize: Theme.fontSizeSmall + wrapMode: Text.Wrap + color: Theme.secondaryHighlightColor + text: { + switch (_syncFailure) { + case CalendarEvent.CreationFailure: + //% "The event created on the device failed to be copied to the server." + return qsTrId("sailfish_calendar-la-sync_failure_create") + case CalendarEvent.UploadFailure: + //% "The last modifications done on the device failed to be copied to the server." + return qsTrId("sailfish_calendar-la-sync_failure_upload") + case CalendarEvent.UpdateFailure: + //% "This event on the device does not reflect the latest modifications done on the server." + return qsTrId("sailfish_calendar-la-sync_failure_update") + case CalendarEvent.DeleteFailure: + //% "This event has been deleted on the server, but cannot be removed from the device." + return qsTrId("sailfish_calendar-la-sync_failure_delete") + case CalendarEvent.NoSyncFailure: + return "" // Won't be visible in that case + } + } + } + ComboBox { + id: combo + property int value: currentItem ? currentItem.value : CalendarEvent.RetrySync + + function select(resolution) { + switch (resolution) { + case CalendarEvent.KeepOutOfSync: + currentIndex = 1 + break + case CalendarEvent.PullServerData: + currentIndex = 2 + break + case CalendarEvent.PushDeviceData: + currentIndex = 3 + break + default: + currentIndex = 0 + break + } + } + Connections { + target: root.event + onSyncFailureResolutionChanged: combo.select(syncFailureResolution) + } + onValueChanged: { + if (!root.event || value == root.event.syncFailureResolution) + return + var modification = Calendar.createModification(root.event) + modification.syncFailureResolution = value + modification.save() + } + + //% "Action" + label: qsTrId("sailfish_calendar-cb-sync_failure_resolution") + menu: ContextMenu { + MenuItem { + property int value: CalendarEvent.RetrySync + //% "Retry on next sync" + text: qsTrId("sailfish_calendar-me-sync_resolution_retry") + } + MenuItem { + property int value: CalendarEvent.KeepOutOfSync + //% "Keep out of sync" + text: qsTrId("sailfish_calendar-me-sync_resolution_keep") + } + MenuItem { + property int value: CalendarEvent.PullServerData + visible: root._syncFailure == CalendarEvent.UploadFailure + //% "Overwrite local modifications" + text: qsTrId("sailfish_calendar-me-sync_resolution_reset_with_server") + } + MenuItem { + property int value: CalendarEvent.PushDeviceData + visible: root._syncFailure == CalendarEvent.UpdateFailure + || root._syncFailure == CalendarEvent.DeleteFailure + text: root._syncFailure == CalendarEvent.UpdateFailure + //% "Overwrite remote modifications" + ? qsTrId("sailfish_calendar-me-sync_resolution_reset_with_device") + //% "Revert remote deletion" + : qsTrId("sailfish_calendar-me-sync_resolution_reset_remote_deletion") + } + } + } +} diff --git a/usr/lib/qt5/qml/Sailfish/Calendar/SyncWarningItem.qml b/usr/lib/qt5/qml/Sailfish/Calendar/SyncWarningItem.qml index b2631b53..e6e859c1 100644 --- a/usr/lib/qt5/qml/Sailfish/Calendar/SyncWarningItem.qml +++ b/usr/lib/qt5/qml/Sailfish/Calendar/SyncWarningItem.qml @@ -6,17 +6,13 @@ Item { id: root property bool highlighted: true property int syncFailure: CalendarEvent.NoSyncFailure - property bool withDetails property alias color: syncFailureLabel.color - height: Math.max(syncFailureLabel.height, syncFailureLabel.height) + 2 * Theme.paddingSmall + height: Math.max(syncFailureLabel.height, syncFailureIcon.height) HighlightImage { id: syncFailureIcon - anchors { - verticalCenter: parent.verticalCenter - left: parent.left - } + anchors.verticalCenter: parent.verticalCenter highlighted: root.highlighted source: "image://theme/icon-s-warning" } @@ -30,27 +26,7 @@ Item { width: parent.width - syncFailureIcon.width - Theme.paddingMedium font.pixelSize: Theme.fontSizeSmall wrapMode: Text.Wrap - text: { - //% "Problem with syncing." - var head = qsTrId("sailfish_calendar-la-sync_failure") - if (!root.withDetails) { - return head - } else { - head = head + "\n" - } - switch (root.syncFailure) { - case CalendarEvent.UploadFailure: - //% "The last modifications done on device failed to be copied to the web." - return head + qsTrId("sailfish_calendar-la-sync_failure_upload") - case CalendarEvent.UpdateFailure: - //% "This event on device does not reflect the lastest modifications done on the web." - return head + qsTrId("sailfish_calendar-la-sync_failure_update") - case CalendarEvent.DeleteFailure: - //% "This event has been deleted on the web, but cannot be removed from the device." - return head + qsTrId("sailfish_calendar-la-sync_failure_delete") - case CalendarEvent.NoSyncFailure: - return "" // Won't be visible in that case - } - } + //% "Problem with syncing." + text: qsTrId("sailfish_calendar-la-sync_failure") } } diff --git a/usr/lib/qt5/qml/Sailfish/Calendar/qmldir b/usr/lib/qt5/qml/Sailfish/Calendar/qmldir index e22161f8..8d58db80 100644 --- a/usr/lib/qt5/qml/Sailfish/Calendar/qmldir +++ b/usr/lib/qt5/qml/Sailfish/Calendar/qmldir @@ -11,4 +11,5 @@ InvitationResponseButtons 1.0 InvitationResponseButtons.qml TimeRangeSelector 1.0 TimeRangeSelector.qml CalendarSelector 1.0 CalendarSelector.qml SyncWarningItem 1.0 SyncWarningItem.qml -CommonCalendarTranslations 1.0 CommonCalendarTranslations.js +SyncFailureResolver 1.0 SyncFailureResolver.qml +CalendarTexts 1.0 CalendarTexts.js diff --git a/usr/lib/qt5/qml/Sailfish/Contacts/AddressBookComboBox.qml b/usr/lib/qt5/qml/Sailfish/Contacts/AddressBookComboBox.qml index e9bc0f2d..3d126af1 100644 --- a/usr/lib/qt5/qml/Sailfish/Contacts/AddressBookComboBox.qml +++ b/usr/lib/qt5/qml/Sailfish/Contacts/AddressBookComboBox.qml @@ -10,6 +10,9 @@ import Sailfish.Accounts 1.0 import Sailfish.Contacts 1.0 as SailfishContacts import org.nemomobile.contacts 1.0 +/*! + \inqmlmodule Sailfish.Contacts +*/ IconComboBox { id: root diff --git a/usr/lib/qt5/qml/Sailfish/Contacts/AddressBookDisplayInfo.qml b/usr/lib/qt5/qml/Sailfish/Contacts/AddressBookDisplayInfo.qml index 78cf844b..9dd61de1 100644 --- a/usr/lib/qt5/qml/Sailfish/Contacts/AddressBookDisplayInfo.qml +++ b/usr/lib/qt5/qml/Sailfish/Contacts/AddressBookDisplayInfo.qml @@ -9,11 +9,20 @@ import Sailfish.Accounts 1.0 import Sailfish.Contacts 1.0 as SailfishContacts import org.nemomobile.contacts 1.0 +/*! + \inqmlmodule Sailfish.Contacts +*/ QtObject { property var addressBook property var simManager + /*! + \internal + */ readonly property var _accountProvider: SailfishContacts.ContactAccountCache.accountManager.providerForAccount(addressBook.accountId) + /*! + \internal + */ readonly property int _modemIndex: (!!simManager && addressBook.name === "SIM") ? simManager.indexOfModem(addressBook.extendedMetaData["ModemPath"]) : -1 @@ -53,6 +62,9 @@ QtObject { readonly property url iconUrl: SailfishContacts.ContactsUtil.addressBookIconUrl(addressBook, _accountProvider) + /*! + \internal + */ readonly property var _account: Account { identifier: addressBook.accountId } diff --git a/usr/lib/qt5/qml/Sailfish/Contacts/ConstituentPicker.qml b/usr/lib/qt5/qml/Sailfish/Contacts/ConstituentPicker.qml index eb25e126..d1c3332b 100644 --- a/usr/lib/qt5/qml/Sailfish/Contacts/ConstituentPicker.qml +++ b/usr/lib/qt5/qml/Sailfish/Contacts/ConstituentPicker.qml @@ -10,16 +10,25 @@ import Sailfish.Contacts 1.0 import Sailfish.Telephony 1.0 import org.nemomobile.contacts 1.0 +/*! + \inqmlmodule Sailfish.Contacts +*/ Page { id: root property var aggregateContact property var peopleModel property bool autoSelect: true + /*! + \internal + */ property var _autoSelectedId signal constituentClicked(var constituentId) + /*! + \internal + */ function _reload(contactIds) { if (autoSelect && contactIds.length === 1) { _autoSelectedId = contactIds[0] diff --git a/usr/lib/qt5/qml/Sailfish/Contacts/ContactActivityDelegate.qml b/usr/lib/qt5/qml/Sailfish/Contacts/ContactActivityDelegate.qml index fb8c0f98..d5254d0f 100644 --- a/usr/lib/qt5/qml/Sailfish/Contacts/ContactActivityDelegate.qml +++ b/usr/lib/qt5/qml/Sailfish/Contacts/ContactActivityDelegate.qml @@ -5,6 +5,9 @@ import Sailfish.Telephony 1.0 import org.nemomobile.contacts 1.0 import org.nemomobile.commhistory 1.0 +/*! + \inqmlmodule Sailfish.Contacts +*/ ListItem { id: root diff --git a/usr/lib/qt5/qml/Sailfish/Contacts/ContactActivitySimIndicator.qml b/usr/lib/qt5/qml/Sailfish/Contacts/ContactActivitySimIndicator.qml index 7e39add1..3bb7ec21 100644 --- a/usr/lib/qt5/qml/Sailfish/Contacts/ContactActivitySimIndicator.qml +++ b/usr/lib/qt5/qml/Sailfish/Contacts/ContactActivitySimIndicator.qml @@ -1,6 +1,9 @@ import QtQuick 2.6 import Sailfish.Silica 1.0 +/*! + \inqmlmodule Sailfish.Contacts +*/ Row { id: root @@ -12,6 +15,9 @@ Row { property color color: palette.secondaryColor property color highlightColor: palette.secondaryHighlightColor + /*! + \internal + */ readonly property int _modemIndex: simManager && simManager.simNames.length && imsi.length > 0 ? simManager.indexOfModemFromImsi(imsi) : -1 diff --git a/usr/lib/qt5/qml/Sailfish/Contacts/ContactAddressBookComboBox.qml b/usr/lib/qt5/qml/Sailfish/Contacts/ContactAddressBookComboBox.qml index ea059dd2..07baed19 100644 --- a/usr/lib/qt5/qml/Sailfish/Contacts/ContactAddressBookComboBox.qml +++ b/usr/lib/qt5/qml/Sailfish/Contacts/ContactAddressBookComboBox.qml @@ -7,7 +7,7 @@ import QtQuick 2.6 import Sailfish.Silica 1.0 import Sailfish.Contacts 1.0 as SailfishContacts -import org.nemomobile.configuration 1.0 +import Nemo.Configuration 1.0 AddressBookComboBox { id: root diff --git a/usr/lib/qt5/qml/Sailfish/Contacts/ContactAddressBookItem.qml b/usr/lib/qt5/qml/Sailfish/Contacts/ContactAddressBookItem.qml index ab83cd0a..bf1a58a5 100644 --- a/usr/lib/qt5/qml/Sailfish/Contacts/ContactAddressBookItem.qml +++ b/usr/lib/qt5/qml/Sailfish/Contacts/ContactAddressBookItem.qml @@ -10,6 +10,9 @@ import Sailfish.Contacts 1.0 as SailfishContacts import Sailfish.Accounts 1.0 import org.nemomobile.contacts 1.0 +/*! + \inqmlmodule Sailfish.Contacts +*/ Item { id: root @@ -21,6 +24,9 @@ Item { property int leftMargin: Theme.horizontalPageMargin property int rightMargin: Theme.horizontalPageMargin + /*! + \internal + */ readonly property bool _highlighted: highlighted width: parent.width diff --git a/usr/lib/qt5/qml/Sailfish/Contacts/ContactAvatar.qml b/usr/lib/qt5/qml/Sailfish/Contacts/ContactAvatar.qml index 86e1075c..603048d1 100644 --- a/usr/lib/qt5/qml/Sailfish/Contacts/ContactAvatar.qml +++ b/usr/lib/qt5/qml/Sailfish/Contacts/ContactAvatar.qml @@ -2,6 +2,9 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 import Sailfish.Contacts 1.0 as Contacts +/*! + \inqmlmodule Sailfish.Contacts +*/ MouseArea { id: root @@ -12,13 +15,28 @@ MouseArea { property real contentHeight: (!readOnly || avatarAvailable) ? Math.round(Screen.width / 3) : avatarImage.implicitHeight readonly property bool avatarAvailable: avatarImage.available + /*! + \internal + */ property ListModel _avatarUrlModel: ListModel {} + /*! + \internal + */ property var _avatarUrls: [] + /*! + \internal + */ property string _avatarUrl + /*! + \internal + */ property Item _contextMenu signal contactModified() + /*! + \internal + */ function _setAvatarPath(path) { contact.avatarPath = path contactModified() @@ -26,6 +44,9 @@ MouseArea { _updateAvatarMenu() } + /*! + \internal + */ function _changeAvatar() { if (_avatarUrl === '' && (!_avatarUrls.length || (_avatarUrls.length == 1 && _avatarUrls[0] == ''))) { @@ -48,6 +69,9 @@ MouseArea { _contextMenu.open(root.menuParent) } + /*! + \internal + */ function _avatarFromGallery() { // TODO fix bug: if the contact card is popped immediately after the image is selected, the contact // is not saved with the new image. @@ -60,6 +84,9 @@ MouseArea { pageStack.animatorPush(picker) } + /*! + \internal + */ function _updateAvatarModel() { // Get URLs for all avatars that are not covers _avatarUrls = contact.avatarUrlsExcluding('cover') @@ -67,6 +94,9 @@ MouseArea { _updateAvatarMenu() } + /*! + \internal + */ function _removeFileScheme(url) { var fileScheme = 'file:///' if (url && url.length >= fileScheme.length && url.substring(0, fileScheme.length) == fileScheme) { @@ -75,6 +105,9 @@ MouseArea { return url } + /*! + \internal + */ function _updateAvatarMenu() { if (_contextMenu && _contextMenu.height > 0) { // Don't update while the context menu is open diff --git a/usr/lib/qt5/qml/Sailfish/Contacts/ContactBrowser.qml b/usr/lib/qt5/qml/Sailfish/Contacts/ContactBrowser.qml index 2904b2f5..e3f82143 100644 --- a/usr/lib/qt5/qml/Sailfish/Contacts/ContactBrowser.qml +++ b/usr/lib/qt5/qml/Sailfish/Contacts/ContactBrowser.qml @@ -4,61 +4,90 @@ import Sailfish.Contacts 1.0 as SailfishContacts import org.nemomobile.commhistory 1.0 import org.nemomobile.contacts 1.0 +/*! + \inqmlmodule Sailfish.Contacts +*/ Item { id: root //--- Searching and selection properties --- - // Whether the search bar is shown + /*! Whether the search bar is shown */ property bool searchActive - // Whether the search bar can be hidden by the user + /*! Whether the search bar can be hidden by the user */ property bool canHideSearchField - // Whether contacts can be highlighted and added to the selection model + /*! Whether contacts can be highlighted and added to the selection model */ property bool canSelect - // Filter to apply to the recent contacts list + /*! Filter to apply to the recent contacts list */ property alias recentContactsCategoryMask: _recentContactsModel.eventCategoryMask - // If set, then only contacts with this property can be selected or found in a search. - // Supported properties is a combination of: PeopleModel.EmailAddressRequired, AccountUriRequired, PhoneNumberRequired + /*! + If set, then only contacts with this property can be selected or found in a search. + + Supported values are a combination of the following constants defined in PeopleModel: + \value EmailAddressRequired + \value AccountUriRequired + \value PhoneNumberRequired + */ property int requiredContactProperty: PeopleModel.NoPropertyRequired - // If true, once a contact is selected, the browser will only show other contacts that also - // have the same type. + /*! + If true, once a contact is selected, the browser will only show other contacts that also + have the same type. + */ property bool uniformSelectionTypes: true - // Properties to be made searchable as part of a search query + /*! + Properties to be made searchable as part of a search query + */ property int searchableContactProperty - // Model of the selected contacts + /*! + Model of the selected contacts + */ readonly property alias selectedContacts: contactSelectionModel //--- UI configuration: --- - // Reference to the main flickable item that presents the list of contacts. + /*! + Reference to the main flickable item that presents the list of contacts. + */ readonly property alias contactView: mainContactsList - // Page or dialog header component + /*! + Page or dialog header component + */ property Component pageHeader - // Page or dialog header object, instantiated from the component + /*! + Page or dialog header object, instantiated from the component + */ readonly property var pageHeaderItem: mainContactsList.headerItem ? mainContactsList.headerItem.pageHeaderLoader.item : null - // Margin above the view (defaults to DialogHeader height if inside a dialog) + /*! + Margin above the view (defaults to DialogHeader height if inside a dialog) + */ property real topMargin: _dialogHeaderHeight - // Full-page placeholder text to be shown when no contacts are available + /*! + Full-page placeholder text to be shown when no contacts are available + */ //: Displayed when there are no contacts //% "No people" property string placeholderText: qsTrId("components_contacts-la-no_people") - // Placeholder component shown when no contacts are available. Override to customize shown placeholder labels and actions. + /*! + Placeholder component shown when no contacts are available. Override to customize shown placeholder labels and actions. + */ property alias placeholder: placeholder.sourceComponent - // Configuration of the symbol scrollbar. + /*! + Configuration of the symbol scrollbar. + */ property alias symbolScroller: symbolScrollConfiguration SymbolScrollConfiguration { id: symbolScrollConfiguration @@ -87,7 +116,10 @@ Item { We don't simply use one model and pass it around, as that will cause delegates of the list view to be recreated, and possible save failures when editing contacts. - */ + */ + /*! + \internal + */ property var _dynamicContactsModel: PeopleModel { filterType: PeopleModel.FilterNone filterPattern: root._searchPattern @@ -121,30 +153,31 @@ Item { symbolScrollBar.resetScrollPosition() } - // Opens a menu to allow the user to select from a list of property values for the last contact - // that was clicked (or pressed+held) in the list. E.g. if requiredProperty==PeopleModel.PhoneNumberRequired, - // then a list of the contact's phone numbers is displayed. If a context menu is already - // open within the contact list, the new menu will be embedded within that instead of opening a - // separate menu. - // - // When the user makes the selection, propertySelectedCallback is called with these arguments: - // - 'contact' - the SeasidePerson* object - // - 'propertyData' - a JavaScript object describing the selected property. E.g. if a phone - // number is selected, this map contains {"property": { "number": }}. - // See common.js selectableProperties() for the possible property values. - // - 'contextMenu' - the context menu showing the list of properties, if applicable - // - 'propertyPicker' the property picker object - // - // If the contact only has one property of the required type, no menu is shown, and the callback - // function is invoked immediately with that single property value. If the contact has no - // properties of the required type, propertySelectedCallback is invoked immediately with an - // empty propertyData map. - // - // The contactId parameter ensures that the menu is shown for the intended contact - i.e. the - // last clicked/held contact. If it does not match the last clicked/held contact, this does nothing. - // - // This does nothing if requiredProperty==PeopleModel.NoPropertyRequired. - // + /*! + Opens a menu to allow the user to select from a list of property values for the last contact + that was clicked (or pressed+held) in the list. E.g. if requiredProperty==PeopleModel.PhoneNumberRequired, + then a list of the contact's phone numbers is displayed. If a context menu is already + open within the contact list, the new menu will be embedded within that instead of opening a + separate menu. + + When the user makes the selection, propertySelectedCallback is called with these arguments: + - 'contact' - the SeasidePerson* object + - 'propertyData' - a JavaScript object describing the selected property. E.g. if a phone + number is selected, this map contains {"property": { "number": }}. + See common.js selectableProperties() for the possible property values. + - 'contextMenu' - the context menu showing the list of properties, if applicable + - 'propertyPicker' the property picker object + + If the contact only has one property of the required type, no menu is shown, and the callback + function is invoked immediately with that single property value. If the contact has no + properties of the required type, propertySelectedCallback is invoked immediately with an + empty propertyData map. + + The contactId parameter ensures that the menu is shown for the intended contact - i.e. the + last clicked/held contact. If it does not match the last clicked/held contact, this does nothing. + + This does nothing if requiredProperty==PeopleModel.NoPropertyRequired. + */ function selectContactProperty(contactId, requiredProperty, propertySelectedCallback) { if (!_verifyActiveDelegateContact(contactId)) { return @@ -175,16 +208,17 @@ Item { _activeDelegate.propertyPicker.openMenu() } - // Opens a context menu for the last contact that was clicked (or pressed+held) in the list. - // Or, if selectContactProperty() has opened a page with a list of contact properties for - // the matching contact, and that page is still active, the context menu is opened for the - // last selected property. - // - // The given menu must be a Component. - // - // The contactId parameter ensures that the menu is shown for the intended contact - i.e. the - // last clicked/held contact. If it does not match the last clicked/held contact, this does nothing. - // + /*! + Opens a context menu for the last contact that was clicked (or pressed+held) in the list. + Or, if selectContactProperty() has opened a page with a list of contact properties for + the matching contact, and that page is still active, the context menu is opened for the + last selected property. + + The given menu must be a Component. + + The contactId parameter ensures that the menu is shown for the intended contact - i.e. the + last clicked/held contact. If it does not match the last clicked/held contact, this does nothing. + */ function openContextMenu(contactId, menu, menuProperties) { if (!_verifyActiveDelegateContact(contactId)) { return @@ -198,19 +232,45 @@ Item { } //--- Internal properties and functions: --- - + /*! + \internal + */ readonly property var _selectionModel: canSelect ? contactSelectionModel : null + /*! + \internal + */ property string _searchPattern + /*! + \internal + */ readonly property bool _searchFiltered: searchActive && _searchPattern.length > 0 + /*! + \internal + */ property int _filterProperty: requiredContactProperty + /*! + \internal + */ property var _activeDelegate + /*! + \internal + */ readonly property int _dialogHeaderHeight: (_isDialog && !!mainContactsList.headerItem) ? mainContactsList.headerItem.pageHeaderLoader.height : 0 + /*! + \internal + */ readonly property int _sectionHeaderHeight: Theme.itemSizeExtraSmall + /*! + \internal + */ readonly property bool _showInitialContent: favoriteContactsModel.populated && allContactsModel.populated && _recentContactsModel.ready + /*! + \internal + */ readonly property real _scrollIgnoredContentHeight: { // Ignore heights of search field and context menus so that the scrollbar doesn't // appear/disappear depending on whether these are visible. @@ -218,6 +278,9 @@ Item { + (mainContactsList.__silica_contextmenu_instance ? mainContactsList.__silica_contextmenu_instance.height : 0) } + /*! + \internal + */ readonly property var _page: { var parentItem = root.parent while (parentItem) { @@ -228,8 +291,17 @@ Item { } return null } + /*! + \internal + */ readonly property bool _isLandscape: _page && _page.isLandscape + /*! + \internal + */ readonly property bool _isDialog: _page && _page.hasOwnProperty('__silica_dialog') + /*! + \internal + */ readonly property int _pageStatus: _page ? _page.status : PageStatus.Inactive on_PageStatusChanged: { if (_pageStatus === PageStatus.Activating) { @@ -239,13 +311,22 @@ Item { } // Place any default content inside the flickable to make it easy to e.g. add pulley menus + /*! + \internal + */ default property alias _content: mainContactsList.data + /*! + \internal + */ function _closeVirtualKeyboard() { root.focus = true } // Returns the section header y for an item currently in view. + /*! + \internal + */ function _sectionHeaderY(index) { // Find the y of the desired index and return the y of the section header above it. var yPos = mainContactsList.contentY @@ -265,10 +346,16 @@ Item { return null } + /*! + \internal + */ function _clampYPos(yPos) { return Math.max(mainContactsList.originY, Math.min(yPos, mainContactsList.contentY)) } + /*! + \internal + */ function _scrollContactsTo(indexOrItem) { // Prevent the pulley menu from running its snap-back animations that automatically return // the contentY to the flickable start if movement stops within 80px of the start. @@ -316,11 +403,17 @@ Item { } } + /*! + \internal + */ function _favoriteContactsEndY() { return mainContactsList.originY + mainContactsList.headerItem.favoriteContactsSection.y + mainContactsList.headerItem.favoriteContactsSection.height } + /*! + \internal + */ function _recentContactsEndY() { return mainContactsList.originY + mainContactsList.headerItem.recentContactsSection.y + mainContactsList.headerItem.recentContactsSection.height @@ -329,6 +422,9 @@ Item { // Find the section of the first contact currently seen at the top of the view. If only half of // this contact delegate is visible, and it is also the last entry for its section, return the // following section instead. + /*! + \internal + */ function _findFirstVisibleSection() { if (!mainContactsList.headerItem) { return "" // not yet fully loaded. @@ -374,6 +470,9 @@ Item { return allContactsModel.get(contactIndex, PeopleModel.SectionBucketRole) } + /*! + \internal + */ function _resetHighlightedSymbol() { if (!_showInitialContent || displayLabelGroupModel.count === 0) { return @@ -391,6 +490,9 @@ Item { } } + /*! + \internal + */ function _setMenuOpen(menuOpen, menuItem) { symbolScrollBar.enabled = !menuOpen if (menuOpen) { @@ -400,6 +502,9 @@ Item { } } + /*! + \internal + */ function _contactClicked(contactDelegateItem, contact) { if (!contactDelegateItem) { console.log("Contact clicked but no delegate specified!") @@ -423,6 +528,9 @@ Item { contactClicked(ContactsUtil.ensureContactComplete(contact, allContactsModel)) } + /*! + \internal + */ function _contactPressAndHold(contactDelegateItem, contact) { if (!contactDelegateItem) { console.log("Contact press+hold but no delegate specified!") @@ -432,6 +540,9 @@ Item { contactPressAndHold(ContactsUtil.ensureContactComplete(contact, allContactsModel)) } + /*! + \internal + */ function _verifyActiveDelegateContact(contactId) { if (!contactId) { console.warn("Invalid contact ID") diff --git a/usr/lib/qt5/qml/Sailfish/Contacts/ContactBrowserItem.qml b/usr/lib/qt5/qml/Sailfish/Contacts/ContactBrowserItem.qml index 29f6cc1d..49100cfd 100644 --- a/usr/lib/qt5/qml/Sailfish/Contacts/ContactBrowserItem.qml +++ b/usr/lib/qt5/qml/Sailfish/Contacts/ContactBrowserItem.qml @@ -3,6 +3,9 @@ import Sailfish.Silica 1.0 import Sailfish.Contacts 1.0 as Contacts import org.nemomobile.contacts 1.0 +/*! + \inqmlmodule Sailfish.Contacts +*/ ContactItem { id: contactItem diff --git a/usr/lib/qt5/qml/Sailfish/Contacts/ContactBrowserMenu.qml b/usr/lib/qt5/qml/Sailfish/Contacts/ContactBrowserMenu.qml index 0f2cd32e..d149dff9 100644 --- a/usr/lib/qt5/qml/Sailfish/Contacts/ContactBrowserMenu.qml +++ b/usr/lib/qt5/qml/Sailfish/Contacts/ContactBrowserMenu.qml @@ -9,11 +9,17 @@ import Sailfish.Silica 1.0 import Sailfish.Contacts 1.0 import QtQuick 2.5 +/*! + \inqmlmodule Sailfish.Contacts +*/ ContextMenu { id: root property QtObject person property var peopleModel + /*! + \internal + */ property bool _favorite signal editContact() diff --git a/usr/lib/qt5/qml/Sailfish/Contacts/ContactCard.qml b/usr/lib/qt5/qml/Sailfish/Contacts/ContactCard.qml index 56b857ac..d5a61136 100644 --- a/usr/lib/qt5/qml/Sailfish/Contacts/ContactCard.qml +++ b/usr/lib/qt5/qml/Sailfish/Contacts/ContactCard.qml @@ -11,12 +11,15 @@ import Sailfish.Silica.private 1.0 import Sailfish.Telephony 1.0 import Sailfish.Contacts 1.0 as SailfishContacts import Sailfish.AccessControl 1.0 -import MeeGo.QOfono 0.2 +import QOfono 0.2 import org.nemomobile.contacts 1.0 -import org.nemomobile.dbus 2.0 +import Nemo.DBus 2.0 import "contactcard/contactcardmodelfactory.js" as ModelFactory import "contactcard" +/*! + \inqmlmodule Sailfish.Contacts +*/ SilicaFlickable { id: root @@ -27,7 +30,13 @@ SilicaFlickable { property bool hidePhoneActions: cellular1Status.modemPath.length === 0 && cellular2Status.modemPath.length === 0 || !actionPermitted property bool disablePhoneActions: !cellular1Status.registered && !cellular2Status.registered + /*! + \internal + */ property QtObject _messagesInterface + /*! + \internal + */ property date _today: new Date() function refreshDetails() { @@ -36,6 +45,9 @@ SilicaFlickable { ModelFactory.getContactCardDetailsModel(details.model, contact) } + /*! + \internal + */ function _asyncRefresh() { if (contact.complete) { contact.completeChanged.disconnect(_asyncRefresh) @@ -68,6 +80,9 @@ SilicaFlickable { return _messagesInterface } + /*! + \internal + */ function _scrollToFit(item, newItemHeight) { var newContentY = Math.max(item.mapToItem(root.contentItem, 0, newItemHeight).y - root.height, root.contentY) @@ -75,6 +90,31 @@ SilicaFlickable { repositionAnimation.start() } + function appendIfExists(query, key, value) { + if (value != "") { + return query + "&"+ key + "=" + encodeURIComponent(value) + } else { + return query + } + } + + function openAddress(street, city, region, zipcode, country) { + var content = [street, city, region, zipcode, country] + content = content.filter(function(item) { return item != "" } ) + + // q= is android extension, containing a search query. try to combine all the details there + var geoUri = "geo:0,0?q=" + encodeURIComponent(content.join(",")) + + // these are sailfish extension, having the fields separately + geoUri = appendIfExists(geoUri, "street", street) + geoUri = appendIfExists(geoUri, "city", city) + geoUri = appendIfExists(geoUri, "region", region) + geoUri = appendIfExists(geoUri, "zipcode", zipcode) + geoUri = appendIfExists(geoUri, "country", country) + + Qt.openUrlExternally(geoUri) + } + onContactChanged: { if (contact) { if (contact.complete) { @@ -181,9 +221,9 @@ SilicaFlickable { onAddressClicked: { console.log("Address: " + address) - mapsInterface.openAddress(addressParts["street"], addressParts["city"], - addressParts["region"], addressParts["zipcode"], - addressParts["country"]) + root.openAddress(addressParts["street"], addressParts["city"], + addressParts["region"], addressParts["zipcode"], + addressParts["country"]) } onCopyToClipboardClicked: { @@ -378,17 +418,6 @@ SilicaFlickable { call('dialViaModem', [ modemPath, number ]) } } - DBusInterface { - id: mapsInterface - - service: "org.sailfishos.maps" - path: "/" - iface: "org.sailfishos.maps" - - function openAddress(street, city, region, zipcode, country) { - call('openAddress', [street, city, region, zipcode, country]) - } - } DBusInterface { id: calendarInterface diff --git a/usr/lib/qt5/qml/Sailfish/Contacts/ContactCardPage.qml b/usr/lib/qt5/qml/Sailfish/Contacts/ContactCardPage.qml index b79f2ae6..b8e281eb 100644 --- a/usr/lib/qt5/qml/Sailfish/Contacts/ContactCardPage.qml +++ b/usr/lib/qt5/qml/Sailfish/Contacts/ContactCardPage.qml @@ -10,6 +10,9 @@ import Sailfish.Silica 1.0 import Sailfish.Contacts 1.0 as SailfishContacts import org.nemomobile.contacts 1.0 +/*! + \inqmlmodule Sailfish.Contacts +*/ Page { id: root @@ -21,6 +24,9 @@ Page { property alias activeDetail: contactCard.activeDetail + /*! + \internal + */ property var _unsavedContact function showError(errorText) { diff --git a/usr/lib/qt5/qml/Sailfish/Contacts/ContactCardPostSavePage.qml b/usr/lib/qt5/qml/Sailfish/Contacts/ContactCardPostSavePage.qml index 4053273f..3cb673d5 100644 --- a/usr/lib/qt5/qml/Sailfish/Contacts/ContactCardPostSavePage.qml +++ b/usr/lib/qt5/qml/Sailfish/Contacts/ContactCardPostSavePage.qml @@ -3,14 +3,23 @@ import Sailfish.Silica 1.0 import Sailfish.Contacts 1.0 as SailfishContacts import org.nemomobile.contacts 1.0 +/*! + \inqmlmodule Sailfish.Contacts +*/ ContactCardPage { id: root property int contactId property var peopleModel + /*! + \internal + */ property bool _loaded + /*! + \internal + */ function _loadSavedContact() { if (!_loaded && contactId > 0) { contact = root.peopleModel.personById(contactId) diff --git a/usr/lib/qt5/qml/Sailfish/Contacts/ContactDeleteMenuItem.qml b/usr/lib/qt5/qml/Sailfish/Contacts/ContactDeleteMenuItem.qml index 00cedc10..9886416c 100644 --- a/usr/lib/qt5/qml/Sailfish/Contacts/ContactDeleteMenuItem.qml +++ b/usr/lib/qt5/qml/Sailfish/Contacts/ContactDeleteMenuItem.qml @@ -8,6 +8,9 @@ import QtQuick 2.6 import Sailfish.Silica 1.0 import Sailfish.Contacts 1.0 +/*! + \inqmlmodule Sailfish.Contacts +*/ MenuItem { id: root diff --git a/usr/lib/qt5/qml/Sailfish/Contacts/ContactEditMenuItem.qml b/usr/lib/qt5/qml/Sailfish/Contacts/ContactEditMenuItem.qml index 04e8ee03..caa707c3 100644 --- a/usr/lib/qt5/qml/Sailfish/Contacts/ContactEditMenuItem.qml +++ b/usr/lib/qt5/qml/Sailfish/Contacts/ContactEditMenuItem.qml @@ -8,6 +8,9 @@ import QtQuick 2.6 import Sailfish.Silica 1.0 import Sailfish.Contacts 1.0 +/*! + \inqmlmodule Sailfish.Contacts +*/ MenuItem { id: root diff --git a/usr/lib/qt5/qml/Sailfish/Contacts/ContactEditorDialog.qml b/usr/lib/qt5/qml/Sailfish/Contacts/ContactEditorDialog.qml index bb27386e..604dd0f7 100644 --- a/usr/lib/qt5/qml/Sailfish/Contacts/ContactEditorDialog.qml +++ b/usr/lib/qt5/qml/Sailfish/Contacts/ContactEditorDialog.qml @@ -12,11 +12,13 @@ import Sailfish.Silica.private 1.0 as Private import org.nemomobile.contacts 1.0 import "detaileditors" -/** - * Main editor page that contains sections for each type of contact detail. - * These sections and their names are populated on this page, but each section - * populates its own data from the contact object that is passed from here to - * them. +/*! + \brief Main editor page that contains sections for each type of contact detail. + \inqmlmodule Sailfish.Contacts + + These sections and their names are populated on this page, but each section + populates its own data from the contact object that is passed from here to + them. */ Dialog { id: root @@ -25,10 +27,25 @@ Dialog { property Person subject property var focusField: ({}) + /*! + \internal + */ property var _originalContactData + /*! + \internal + */ property Person _contact: subject && subject.complete && !_readOnly ? subject : null + /*! + \internal + */ property var _peopleModel: peopleModel || SailfishContacts.ContactModelCache.unfilteredModel() + /*! + \internal + */ property var _editors: [name, company, phone, email, note, address, date, website, info] + /*! + \internal + */ readonly property bool _readOnly: !subject || !subject.complete || !ContactsUtil.isWritableContact(subject) diff --git a/usr/lib/qt5/qml/Sailfish/Contacts/ContactFavoriteModifier.qml b/usr/lib/qt5/qml/Sailfish/Contacts/ContactFavoriteModifier.qml index 9d7eabcd..a060e3ba 100644 --- a/usr/lib/qt5/qml/Sailfish/Contacts/ContactFavoriteModifier.qml +++ b/usr/lib/qt5/qml/Sailfish/Contacts/ContactFavoriteModifier.qml @@ -7,14 +7,29 @@ import QtQuick 2.6 import Sailfish.Silica 1.0 +/*! + \inqmlmodule Sailfish.Contacts +*/ QtObject { id: root property var peopleModel + /*! + \internal + */ readonly property bool lastStatus: _pendingStatus + /*! + \internal + */ readonly property bool lastStatusValid: _wasSet + /*! + \internal + */ property bool _pendingStatus + /*! + \internal + */ property bool _wasSet function setFavoriteStatus(contact, favorite) { @@ -36,6 +51,9 @@ QtObject { contact.fetchConstituents() } + /*! + \internal + */ function _applyFavoriteStatus(constituents) { var people = [] for (var i = 0; i < constituents.length; ++i) { @@ -48,6 +66,9 @@ QtObject { } } + /*! + \internal + */ property var _conn: Connections { target: null onConstituentsChanged: { diff --git a/usr/lib/qt5/qml/Sailfish/Contacts/ContactImportPage.qml b/usr/lib/qt5/qml/Sailfish/Contacts/ContactImportPage.qml index bc6f0284..5cc8adb0 100644 --- a/usr/lib/qt5/qml/Sailfish/Contacts/ContactImportPage.qml +++ b/usr/lib/qt5/qml/Sailfish/Contacts/ContactImportPage.qml @@ -1,8 +1,11 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 import Sailfish.Contacts 1.0 -import MeeGo.QOfono 0.2 +import QOfono 0.2 +/*! + \inqmlmodule Sailfish.Contacts +*/ Page { id: root @@ -13,12 +16,33 @@ Page { //=== internal/private members follow + /*! + \internal + */ property string _fileName + /*! + \internal + */ property bool _fileImport: _fileName != '' + /*! + \internal + */ property int _readCount + /*! + \internal + */ property int _savedCount + /*! + \internal + */ property bool _error + /*! + \internal + */ property var _importedContactId + /*! + \internal + */ property bool _simImportStarted onStatusChanged: { @@ -44,6 +68,9 @@ Page { } } + /*! + \internal + */ function _statusText() { if (busyIndicator.running) { if (_fileImport) { diff --git a/usr/lib/qt5/qml/Sailfish/Contacts/ContactItem.qml b/usr/lib/qt5/qml/Sailfish/Contacts/ContactItem.qml index c5ca6c9c..247bb96b 100644 --- a/usr/lib/qt5/qml/Sailfish/Contacts/ContactItem.qml +++ b/usr/lib/qt5/qml/Sailfish/Contacts/ContactItem.qml @@ -9,6 +9,9 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 import org.nemomobile.contacts 1.0 +/*! + \inqmlmodule Sailfish.Contacts +*/ ListItem { id: root @@ -27,9 +30,15 @@ ListItem { // Same as: SearchField.textLeftMargin property real searchLeftMargin: Theme.itemSizeSmall + Theme.paddingMedium + /*! + \internal + */ property bool _matchTextVisible: searchString.length > 0 && matchText.length > 0 && firstText != matchText contentHeight: _matchTextVisible ? Theme.itemSizeMedium : Theme.itemSizeSmall + /*! + \internal + */ function _regExpFor(term) { // Escape any significant chars in the search term term = term.replace(/([.?*+^$[\]\\(){}|-])/g, "\\$1") diff --git a/usr/lib/qt5/qml/Sailfish/Contacts/ContactItemGradient.qml b/usr/lib/qt5/qml/Sailfish/Contacts/ContactItemGradient.qml index 834b1732..664eb94a 100644 --- a/usr/lib/qt5/qml/Sailfish/Contacts/ContactItemGradient.qml +++ b/usr/lib/qt5/qml/Sailfish/Contacts/ContactItemGradient.qml @@ -1,6 +1,9 @@ import QtQuick 2.5 import Sailfish.Silica 1.0 +/*! + \inqmlmodule Sailfish.Contacts +*/ Rectangle { property var listItem diff --git a/usr/lib/qt5/qml/Sailfish/Contacts/ContactMultiSelectPage.qml b/usr/lib/qt5/qml/Sailfish/Contacts/ContactMultiSelectPage.qml index 82ee0dee..fe3ed5b4 100644 --- a/usr/lib/qt5/qml/Sailfish/Contacts/ContactMultiSelectPage.qml +++ b/usr/lib/qt5/qml/Sailfish/Contacts/ContactMultiSelectPage.qml @@ -3,6 +3,9 @@ import Sailfish.Silica 1.0 import Sailfish.Contacts 1.0 import org.nemomobile.contacts 1.0 +/*! + \inqmlmodule Sailfish.Contacts +*/ Page { id: root allowedOrientations: Orientation.All @@ -16,10 +19,16 @@ Page { signal shareClicked(var content) signal deleteClicked(var contactIds) + /*! + \internal + */ function _deleteSelection() { root.deleteClicked(selectedContacts.allContactIds()) } + /*! + \internal + */ function _shareSelection() { // share all of the selected contacts var vcardName = "" + root.selectedContacts.count + "-contacts.vcf" @@ -36,6 +45,9 @@ Page { root.shareClicked(content) } + /*! + \internal + */ function _doSelectionOperation(selectAll) { contactBrowser.selectedContacts.removeAllContacts() if (selectAll) { diff --git a/usr/lib/qt5/qml/Sailfish/Contacts/ContactPresenceIndicator.qml b/usr/lib/qt5/qml/Sailfish/Contacts/ContactPresenceIndicator.qml index 08e5c2a6..d2a2933d 100644 --- a/usr/lib/qt5/qml/Sailfish/Contacts/ContactPresenceIndicator.qml +++ b/usr/lib/qt5/qml/Sailfish/Contacts/ContactPresenceIndicator.qml @@ -2,6 +2,9 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 import org.nemomobile.contacts 1.0 +/*! + \inqmlmodule Sailfish.Contacts +*/ Rectangle { property int presenceState property bool offline: (presenceState === Person.PresenceUnknown) || (presenceState === Person.PresenceHidden) || (presenceState === Person.PresenceOffline) diff --git a/usr/lib/qt5/qml/Sailfish/Contacts/ContactPresenceUpdate.qml b/usr/lib/qt5/qml/Sailfish/Contacts/ContactPresenceUpdate.qml index 6f093a89..dee768d2 100644 --- a/usr/lib/qt5/qml/Sailfish/Contacts/ContactPresenceUpdate.qml +++ b/usr/lib/qt5/qml/Sailfish/Contacts/ContactPresenceUpdate.qml @@ -1,5 +1,8 @@ -import org.nemomobile.dbus 2.0 +import Nemo.DBus 2.0 +/*! + \inqmlmodule Sailfish.Contacts +*/ DBusInterface { // Request an update from the service implemented by commhistoryd service: "org.nemomobile.AccountPresence" @@ -7,6 +10,9 @@ DBusInterface { iface: "org.nemomobile.AccountPresenceIf" // 'state' should correspond to a member of SeasidePerson::PresenceState + /*! + See \l {Person::globalPresenceState} {Person.globalPresenceState} for possible values of \a state + */ function setGlobalPresence(state, message) { if (message !== undefined) { call('setGlobalPresenceWithMessage', [state, message]) diff --git a/usr/lib/qt5/qml/Sailfish/Contacts/ContactPropertyModel.qml b/usr/lib/qt5/qml/Sailfish/Contacts/ContactPropertyModel.qml index f699b667..7c8fe94e 100644 --- a/usr/lib/qt5/qml/Sailfish/Contacts/ContactPropertyModel.qml +++ b/usr/lib/qt5/qml/Sailfish/Contacts/ContactPropertyModel.qml @@ -3,22 +3,34 @@ import Sailfish.Silica 1.0 import Sailfish.Contacts 1.0 as Contacts import org.nemomobile.contacts 1.0 +/*! + \inqmlmodule Sailfish.Contacts +*/ ListModel { property int requiredProperty property QtObject contact onContactChanged: setProperties(getSelectableProperties()) + /*! + \internal + */ property var _emailUpdater: Connections { target: requiredProperty & PeopleModel.EmailAddressRequired ? contact : null onEmailDetailsChanged: setProperties(getSelectableProperties()) } + /*! + \internal + */ property var _phoneUpdater: Connections { target: requiredProperty & PeopleModel.PhoneNumberRequired ? contact : null onPhoneDetailsChanged: setProperties(getSelectableProperties()) } + /*! + \internal + */ property var _accountUpdater: Connections { target: requiredProperty & PeopleModel.AccountUriRequired ? contact : null onAccountDetailsChanged: setProperties(getSelectableProperties()) diff --git a/usr/lib/qt5/qml/Sailfish/Contacts/ContactSelectPage.qml b/usr/lib/qt5/qml/Sailfish/Contacts/ContactSelectPage.qml index f05efa11..8d8ab245 100644 --- a/usr/lib/qt5/qml/Sailfish/Contacts/ContactSelectPage.qml +++ b/usr/lib/qt5/qml/Sailfish/Contacts/ContactSelectPage.qml @@ -3,6 +3,9 @@ import Sailfish.Silica 1.0 import Sailfish.Contacts 1.0 import org.nemomobile.contacts 1.0 +/*! + \inqmlmodule Sailfish.Contacts +*/ Page { id: root allowedOrientations: Orientation.All @@ -26,6 +29,9 @@ Page { signal contactClicked(var contact, var property, string propertyType) + /*! + \internal + */ function _propertySelected(contact, propertyData, contextMenu, propertyPicker) { root.contactClicked(contact, propertyData.property, propertyData.propertyType) } diff --git a/usr/lib/qt5/qml/Sailfish/Contacts/ContactSelectionDockedPanel.qml b/usr/lib/qt5/qml/Sailfish/Contacts/ContactSelectionDockedPanel.qml index f777f10a..8d3c11de 100644 --- a/usr/lib/qt5/qml/Sailfish/Contacts/ContactSelectionDockedPanel.qml +++ b/usr/lib/qt5/qml/Sailfish/Contacts/ContactSelectionDockedPanel.qml @@ -8,6 +8,9 @@ import QtQuick 2.5 import Sailfish.Silica 1.0 +/*! + \inqmlmodule Sailfish.Contacts +*/ DockedPanel { id: root diff --git a/usr/lib/qt5/qml/Sailfish/Contacts/ContactShareAction.qml b/usr/lib/qt5/qml/Sailfish/Contacts/ContactShareAction.qml index 5e4cb2f3..106ddd89 100644 --- a/usr/lib/qt5/qml/Sailfish/Contacts/ContactShareAction.qml +++ b/usr/lib/qt5/qml/Sailfish/Contacts/ContactShareAction.qml @@ -7,6 +7,9 @@ import QtQuick 2.6 import Sailfish.Silica 1.0 import Sailfish.Share 1.0 +/*! + \inqmlmodule Sailfish.Contacts +*/ ShareAction { //% "Share contact" title: qsTrId("components_contacts-he-share_contact") diff --git a/usr/lib/qt5/qml/Sailfish/Contacts/ContactsMultiSelectDialog.qml b/usr/lib/qt5/qml/Sailfish/Contacts/ContactsMultiSelectDialog.qml index b65b99fa..cfb12c79 100644 --- a/usr/lib/qt5/qml/Sailfish/Contacts/ContactsMultiSelectDialog.qml +++ b/usr/lib/qt5/qml/Sailfish/Contacts/ContactsMultiSelectDialog.qml @@ -3,6 +3,9 @@ import Sailfish.Silica 1.0 import Sailfish.Contacts 1.0 import org.nemomobile.contacts 1.0 +/*! + \inqmlmodule Sailfish.Contacts +*/ Dialog { id: root allowedOrientations: Orientation.All @@ -14,6 +17,9 @@ Dialog { signal contactClicked(var contact, var property, string propertyType) + /*! + \internal + */ function _propertySelected(contact, propertyData, contextMenu, propertyPicker) { root.contactClicked(contact, propertyData.property, propertyData.propertyType) } diff --git a/usr/lib/qt5/qml/Sailfish/Contacts/FavoriteContactItem.qml b/usr/lib/qt5/qml/Sailfish/Contacts/FavoriteContactItem.qml index e6388a55..bfd8e8bb 100644 --- a/usr/lib/qt5/qml/Sailfish/Contacts/FavoriteContactItem.qml +++ b/usr/lib/qt5/qml/Sailfish/Contacts/FavoriteContactItem.qml @@ -11,6 +11,9 @@ import Sailfish.Contacts 1.0 as Contacts import org.nemomobile.contacts 1.0 import Nemo.Thumbnailer 1.0 +/*! + \inqmlmodule Sailfish.Contacts +*/ GridItem { id: favoriteItem @@ -20,9 +23,15 @@ GridItem { readonly property int selectionModelIndex: selectionModel !== null ? (selectionModel.count > 0, selectionModel.findContactId(model.contactId)) : -1 // count to retrigger on change. property var propertyPicker + /*! + \internal + */ property bool _hasAvatar: favoriteImage.status !== Thumbnail.Null && favoriteImage.status !== Thumbnail.Error + /*! + \internal + */ property bool _pendingDeletion: Contacts.ContactModelCache._deletingContactId === contactId property int symbolScrollBarWidth @@ -57,7 +66,7 @@ GridItem { Image { anchors.fill: parent - source: _hasAvatar ? "" : "image://theme/graphic-avatar-text-back" + source: _hasAvatar ? "" : "image://theme/graphic-grid-item-background" } Thumbnail { diff --git a/usr/lib/qt5/qml/Sailfish/Contacts/FavoritesBar.qml b/usr/lib/qt5/qml/Sailfish/Contacts/FavoritesBar.qml index 7d11806d..db846ae9 100644 --- a/usr/lib/qt5/qml/Sailfish/Contacts/FavoritesBar.qml +++ b/usr/lib/qt5/qml/Sailfish/Contacts/FavoritesBar.qml @@ -3,6 +3,9 @@ import Sailfish.Silica 1.0 import Sailfish.Contacts 1.0 import org.nemomobile.contacts 1.0 +/*! + \inqmlmodule Sailfish.Contacts +*/ Item { id: favoriteBar @@ -21,6 +24,9 @@ Item { return width / minColumnCount } + /*! + \internal + */ readonly property bool _transitionsEnabled: allowAnimations.running && !pageStack.currentPage.orientationTransitionRunning diff --git a/usr/lib/qt5/qml/Sailfish/Contacts/GlobalPresenceSwitchBar.qml b/usr/lib/qt5/qml/Sailfish/Contacts/GlobalPresenceSwitchBar.qml index d9ae7f3a..9cd95518 100644 --- a/usr/lib/qt5/qml/Sailfish/Contacts/GlobalPresenceSwitchBar.qml +++ b/usr/lib/qt5/qml/Sailfish/Contacts/GlobalPresenceSwitchBar.qml @@ -2,6 +2,9 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 import org.nemomobile.contacts 1.0 +/*! + \inqmlmodule Sailfish.Contacts +*/ Item { id: root diff --git a/usr/lib/qt5/qml/Sailfish/Contacts/PresenceDetailsPage.qml b/usr/lib/qt5/qml/Sailfish/Contacts/PresenceDetailsPage.qml index cbc712a2..0573e9ac 100644 --- a/usr/lib/qt5/qml/Sailfish/Contacts/PresenceDetailsPage.qml +++ b/usr/lib/qt5/qml/Sailfish/Contacts/PresenceDetailsPage.qml @@ -3,6 +3,9 @@ import Sailfish.Silica 1.0 import Sailfish.Contacts 1.0 as Contacts import org.nemomobile.contacts 1.0 +/*! + \inqmlmodule Sailfish.Contacts +*/ Page { id: presencePage @@ -11,6 +14,9 @@ Page { property Component presenceSwitchBar: presenceSwitchBarComponent + /*! + \internal + */ property bool _presenceAvailable function scheduleUpdatePresenceModel() { diff --git a/usr/lib/qt5/qml/Sailfish/Contacts/RecipientField.qml b/usr/lib/qt5/qml/Sailfish/Contacts/RecipientField.qml index a3ba4910..604e845c 100644 --- a/usr/lib/qt5/qml/Sailfish/Contacts/RecipientField.qml +++ b/usr/lib/qt5/qml/Sailfish/Contacts/RecipientField.qml @@ -11,6 +11,9 @@ import org.nemomobile.contacts 1.0 import "recipientfield" import Sailfish.Contacts 1.0 as Contacts +/*! + \inqmlmodule Sailfish.Contacts +*/ Item { id: root property int actionType @@ -23,18 +26,35 @@ Item { property alias onlineSearchModel: namesList.onlineSearchModel property alias onlineSearchDisplayName: namesList.onlineSearchDisplayName property bool empty: namesList.summary == "" - // Supported properties is a combination of: PeopleModel.EmailAddressRequired, AccountUriRequired, PhoneNumberRequired + /*! + Supported values are a combination of the following constants defined in PeopleModel: + \value EmailAddressRequired (default) + \value AccountUriRequired + \value PhoneNumberRequired + */ property int requiredProperty: PeopleModel.EmailAddressRequired property alias multipleAllowed: namesList.multipleAllowed property alias inputMethodHints: namesList.inputMethodHints property alias recentContactsCategoryMask: namesList.recentContactsCategoryMask - // A model with the following roles: - // "property" - an object containing the value of the property that the user chose: - // a phone number { 'number' }, an email address { 'address' }, or IM account { 'uri', 'path' } - // "propertyType" - the type of property that the user chose. Either "phoneNumber", "emailAddress" or "accountUri" - // "formattedNameText" - the name of the contact - // "person" - the Person object if the user chose from the known contacts + /*! + A model with the following roles: + \table + \row + \li property + \li an object containing the value of the property that the user chose: + a phone number { 'number' }, an email address { 'address' }, or IM account { 'uri', 'path' } + \row + \li propertyType + \li the type of property that the user chose. Either "phoneNumber", "emailAddress" or "accountUri" + \row + \li formattedNameText + \li the name of the contact + \row + \li person + \li the \l Person object if the user chose from the known contacts + \endtable + */ property QtObject selectedContacts: namesList.recipientsModel property QtObject addressesModel: addressesModelId @@ -55,6 +75,9 @@ Item { namesList.setEmailRecipients(addresses) } + /*! + \internal + */ function _addressList(contact) { return ContactsUtil.selectableProperties(contact, requiredProperty, Person) } diff --git a/usr/lib/qt5/qml/Sailfish/Contacts/SelfPresenceDetailsPage.qml b/usr/lib/qt5/qml/Sailfish/Contacts/SelfPresenceDetailsPage.qml index 95cc6b35..78fb208c 100644 --- a/usr/lib/qt5/qml/Sailfish/Contacts/SelfPresenceDetailsPage.qml +++ b/usr/lib/qt5/qml/Sailfish/Contacts/SelfPresenceDetailsPage.qml @@ -2,6 +2,9 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 import org.nemomobile.contacts 1.0 +/*! + \inqmlmodule Sailfish.Contacts +*/ PresenceDetailsPage { id: presencePage diff --git a/usr/lib/qt5/qml/Sailfish/Contacts/common/MessagesInterface.qml b/usr/lib/qt5/qml/Sailfish/Contacts/common/MessagesInterface.qml index 8c664aa2..3f1f023f 100644 --- a/usr/lib/qt5/qml/Sailfish/Contacts/common/MessagesInterface.qml +++ b/usr/lib/qt5/qml/Sailfish/Contacts/common/MessagesInterface.qml @@ -1,4 +1,4 @@ -import org.nemomobile.dbus 2.0 +import Nemo.DBus 2.0 DBusInterface { service: "org.sailfishos.Messages" diff --git a/usr/lib/qt5/qml/Sailfish/Contacts/qmldir b/usr/lib/qt5/qml/Sailfish/Contacts/qmldir index f5da2056..fe10c246 100644 --- a/usr/lib/qt5/qml/Sailfish/Contacts/qmldir +++ b/usr/lib/qt5/qml/Sailfish/Contacts/qmldir @@ -1,5 +1,6 @@ module Sailfish.Contacts plugin sailfishcontactsplugin +typeinfo plugins.qmltypes singleton ContactCreator 1.0 ContactCreator.qml singleton ContactModelCache 1.0 ContactModelCache.qml singleton ContactAccountCache 1.0 ContactAccountCache.qml diff --git a/usr/lib/qt5/qml/Sailfish/Contacts/recipientfield/AutoCompleteField.qml b/usr/lib/qt5/qml/Sailfish/Contacts/recipientfield/AutoCompleteField.qml index ed3c1af2..7df06769 100644 --- a/usr/lib/qt5/qml/Sailfish/Contacts/recipientfield/AutoCompleteField.qml +++ b/usr/lib/qt5/qml/Sailfish/Contacts/recipientfield/AutoCompleteField.qml @@ -79,7 +79,7 @@ Item { function updateModelText() { addressesModel.contact = null - if (model.index != -1 && !readOnly) { + if (model.index != -1 && model.formattedNameText == "") { text = trimmedText recipientsModel.updateRecipientAddress(model.index, text) } @@ -90,7 +90,6 @@ Item { width: parent.width - actionButton.width textRightMargin: Theme.horizontalPageMargin - Theme.paddingLarge + 2 * Theme.paddingSmall label: placeholderText - readOnly: model.formattedNameText != "" onReadOnlyChanged: { if (readOnly) { focus = false @@ -109,6 +108,7 @@ Item { contact ? contact.displayLabel : '', contact) text = model.formattedNameText recipientsModel.nextRecipient(model.index) + readOnly = true } function updateFromKnownContact(item, name, email) { @@ -158,6 +158,9 @@ Item { Component.onCompleted: { text = textValue() + if (model.formattedNameText != "") { + readOnly = true + } // TODO: Replace with "Keys.onPressed" once JB#16601 is implemented. inputField._editor.Keys.pressed.connect(function(event) { if (event.key === Qt.Key_Backspace) { diff --git a/usr/lib/qt5/qml/Sailfish/Contacts/recipientfield/OnlineSearchItem.qml b/usr/lib/qt5/qml/Sailfish/Contacts/recipientfield/OnlineSearchItem.qml index 759e6962..b134ad3a 100644 --- a/usr/lib/qt5/qml/Sailfish/Contacts/recipientfield/OnlineSearchItem.qml +++ b/usr/lib/qt5/qml/Sailfish/Contacts/recipientfield/OnlineSearchItem.qml @@ -5,7 +5,7 @@ */ import QtQuick 2.0 -import MeeGo.Connman 0.2 +import Connman 0.2 import Nemo.Notifications 1.0 import Nemo.DBus 2.0 import Sailfish.Contacts 1.0 as Contacts diff --git a/usr/lib/qt5/qml/Sailfish/Crypto/qmldir b/usr/lib/qt5/qml/Sailfish/Crypto/qmldir index c0fb5b32..789e61fb 100644 --- a/usr/lib/qt5/qml/Sailfish/Crypto/qmldir +++ b/usr/lib/qt5/qml/Sailfish/Crypto/qmldir @@ -1,2 +1,3 @@ module Sailfish.Crypto plugin sailfishcryptoplugin +typeinfo plugins.qmltypes diff --git a/usr/lib/qt5/qml/Sailfish/Encryption/EncryptionService.qml b/usr/lib/qt5/qml/Sailfish/Encryption/EncryptionService.qml new file mode 100644 index 00000000..ce5cbf37 --- /dev/null +++ b/usr/lib/qt5/qml/Sailfish/Encryption/EncryptionService.qml @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2019 Jolla Ltd. + * + * License: Proprietary + */ + +import QtQuick 2.0 +import Nemo.DBus 2.0 +import Nemo.FileManager 1.0 +import Nemo.Configuration 1.0 +import Sailfish.Encryption 1.0 + +DBusInterface { + id: encryptionService + + bus: DBus.SystemBus + service: "org.sailfishos.EncryptionService" + path: "/org/sailfishos/EncryptionService" + iface: "org.sailfishos.EncryptionService" + signalsEnabled: true + // Prevents automatic introspection but simplifies the code otherwise + watchServiceStatus: true + + property string errorString + property string errorMessage + property int encryptionStatus + property bool serviceSeen + readonly property bool encryptionWanted: encryptHome.exists && (status !== DBusInterface.Unavailable || serviceSeen) + readonly property bool available: encryptHome.exists && (status === DBusInterface.Available || serviceSeen) + readonly property bool busy: encryptionWanted && encryptionStatus == EncryptionStatus.Busy + + onStatusChanged: if (status === DBusInterface.Available) serviceSeen = true + + // DBusInterface is a QObject so no child items + property FileWatcher encryptHome: FileWatcher { + id: encryptHome + fileName: "/var/lib/sailfish-device-encryption/encrypt-home" + } + + // This introspects the interface. Thus, starting the dbus service. + readonly property DBusInterface introspectAtStart: DBusInterface { + bus: DBus.SystemBus + service: encryptionService.service + path: encryptionService.path + iface: "org.freedesktop.DBus.Introspectable" + Component.onCompleted: call("Introspect") + } + + onAvailableChanged: { + // Move to busy state right after service is available. So that + // user do not see text change from Idle to Busy (encryption is started + // when we hit the PleaseWaitPage). + if (available) { + encryptionStatus = EncryptionStatus.Busy + } + } + + function encrypt() { + call("BeginEncryption", undefined, + function() { + encryptionStatus = EncryptionStatus.Busy + }, + function(error, message) { + errorString = error + errorMessage = message + encryptionStatus = EncryptionStatus.Error + } + ) + } + + function finalize() { + call("FinalizeEncryption") + } + + function prepare(passphrase, overwriteType) { + call("PrepareToEncrypt", [passphrase, overwriteType]) + } + + function encryptionFinished(success, error) { + encryptionStatus = success ? EncryptionStatus.Encrypted : EncryptionStatus.Error + } +} diff --git a/usr/lib/qt5/qml/Sailfish/Encryption/qmldir b/usr/lib/qt5/qml/Sailfish/Encryption/qmldir new file mode 100644 index 00000000..7d62bfbd --- /dev/null +++ b/usr/lib/qt5/qml/Sailfish/Encryption/qmldir @@ -0,0 +1,4 @@ +module Sailfish.Encryption +plugin settingsencryptionplugin +typeinfo plugins.qmltypes +EncryptionService 1.0 EncryptionService.qml diff --git a/usr/lib/qt5/qml/Sailfish/FileManager/ArchivePage.qml b/usr/lib/qt5/qml/Sailfish/FileManager/ArchivePage.qml index 2dce689b..0daad4b8 100644 --- a/usr/lib/qt5/qml/Sailfish/FileManager/ArchivePage.qml +++ b/usr/lib/qt5/qml/Sailfish/FileManager/ArchivePage.qml @@ -1,9 +1,40 @@ -/* - * Copyright (c) 2018 – 2019 Jolla Ltd. - * Copyright (c) 2019 Open Mobile Platform LLC. - * - * License: Proprietary - */ +/**************************************************************************************** +** Copyright (c) 2018 - 2023 Jolla Ltd. +** Copyright (c) 2019 Open Mobile Platform LLC. +** +** All rights reserved. +** +** This file is part of Sailfish FileManager components package. +** +** You may use this file under the terms of BSD license as follows: +** +** Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are met: +** +** 1. Redistributions of source code must retain the above copyright notice, this +** list of conditions and the following disclaimer. +** +** 2. Redistributions in binary form must reproduce the above copyright notice, +** this list of conditions and the following disclaimer in the documentation +** and/or other materials provided with the distribution. +** +** 3. Neither the name of the copyright holder nor the names of its +** contributors may be used to endorse or promote products derived from +** this software without specific prior written permission. +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +** AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +** IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +** DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +** FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +** DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +** SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +** CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +** OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +** +****************************************************************************************/ + import QtQuick 2.5 import Sailfish.Silica 1.0 import Sailfish.FileManager 1.0 diff --git a/usr/lib/qt5/qml/Sailfish/FileManager/BusyView.qml b/usr/lib/qt5/qml/Sailfish/FileManager/BusyView.qml index bb1d0624..21129f57 100644 --- a/usr/lib/qt5/qml/Sailfish/FileManager/BusyView.qml +++ b/usr/lib/qt5/qml/Sailfish/FileManager/BusyView.qml @@ -1,3 +1,39 @@ +/**************************************************************************************** +** Copyright (c) 2018 - 2023 Jolla Ltd. +** +** All rights reserved. +** +** This file is part of Sailfish FileManager components package. +** +** You may use this file under the terms of BSD license as follows: +** +** Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are met: +** +** 1. Redistributions of source code must retain the above copyright notice, this +** list of conditions and the following disclaimer. +** +** 2. Redistributions in binary form must reproduce the above copyright notice, +** this list of conditions and the following disclaimer in the documentation +** and/or other materials provided with the distribution. +** +** 3. Neither the name of the copyright holder nor the names of its +** contributors may be used to endorse or promote products derived from +** this software without specific prior written permission. +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +** AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +** IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +** DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +** FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +** DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +** SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +** CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +** OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +** +****************************************************************************************/ + import QtQuick 2.0 import Sailfish.Silica 1.0 import Sailfish.Silica.private 1.0 as Private diff --git a/usr/lib/qt5/qml/Sailfish/FileManager/DetailsPage.qml b/usr/lib/qt5/qml/Sailfish/FileManager/DetailsPage.qml index bf53d39c..795ec18c 100644 --- a/usr/lib/qt5/qml/Sailfish/FileManager/DetailsPage.qml +++ b/usr/lib/qt5/qml/Sailfish/FileManager/DetailsPage.qml @@ -1,9 +1,40 @@ -/* - * Copyright (c) 2016 – 2019 Jolla Ltd. - * Copyright (c) 2019 Open Mobile Platform LLC. - * - * License: Proprietary - */ +/**************************************************************************************** +** Copyright (c) 2019 Open Mobile Platform LLC. +** Copyright (c) 2023 Jolla Ltd. +** +** All rights reserved. +** +** This file is part of Sailfish FileManager components package. +** +** You may use this file under the terms of BSD license as follows: +** +** Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are met: +** +** 1. Redistributions of source code must retain the above copyright notice, this +** list of conditions and the following disclaimer. +** +** 2. Redistributions in binary form must reproduce the above copyright notice, +** this list of conditions and the following disclaimer in the documentation +** and/or other materials provided with the distribution. +** +** 3. Neither the name of the copyright holder nor the names of its +** contributors may be used to endorse or promote products derived from +** this software without specific prior written permission. +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +** AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +** IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +** DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +** FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +** DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +** SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +** CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +** OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +** +****************************************************************************************/ + import QtQuick 2.0 import Sailfish.Silica 1.0 import Sailfish.FileManager 1.0 diff --git a/usr/lib/qt5/qml/Sailfish/FileManager/DirectoryPage.qml b/usr/lib/qt5/qml/Sailfish/FileManager/DirectoryPage.qml index 3fe873e5..bf45bb14 100644 --- a/usr/lib/qt5/qml/Sailfish/FileManager/DirectoryPage.qml +++ b/usr/lib/qt5/qml/Sailfish/FileManager/DirectoryPage.qml @@ -1,9 +1,40 @@ -/* - * Copyright (c) 2016 – 2019 Jolla Ltd. - * Copyright (c) 2019 - 2021 Open Mobile Platform LLC. - * - * License: Proprietary - */ +/**************************************************************************************** +** Copyright (c) 2016 – 2023 Jolla Ltd. +** Copyright (c) 2019 - 2021 Open Mobile Platform LLC. +** +** All rights reserved. +** +** This file is part of Sailfish FileManager components package. +** +** You may use this file under the terms of BSD license as follows: +** +** Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are met: +** +** 1. Redistributions of source code must retain the above copyright notice, this +** list of conditions and the following disclaimer. +** +** 2. Redistributions in binary form must reproduce the above copyright notice, +** this list of conditions and the following disclaimer in the documentation +** and/or other materials provided with the distribution. +** +** 3. Neither the name of the copyright holder nor the names of its +** contributors may be used to endorse or promote products derived from +** this software without specific prior written permission. +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +** AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +** IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +** DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +** FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +** DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +** SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +** CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +** OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +** +****************************************************************************************/ + import QtQuick 2.0 import Sailfish.Silica 1.0 import Sailfish.Share 1.0 diff --git a/usr/lib/qt5/qml/Sailfish/FileManager/ExtractorView.qml b/usr/lib/qt5/qml/Sailfish/FileManager/ExtractorView.qml index dddbf870..587e7abc 100644 --- a/usr/lib/qt5/qml/Sailfish/FileManager/ExtractorView.qml +++ b/usr/lib/qt5/qml/Sailfish/FileManager/ExtractorView.qml @@ -1,3 +1,39 @@ +/**************************************************************************************** +** Copyright (c) 2018 – 2023 Jolla Ltd. +** +** All rights reserved. +** +** This file is part of Sailfish FileManager components package. +** +** You may use this file under the terms of BSD license as follows: +** +** Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are met: +** +** 1. Redistributions of source code must retain the above copyright notice, this +** list of conditions and the following disclaimer. +** +** 2. Redistributions in binary form must reproduce the above copyright notice, +** this list of conditions and the following disclaimer in the documentation +** and/or other materials provided with the distribution. +** +** 3. Neither the name of the copyright holder nor the names of its +** contributors may be used to endorse or promote products derived from +** this software without specific prior written permission. +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +** AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +** IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +** DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +** FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +** DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +** SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +** CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +** OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +** +****************************************************************************************/ + import QtQuick 2.0 import Sailfish.Silica 1.0 import Sailfish.FileManager 1.0 diff --git a/usr/lib/qt5/qml/Sailfish/FileManager/FileInfoItem.qml b/usr/lib/qt5/qml/Sailfish/FileManager/FileInfoItem.qml index c5fbae3e..442a45d2 100644 --- a/usr/lib/qt5/qml/Sailfish/FileManager/FileInfoItem.qml +++ b/usr/lib/qt5/qml/Sailfish/FileManager/FileInfoItem.qml @@ -1,12 +1,40 @@ /**************************************************************************************** -** -** Copyright (c) 2019 Jolla Ltd. +** Copyright (c) 2019 - 2023 Jolla Ltd. ** Copyright (c) 2019 Open Mobile Platform LLC +** ** All rights reserved. ** -** License: Proprietary. +** This file is part of Sailfish FileManager components package. +** +** You may use this file under the terms of BSD license as follows: +** +** Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are met: +** +** 1. Redistributions of source code must retain the above copyright notice, this +** list of conditions and the following disclaimer. +** +** 2. Redistributions in binary form must reproduce the above copyright notice, +** this list of conditions and the following disclaimer in the documentation +** and/or other materials provided with the distribution. +** +** 3. Neither the name of the copyright holder nor the names of its +** contributors may be used to endorse or promote products derived from +** this software without specific prior written permission. +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +** AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +** IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +** DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +** FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +** DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +** SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +** CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +** OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ** ****************************************************************************************/ + import QtQuick 2.6 import Sailfish.Silica 1.0 diff --git a/usr/lib/qt5/qml/Sailfish/FileManager/FileItem.qml b/usr/lib/qt5/qml/Sailfish/FileManager/FileItem.qml index b6eab3bb..b5989806 100644 --- a/usr/lib/qt5/qml/Sailfish/FileManager/FileItem.qml +++ b/usr/lib/qt5/qml/Sailfish/FileManager/FileItem.qml @@ -1,9 +1,40 @@ -/* - * Copyright (c) 2018 – 2019 Jolla Ltd. - * Copyright (c) 2019 Open Mobile Platform LLC. - * - * License: Proprietary - */ +/**************************************************************************************** +** Copyright (c) 2018 - 2023 Jolla Ltd. +** Copyright (c) 2019 Open Mobile Platform LLC +** +** All rights reserved. +** +** This file is part of Sailfish FileManager components package. +** +** You may use this file under the terms of BSD license as follows: +** +** Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are met: +** +** 1. Redistributions of source code must retain the above copyright notice, this +** list of conditions and the following disclaimer. +** +** 2. Redistributions in binary form must reproduce the above copyright notice, +** this list of conditions and the following disclaimer in the documentation +** and/or other materials provided with the distribution. +** +** 3. Neither the name of the copyright holder nor the names of its +** contributors may be used to endorse or promote products derived from +** this software without specific prior written permission. +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +** AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +** IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +** DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +** FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +** DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +** SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +** CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +** OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +** +****************************************************************************************/ + import QtQuick 2.6 import Sailfish.Silica 1.0 diff --git a/usr/lib/qt5/qml/Sailfish/FileManager/FileManager.qml b/usr/lib/qt5/qml/Sailfish/FileManager/FileManager.qml index b5659c00..c7bfbd72 100644 --- a/usr/lib/qt5/qml/Sailfish/FileManager/FileManager.qml +++ b/usr/lib/qt5/qml/Sailfish/FileManager/FileManager.qml @@ -1,3 +1,39 @@ +/**************************************************************************************** +** Copyright (c) 2018 - 2023 Jolla Ltd. +** +** All rights reserved. +** +** This file is part of Sailfish FileManager components package. +** +** You may use this file under the terms of BSD license as follows: +** +** Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are met: +** +** 1. Redistributions of source code must retain the above copyright notice, this +** list of conditions and the following disclaimer. +** +** 2. Redistributions in binary form must reproduce the above copyright notice, +** this list of conditions and the following disclaimer in the documentation +** and/or other materials provided with the distribution. +** +** 3. Neither the name of the copyright holder nor the names of its +** contributors may be used to endorse or promote products derived from +** this software without specific prior written permission. +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +** AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +** IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +** DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +** FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +** DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +** SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +** CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +** OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +** +****************************************************************************************/ + pragma Singleton import QtQuick 2.0 diff --git a/usr/lib/qt5/qml/Sailfish/FileManager/FileManagerNotification.qml b/usr/lib/qt5/qml/Sailfish/FileManager/FileManagerNotification.qml index 6411e6bb..533d0233 100644 --- a/usr/lib/qt5/qml/Sailfish/FileManager/FileManagerNotification.qml +++ b/usr/lib/qt5/qml/Sailfish/FileManager/FileManagerNotification.qml @@ -1,9 +1,40 @@ -/* - * Copyright (c) 2018 – 2019 Jolla Ltd. - * Copyright (c) 2020 Open Mobile Platform LLC. - * - * License: Proprietary - */ +/**************************************************************************************** +** Copyright (c) 2018 - 2023 Jolla Ltd. +** Copyright (c) 2020 Open Mobile Platform LLC. +** +** All rights reserved. +** +** This file is part of Sailfish FileManager components package. +** +** You may use this file under the terms of BSD license as follows: +** +** Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are met: +** +** 1. Redistributions of source code must retain the above copyright notice, this +** list of conditions and the following disclaimer. +** +** 2. Redistributions in binary form must reproduce the above copyright notice, +** this list of conditions and the following disclaimer in the documentation +** and/or other materials provided with the distribution. +** +** 3. Neither the name of the copyright holder nor the names of its +** contributors may be used to endorse or promote products derived from +** this software without specific prior written permission. +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +** AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +** IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +** DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +** FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +** DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +** SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +** CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +** OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +** +****************************************************************************************/ + import QtQuick 2.0 import Nemo.Notifications 1.0 diff --git a/usr/lib/qt5/qml/Sailfish/FileManager/FileOperationMonitor.qml b/usr/lib/qt5/qml/Sailfish/FileManager/FileOperationMonitor.qml index 18c93e1c..8d47dfaa 100644 --- a/usr/lib/qt5/qml/Sailfish/FileManager/FileOperationMonitor.qml +++ b/usr/lib/qt5/qml/Sailfish/FileManager/FileOperationMonitor.qml @@ -1,8 +1,40 @@ -/* - * Copyright (c) 2019 Open Mobile Platform LLC. - * - * License: Proprietary - */ +/**************************************************************************************** +** Copyright (c) 2019 Open Mobile Platform LLC. +** Copyright (c) 2023 Jolla Ltd. +** +** All rights reserved. +** +** This file is part of Sailfish FileManager components package. +** +** You may use this file under the terms of BSD license as follows: +** +** Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are met: +** +** 1. Redistributions of source code must retain the above copyright notice, this +** list of conditions and the following disclaimer. +** +** 2. Redistributions in binary form must reproduce the above copyright notice, +** this list of conditions and the following disclaimer in the documentation +** and/or other materials provided with the distribution. +** +** 3. Neither the name of the copyright holder nor the names of its +** contributors may be used to endorse or promote products derived from +** this software without specific prior written permission. +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +** AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +** IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +** DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +** FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +** DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +** SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +** CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +** OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +** +****************************************************************************************/ + pragma Singleton import QtQuick 2.0 diff --git a/usr/lib/qt5/qml/Sailfish/FileManager/InternalFileItem.qml b/usr/lib/qt5/qml/Sailfish/FileManager/InternalFileItem.qml index 3db79ab6..86a4d67d 100644 --- a/usr/lib/qt5/qml/Sailfish/FileManager/InternalFileItem.qml +++ b/usr/lib/qt5/qml/Sailfish/FileManager/InternalFileItem.qml @@ -1,3 +1,39 @@ +/**************************************************************************************** +** Copyright (c) 2020 - 2023 Jolla Ltd. +** +** All rights reserved. +** +** This file is part of Sailfish FileManager components package. +** +** You may use this file under the terms of BSD license as follows: +** +** Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are met: +** +** 1. Redistributions of source code must retain the above copyright notice, this +** list of conditions and the following disclaimer. +** +** 2. Redistributions in binary form must reproduce the above copyright notice, +** this list of conditions and the following disclaimer in the documentation +** and/or other materials provided with the distribution. +** +** 3. Neither the name of the copyright holder nor the names of its +** contributors may be used to endorse or promote products derived from +** this software without specific prior written permission. +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +** AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +** IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +** DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +** FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +** DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +** SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +** CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +** OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +** +****************************************************************************************/ + import Sailfish.FileManager 1.0 FileItem { diff --git a/usr/lib/qt5/qml/Sailfish/FileManager/NewFolderDialog.qml b/usr/lib/qt5/qml/Sailfish/FileManager/NewFolderDialog.qml index bf4fb12c..e2a27254 100644 --- a/usr/lib/qt5/qml/Sailfish/FileManager/NewFolderDialog.qml +++ b/usr/lib/qt5/qml/Sailfish/FileManager/NewFolderDialog.qml @@ -1,3 +1,39 @@ +/**************************************************************************************** +** Copyright (c) 2016 - 2023 Jolla Ltd. +** +** All rights reserved. +** +** This file is part of Sailfish FileManager components package. +** +** You may use this file under the terms of BSD license as follows: +** +** Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are met: +** +** 1. Redistributions of source code must retain the above copyright notice, this +** list of conditions and the following disclaimer. +** +** 2. Redistributions in binary form must reproduce the above copyright notice, +** this list of conditions and the following disclaimer in the documentation +** and/or other materials provided with the distribution. +** +** 3. Neither the name of the copyright holder nor the names of its +** contributors may be used to endorse or promote products derived from +** this software without specific prior written permission. +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +** AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +** IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +** DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +** FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +** DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +** SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +** CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +** OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +** +****************************************************************************************/ + import QtQuick 2.0 import Sailfish.Silica 1.0 import Nemo.FileManager 1.0 diff --git a/usr/lib/qt5/qml/Sailfish/FileManager/RenameDialog.qml b/usr/lib/qt5/qml/Sailfish/FileManager/RenameDialog.qml index bcac507a..cdaabc81 100644 --- a/usr/lib/qt5/qml/Sailfish/FileManager/RenameDialog.qml +++ b/usr/lib/qt5/qml/Sailfish/FileManager/RenameDialog.qml @@ -1,8 +1,40 @@ -/* - * Copyright (c) 2020 Open Mobile Platform LLC. - * - * License: Proprietary - */ +/**************************************************************************************** +** Copyright (c) 2020 Open Mobile Platform LLC. +** Copyright (c) 2021 - 2023 Jolla Ltd. +** +** All rights reserved. +** +** This file is part of Sailfish FileManager components package. +** +** You may use this file under the terms of BSD license as follows: +** +** Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are met: +** +** 1. Redistributions of source code must retain the above copyright notice, this +** list of conditions and the following disclaimer. +** +** 2. Redistributions in binary form must reproduce the above copyright notice, +** this list of conditions and the following disclaimer in the documentation +** and/or other materials provided with the distribution. +** +** 3. Neither the name of the copyright holder nor the names of its +** contributors may be used to endorse or promote products derived from +** this software without specific prior written permission. +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +** AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +** IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +** DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +** FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +** DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +** SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +** CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +** OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +** +****************************************************************************************/ + import QtQuick 2.0 import Sailfish.Silica 1.0 import Nemo.FileManager 1.0 diff --git a/usr/lib/qt5/qml/Sailfish/FileManager/SortingPage.qml b/usr/lib/qt5/qml/Sailfish/FileManager/SortingPage.qml index 808f7f49..a8868ed2 100644 --- a/usr/lib/qt5/qml/Sailfish/FileManager/SortingPage.qml +++ b/usr/lib/qt5/qml/Sailfish/FileManager/SortingPage.qml @@ -1,3 +1,39 @@ +/**************************************************************************************** +** Copyright (c) 2016 - 2023 Jolla Ltd. +** +** All rights reserved. +** +** This file is part of Sailfish FileManager components package. +** +** You may use this file under the terms of BSD license as follows: +** +** Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are met: +** +** 1. Redistributions of source code must retain the above copyright notice, this +** list of conditions and the following disclaimer. +** +** 2. Redistributions in binary form must reproduce the above copyright notice, +** this list of conditions and the following disclaimer in the documentation +** and/or other materials provided with the distribution. +** +** 3. Neither the name of the copyright holder nor the names of its +** contributors may be used to endorse or promote products derived from +** this software without specific prior written permission. +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +** AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +** IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +** DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +** FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +** DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +** SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +** CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +** OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +** +****************************************************************************************/ + import QtQuick 2.0 import Sailfish.Silica 1.0 import Nemo.FileManager 1.0 diff --git a/usr/lib/qt5/qml/Sailfish/FileManager/qmldir b/usr/lib/qt5/qml/Sailfish/FileManager/qmldir index 7a4d6e97..1dd1b25c 100644 --- a/usr/lib/qt5/qml/Sailfish/FileManager/qmldir +++ b/usr/lib/qt5/qml/Sailfish/FileManager/qmldir @@ -1,5 +1,6 @@ module Sailfish.FileManager plugin sailfishfilemanagerplugin +typeinfo plugins.qmltypes ArchivePage 1.0 ArchivePage.qml DirectoryPage 1.0 DirectoryPage.qml SortingPage 1.0 SortingPage.qml diff --git a/usr/lib/qt5/qml/Sailfish/Gallery/DetailsPage.qml b/usr/lib/qt5/qml/Sailfish/Gallery/DetailsPage.qml index d1424e67..cd980ce2 100644 --- a/usr/lib/qt5/qml/Sailfish/Gallery/DetailsPage.qml +++ b/usr/lib/qt5/qml/Sailfish/Gallery/DetailsPage.qml @@ -5,6 +5,9 @@ import QtDocGallery 5.0 import Sailfish.Gallery.private 1.0 import "private" +/*! + \inqmlmodule Sailfish.Gallery +*/ Page { id: page @@ -79,7 +82,7 @@ Page { // Media 'duration', // Photo - 'dateTaken', 'cameraManufacturer', 'cameraModel', + 'dateTaken', 'cameraManufacturer', 'cameraModel', 'orientation', // exposureProgram is not supported by Tracker thus not enabled. // https://github.com/qtproject/qtdocgallery/blob/0b9ca223d4d5539ff09ce49a841fec4c24077830/src/gallery/qdocumentgallery.cpp#L799 'exposureTime', @@ -104,14 +107,22 @@ Page { filePathDetail.value: model.filePath fileSizeDetail.value: Format.formatFileSize(model.fileSize) typeDetail.value: model.mimeType - sizeDetail.value: formatDimensions(model.width, model.height) + sizeDetail.value: { + if (model.orientation === 90 || model.orientation === 270) { + return formatDimensions(model.height, model.width) + } else { + return formatDimensions(model.width, model.height) + } + } dateTakenDetail.value: model.dateTaken != "" ? Format.formatDate(model.dateTaken, Format.Timepoint) : "" cameraManufacturerDetail.value: model.cameraManufacturer cameraModelDetail.value: model.cameraModel - exposureTimeDetail.value: model.exposureTime + exposureTimeDetail.value: model.exposureTime != "" + ? formatExposure(model.exposureTime) + : "" fNumberDetail.value: model.fNumber != "" ? formatFNumber(model.fNumber) : "" @@ -155,16 +166,23 @@ Page { Loader { width: parent.width active: itemModel.status === DocumentGalleryModel.Error - || (itemModel.status === DocumentGalleryModel.Error && itemModel.count == 0) + || (itemModel.status === DocumentGalleryModel.Finished && itemModel.count == 0) sourceComponent: ImageDetailsItem { filePathDetail.value: fileInfo.file fileSizeDetail.value: Format.formatFileSize(fileInfo.size) typeDetail.value: fileInfo.mimeType - sizeDetail.value: metadata.valid - ? formatDimensions(metadata.width, metadata.height) - : "" - + sizeDetail.value: { + if (metadata.valid) { + if (metadata.orientation === 90 || metadata.orientation === 270) { + formatDimensions(metadata.height, metadata.width) + } else { + formatDimensions(metadata.width, metadata.height) + } + } else { + return "" + } + } FileInfo { id: fileInfo @@ -178,7 +196,6 @@ Page { } } } - } VerticalScrollDecorator { } diff --git a/usr/lib/qt5/qml/Sailfish/Gallery/GalleryMediaPlayer.qml b/usr/lib/qt5/qml/Sailfish/Gallery/GalleryMediaPlayer.qml index d73c12b4..271b55b5 100644 --- a/usr/lib/qt5/qml/Sailfish/Gallery/GalleryMediaPlayer.qml +++ b/usr/lib/qt5/qml/Sailfish/Gallery/GalleryMediaPlayer.qml @@ -1,21 +1,30 @@ import QtQuick 2.0 import QtMultimedia 5.0 import Sailfish.Media 1.0 -import org.nemomobile.policy 1.0 +import Nemo.Policy 1.0 import Nemo.KeepAlive 1.2 import Nemo.Notifications 1.0 +/*! + \inqmlmodule Sailfish.Gallery +*/ MediaPlayer { id: root property bool busy onLoadedChanged: if (loaded) playerLoader.anchors.centerIn = currentItem + /*! + \internal + */ property bool _minimizedPlaying property alias active: permissions.enabled readonly property bool playing: playbackState == MediaPlayer.PlayingState readonly property bool loaded: status >= MediaPlayer.Loaded && status <= MediaPlayer.EndOfMedia readonly property bool hasError: error !== MediaPlayer.NoError + /*! + \internal + */ property bool _reseting signal displayError @@ -64,12 +73,18 @@ MediaPlayer { _reseting = false } + /*! + \internal + */ property QtObject _errorNotification: Notification { isTransient: true urgency: Notification.Critical icon: "icon-system-warning" } + /*! + \internal + */ property Item _content: Item { Binding { target: root diff --git a/usr/lib/qt5/qml/Sailfish/Gallery/GalleryOverlay.qml b/usr/lib/qt5/qml/Sailfish/Gallery/GalleryOverlay.qml index 3e409218..a0a5bf7a 100644 --- a/usr/lib/qt5/qml/Sailfish/Gallery/GalleryOverlay.qml +++ b/usr/lib/qt5/qml/Sailfish/Gallery/GalleryOverlay.qml @@ -7,6 +7,9 @@ import Sailfish.Ambience 1.0 import Sailfish.Share 1.0 import Nemo.FileManager 1.0 +/*! + \inqmlmodule Sailfish.Gallery +*/ Item { id: overlay @@ -33,6 +36,9 @@ Item { property bool isImage property bool error property int duration: 1 + /*! + \internal + */ readonly property int _duration: { if (player && player.loaded) { return player.duration / 1000 @@ -40,6 +46,9 @@ Item { return duration } } + /*! + \internal + */ property Item _remorsePopup function remorseAction(text, action) { diff --git a/usr/lib/qt5/qml/Sailfish/Gallery/GalleryVideoOutput.qml b/usr/lib/qt5/qml/Sailfish/Gallery/GalleryVideoOutput.qml index a0c8a6ce..2615fb12 100644 --- a/usr/lib/qt5/qml/Sailfish/Gallery/GalleryVideoOutput.qml +++ b/usr/lib/qt5/qml/Sailfish/Gallery/GalleryVideoOutput.qml @@ -2,6 +2,9 @@ import QtQuick 2.6 import Sailfish.Silica 1.0 import QtMultimedia 5.0 +/*! + \inqmlmodule Sailfish.Gallery +*/ VideoOutput { id: output diff --git a/usr/lib/qt5/qml/Sailfish/Gallery/HighlightItem.qml b/usr/lib/qt5/qml/Sailfish/Gallery/HighlightItem.qml index e77cf151..69086a30 100644 --- a/usr/lib/qt5/qml/Sailfish/Gallery/HighlightItem.qml +++ b/usr/lib/qt5/qml/Sailfish/Gallery/HighlightItem.qml @@ -8,6 +8,9 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 +/*! + \inqmlmodule Sailfish.Gallery +*/ Rectangle { property bool active property real highlightOpacity: Theme.opacityHigh diff --git a/usr/lib/qt5/qml/Sailfish/Gallery/ImageDetailsItem.qml b/usr/lib/qt5/qml/Sailfish/Gallery/ImageDetailsItem.qml index 4b864d5f..898d5178 100644 --- a/usr/lib/qt5/qml/Sailfish/Gallery/ImageDetailsItem.qml +++ b/usr/lib/qt5/qml/Sailfish/Gallery/ImageDetailsItem.qml @@ -1,6 +1,9 @@ import QtQuick 2.6 import Sailfish.Silica 1.0 +/*! + \inqmlmodule Sailfish.Gallery +*/ Column { property alias filePathDetail: filePathItem property alias fileSizeDetail: fileSizeItem @@ -39,6 +42,18 @@ Column { return qsTrId("components_gallery-value-focal-length").arg(focalLength) } + function formatExposure(exposureTime) { + if (exposureTime >= 1) { + //: Camera exposure time in seconds or fraction of seconds + //% "%1 s" + return qsTrId("components_gallery-value-exposure_time").arg(exposureTime) + } else if (exposureTime > 0) { + return qsTrId("components_gallery-value-exposure_time").arg("1/" + Math.round(1 / exposureTime)) + } else { + return exposureTime + } + } + function formatGpsCoordinates(latitude, longitude, altitude) { //: GPS coordinates //% "Latitude %1 - Longitude %2 - Altitude %3" diff --git a/usr/lib/qt5/qml/Sailfish/Gallery/ImageEditDialog.qml b/usr/lib/qt5/qml/Sailfish/Gallery/ImageEditDialog.qml index 38d82aa6..e4493e64 100644 --- a/usr/lib/qt5/qml/Sailfish/Gallery/ImageEditDialog.qml +++ b/usr/lib/qt5/qml/Sailfish/Gallery/ImageEditDialog.qml @@ -14,6 +14,9 @@ import Sailfish.Gallery.private 1.0 import Nemo.Notifications 1.0 import "private" +/*! + \inqmlmodule Sailfish.Gallery +*/ Dialog { id: root @@ -29,12 +32,33 @@ Dialog { property alias contrast: previewImage.previewContrast property alias imageRotation: previewImage.previewRotation + /*! + \internal + */ property bool _lightAndContrastMode + /*! + \internal + */ property bool _cropMenu + /*! + \internal + */ property string _cropType: "none" + /*! + \internal + */ property int _cropRatio: cropOnly ? aspectRatio : -1.0 + /*! + \internal + */ property bool _checkRotation + /*! + \internal + */ property bool _checkLevels + /*! + \internal + */ property bool _checkCrop property bool editInProgress property bool editSuccessful diff --git a/usr/lib/qt5/qml/Sailfish/Gallery/ImageGridView.qml b/usr/lib/qt5/qml/Sailfish/Gallery/ImageGridView.qml index f5bc5358..6fed9984 100644 --- a/usr/lib/qt5/qml/Sailfish/Gallery/ImageGridView.qml +++ b/usr/lib/qt5/qml/Sailfish/Gallery/ImageGridView.qml @@ -2,12 +2,15 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 import Sailfish.Silica.private 1.0 as Private +/*! + \inqmlmodule Sailfish.Gallery +*/ SilicaGridView { id: grid property real cellSize: Math.floor(width / columnCount) property int columnCount: Math.floor(width / Theme.itemSizeHuge) - property int maxContentY: Math.max(0, contentHeight - height) + originY + property int maxContentY: Math.max(0, contentHeight - height) + originY + __silica_menu_height property string dateProperty: "dateTaken" // QTBUG-95676: StopAtBounds does not work with StrictlyEnforceRange, diff --git a/usr/lib/qt5/qml/Sailfish/Gallery/ImageViewer.qml b/usr/lib/qt5/qml/Sailfish/Gallery/ImageViewer.qml index 08c99b72..a8d0f475 100644 --- a/usr/lib/qt5/qml/Sailfish/Gallery/ImageViewer.qml +++ b/usr/lib/qt5/qml/Sailfish/Gallery/ImageViewer.qml @@ -4,12 +4,18 @@ import Sailfish.Silica.private 1.0 import Sailfish.Gallery 1.0 import Sailfish.Gallery.private 1.0 +/*! + \inqmlmodule Sailfish.Gallery +*/ ZoomableFlickable { id: flickable property alias source: photo.source property bool active: true + /*! + \internal + */ readonly property bool _active: active || viewMoving readonly property bool error: photo.status == Image.Error readonly property alias imageMetaData: metadata @@ -131,8 +137,8 @@ ZoomableFlickable { id: errorLabelComponent InfoLabel { //: Image loading failed - //% "Oops, can't display the image" - text: qsTrId("components_gallery-la-image-loading-failed") + //% "Couldn't load the image. It could have been deleted or become inaccessible." + text: qsTrId("components_gallery-la-image-loading-failed-inaccessible") anchors.verticalCenter: parent.verticalCenter opacity: photo.status == Image.Error ? 1.0 : 0.0 Behavior on opacity { FadeAnimator {}} diff --git a/usr/lib/qt5/qml/Sailfish/Gallery/ThumbnailImage.qml b/usr/lib/qt5/qml/Sailfish/Gallery/ThumbnailImage.qml index a0c3b80c..88a0e587 100644 --- a/usr/lib/qt5/qml/Sailfish/Gallery/ThumbnailImage.qml +++ b/usr/lib/qt5/qml/Sailfish/Gallery/ThumbnailImage.qml @@ -2,16 +2,22 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 import Nemo.Thumbnailer 1.0 +/*! + \inqmlmodule Sailfish.Gallery +*/ ThumbnailBase { id: thumbnailBase readonly property alias status: thumbnail.status + /*! + \internal + */ property alias _thumbnail: thumbnail Image { anchors.fill: parent source: thumbnail.status === Thumbnail.Ready ? "" - : "image://theme/graphic-avatar-text-back" + : "image://theme/graphic-grid-item-background" } Thumbnail { diff --git a/usr/lib/qt5/qml/Sailfish/Gallery/ThumbnailVideo.qml b/usr/lib/qt5/qml/Sailfish/Gallery/ThumbnailVideo.qml index 56faea2f..cd5ce5b6 100644 --- a/usr/lib/qt5/qml/Sailfish/Gallery/ThumbnailVideo.qml +++ b/usr/lib/qt5/qml/Sailfish/Gallery/ThumbnailVideo.qml @@ -1,6 +1,9 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 +/*! + \inqmlmodule Sailfish.Gallery +*/ ThumbnailImage { property alias duration: durationLabel.text property alias title: titleLabel.text diff --git a/usr/lib/qt5/qml/Sailfish/Gallery/VideoPoster.qml b/usr/lib/qt5/qml/Sailfish/Gallery/VideoPoster.qml index 70fd52d2..649bdebb 100644 --- a/usr/lib/qt5/qml/Sailfish/Gallery/VideoPoster.qml +++ b/usr/lib/qt5/qml/Sailfish/Gallery/VideoPoster.qml @@ -2,6 +2,9 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 import Nemo.Thumbnailer 1.0 +/*! + \inqmlmodule Sailfish.Gallery +*/ Item { id: root diff --git a/usr/lib/qt5/qml/Sailfish/Gallery/private/DebugLabel.qml b/usr/lib/qt5/qml/Sailfish/Gallery/private/DebugLabel.qml index a8abf3a5..d6bea071 100644 --- a/usr/lib/qt5/qml/Sailfish/Gallery/private/DebugLabel.qml +++ b/usr/lib/qt5/qml/Sailfish/Gallery/private/DebugLabel.qml @@ -4,4 +4,4 @@ import Sailfish.Silica 1.0 Label { font.pixelSize: Theme.fontSizeSmall color: Theme.highlightColor -} \ No newline at end of file +} diff --git a/usr/lib/qt5/qml/Sailfish/Gallery/private/ImageEditPreview.qml b/usr/lib/qt5/qml/Sailfish/Gallery/private/ImageEditPreview.qml index d99485c5..067e0db2 100644 --- a/usr/lib/qt5/qml/Sailfish/Gallery/private/ImageEditPreview.qml +++ b/usr/lib/qt5/qml/Sailfish/Gallery/private/ImageEditPreview.qml @@ -111,18 +111,6 @@ Item { onTriggered: root.resetZoom() } - Label { - visible: zoomableImage.error - //: Image to be edited can't be opened - //% "Oops, image error!" - text: qsTrId("sailfish-components-gallery-la_image-loading-error") - anchors.centerIn: zoomableImage - width: parent.width - 2 * Theme.paddingMedium - wrapMode: Text.Wrap - font.pixelSize: Theme.fontSizeLarge - horizontalAlignment: Text.AlignHCenter - } - ImageEditor { id : editor @@ -257,6 +245,6 @@ Item { BusyIndicator { anchors.centerIn: parent size: BusyIndicatorSize.Large - running: editInProgress || zoomableImage.error + running: editInProgress } } diff --git a/usr/lib/qt5/qml/Sailfish/Gallery/private/qmldir b/usr/lib/qt5/qml/Sailfish/Gallery/private/qmldir index d1858d0e..e89017a6 100644 --- a/usr/lib/qt5/qml/Sailfish/Gallery/private/qmldir +++ b/usr/lib/qt5/qml/Sailfish/Gallery/private/qmldir @@ -1,5 +1,6 @@ module Sailfish.Gallery.private plugin sailfishgalleryplugin +typeinfo plugins.qmltypes AvatarCropDialog 1.0 AvatarCropDialog.qml FlickableDebugItem 1.0 FlickableDebugItem.qml ImageEditPreview 1.0 ImageEditPreview.qml diff --git a/usr/lib/qt5/qml/Sailfish/Gallery/qmldir b/usr/lib/qt5/qml/Sailfish/Gallery/qmldir index c001479f..dca05b95 100644 --- a/usr/lib/qt5/qml/Sailfish/Gallery/qmldir +++ b/usr/lib/qt5/qml/Sailfish/Gallery/qmldir @@ -1,5 +1,6 @@ module Sailfish.Gallery plugin sailfishgalleryplugin +typeinfo plugins.qmltypes DetailsPage 1.0 DetailsPage.qml ImageDetailsItem 1.0 ImageDetailsItem.qml GalleryMediaPlayer 1.0 GalleryMediaPlayer.qml diff --git a/usr/lib/qt5/qml/Sailfish/Homescreen/qmldir b/usr/lib/qt5/qml/Sailfish/Homescreen/qmldir new file mode 100644 index 00000000..330761fe --- /dev/null +++ b/usr/lib/qt5/qml/Sailfish/Homescreen/qmldir @@ -0,0 +1,2 @@ +module Sailfish.WindowPlugin +plugin SailfishHomescreenPlugin diff --git a/usr/lib/qt5/qml/Sailfish/Lipstick/BatteryStatusIndicator.qml b/usr/lib/qt5/qml/Sailfish/Lipstick/BatteryStatusIndicator.qml index 52331904..726d4e16 100644 --- a/usr/lib/qt5/qml/Sailfish/Lipstick/BatteryStatusIndicator.qml +++ b/usr/lib/qt5/qml/Sailfish/Lipstick/BatteryStatusIndicator.qml @@ -5,7 +5,7 @@ ** ****************************************************************************/ -import QtQuick 2.0 +import QtQuick 2.6 import Sailfish.Silica 1.0 import org.nemomobile.systemsettings 1.0 import Nemo.Mce 1.0 @@ -13,38 +13,58 @@ import Nemo.Mce 1.0 SilicaItem { id: batteryStatusIndicator - property string iconSuffix property alias icon: batteryStatusIndicatorImage.source property alias text: batteryStatusIndicatorText.text property alias color: batteryStatusIndicatorText.color - property real totalHeight: height property bool usbPreparingMode + readonly property bool isCharging: batteryStatus.chargerStatus == BatteryStatus.Connected height: Theme.iconSizeExtraSmall - width: batteryStatusIndicatorText.x+batteryStatusIndicatorText.width + width: batteryStatusIndicatorText.x + batteryStatusIndicatorText.width + visible: deviceInfo.hasFeature(DeviceInfo.FeatureBattery) BatteryStatus { id: batteryStatus } + McePowerSaveMode { id: mcePowerSaveMode } - readonly property bool isCharging: batteryStatus.chargerStatus == BatteryStatus.Connected + DeviceInfo { + id: deviceInfo + } - Icon { + Item { id: chargeItem + anchors.verticalCenter: parent.verticalCenter height: chargeCableIcon.height - width: chargeCableIcon.width + chargeCableIcon.x + // assuming root item is in a container with 0 position being screen left edge + width: chargeCableIcon.width + batteryStatusIndicator.x + x: isCharging ? -extensionCord.width // normal charge cable starts at 0 + : (-batteryStatusIndicator.x - width) clip: chargeCableAnim.running + + Behavior on x { NumberAnimation { id: chargeCableAnim; duration: 500; easing.type: Easing.InOutQuad } } + Icon { id: chargeCableIcon - source: "image://theme/icon-status-charge-cable" + iconSuffix + + source: "image://theme/icon-status-charge-cable" anchors.verticalCenter: parent.verticalCenter visible: isCharging || chargeCableAnim.running - x: isCharging ? 0 : -width - Behavior on x { NumberAnimation { id: chargeCableAnim; duration: 500; easing.type: Easing.InOutQuad } } + anchors.right: chargeItem.right + } + + // some extra cord if the indicator needs to be indented + Icon { + id: extensionCord + + source: "image://theme/icon-status-charge-extension" + anchors.verticalCenter: parent.verticalCenter + visible: chargeCableIcon.visible + anchors.right: chargeCableIcon.left } layer.enabled: usbPreparingMode @@ -84,9 +104,9 @@ SilicaItem { Icon { id: batteryStatusIndicatorImage + anchors.verticalCenter: parent.verticalCenter - x: Math.max(chargeItem.width, Theme.paddingMedium) - source: sourceValue + x: Math.max(0, chargeItem.x + chargeItem.width) readonly property bool baseNameEquals: sourceValue.indexOf(source) === 0 || source.toString().indexOf(sourceValue) === 0 property string sourceValue: { @@ -98,14 +118,16 @@ SilicaItem { } else if (mcePowerSaveMode.active) { name = "powersave" } - return ["image://theme/icon-status-", name, iconSuffix].join("") + return "image://theme/icon-status-" + name } // delay updating state to coincide with cable animation touching the indicator onSourceValueChanged: statusChangeTimer.restart() + Component.onCompleted: source = sourceValue // avoid binding in start Timer { id: statusChangeTimer + interval: batteryStatusIndicatorImage.baseNameEquals ? 0 : chargeCableAnim.duration/2 onTriggered: batteryStatusIndicatorImage.source = batteryStatusIndicatorImage.sourceValue } @@ -113,6 +135,7 @@ SilicaItem { Text { id: batteryStatusIndicatorText + anchors { left: batteryStatusIndicatorImage.right leftMargin: Theme.paddingSmall diff --git a/usr/lib/qt5/qml/Sailfish/Lipstick/BoundedModel.qml b/usr/lib/qt5/qml/Sailfish/Lipstick/BoundedModel.qml index 68963e6c..61b1f14d 100644 --- a/usr/lib/qt5/qml/Sailfish/Lipstick/BoundedModel.qml +++ b/usr/lib/qt5/qml/Sailfish/Lipstick/BoundedModel.qml @@ -5,7 +5,7 @@ ** ****************************************************************************/ -import QtQuick 2.0 +import QtQuick 2.6 import QtQml.Models 2.1 // Use a DelegateModel to only show the first N items from a source model diff --git a/usr/lib/qt5/qml/Sailfish/Lipstick/ClockItem.qml b/usr/lib/qt5/qml/Sailfish/Lipstick/ClockItem.qml index 956ad559..14375c63 100644 --- a/usr/lib/qt5/qml/Sailfish/Lipstick/ClockItem.qml +++ b/usr/lib/qt5/qml/Sailfish/Lipstick/ClockItem.qml @@ -5,10 +5,10 @@ ** ****************************************************************************/ -import QtQuick 2.0 +import QtQuick 2.6 import Sailfish.Silica 1.0 -import org.nemomobile.time 1.0 -import org.nemomobile.configuration 1.0 +import Nemo.Time 1.0 +import Nemo.Configuration 1.0 Text { @@ -84,6 +84,4 @@ Text { enabled: allowEnabled && timeText.updatesEnabled updateFrequency: WallClock.Minute } - - } diff --git a/usr/lib/qt5/qml/Sailfish/Lipstick/LauncherGridItem.qml b/usr/lib/qt5/qml/Sailfish/Lipstick/LauncherGridItem.qml index fb19b371..0f593660 100644 --- a/usr/lib/qt5/qml/Sailfish/Lipstick/LauncherGridItem.qml +++ b/usr/lib/qt5/qml/Sailfish/Lipstick/LauncherGridItem.qml @@ -1,4 +1,4 @@ -import QtQuick 2.0 +import QtQuick 2.6 import Sailfish.Silica 1.0 import com.jolla.settings 1.0 diff --git a/usr/lib/qt5/qml/Sailfish/Lipstick/LauncherIcon.qml b/usr/lib/qt5/qml/Sailfish/Lipstick/LauncherIcon.qml index a5f0d0a8..bb344bd8 100644 --- a/usr/lib/qt5/qml/Sailfish/Lipstick/LauncherIcon.qml +++ b/usr/lib/qt5/qml/Sailfish/Lipstick/LauncherIcon.qml @@ -1,4 +1,4 @@ -import QtQuick 2.0 +import QtQuick 2.6 import Sailfish.Silica 1.0 HighlightImage { diff --git a/usr/lib/qt5/qml/Sailfish/Lipstick/NotificationAddAnimation.qml b/usr/lib/qt5/qml/Sailfish/Lipstick/NotificationAddAnimation.qml index 78e77d60..d5ebdbfa 100644 --- a/usr/lib/qt5/qml/Sailfish/Lipstick/NotificationAddAnimation.qml +++ b/usr/lib/qt5/qml/Sailfish/Lipstick/NotificationAddAnimation.qml @@ -5,7 +5,7 @@ ** ****************************************************************************/ -import QtQuick 2.0 +import QtQuick 2.6 import Sailfish.Silica 1.0 import com.jolla.lipstick 0.1 diff --git a/usr/lib/qt5/qml/Sailfish/Lipstick/NotificationBaseItem.qml b/usr/lib/qt5/qml/Sailfish/Lipstick/NotificationBaseItem.qml index 4267c88d..e4f93eec 100644 --- a/usr/lib/qt5/qml/Sailfish/Lipstick/NotificationBaseItem.qml +++ b/usr/lib/qt5/qml/Sailfish/Lipstick/NotificationBaseItem.qml @@ -5,7 +5,7 @@ ** ****************************************************************************/ -import QtQuick 2.0 +import QtQuick 2.6 import Sailfish.Silica 1.0 import Sailfish.Silica.private 1.0 as Private import org.nemomobile.lipstick 0.1 diff --git a/usr/lib/qt5/qml/Sailfish/Lipstick/NotificationExpansionButton.qml b/usr/lib/qt5/qml/Sailfish/Lipstick/NotificationExpansionButton.qml index 18eed0dc..e5e8cef9 100644 --- a/usr/lib/qt5/qml/Sailfish/Lipstick/NotificationExpansionButton.qml +++ b/usr/lib/qt5/qml/Sailfish/Lipstick/NotificationExpansionButton.qml @@ -5,7 +5,7 @@ ** ****************************************************************************/ -import QtQuick 2.0 +import QtQuick 2.6 import Sailfish.Silica 1.0 import Sailfish.Silica.private 1.0 import org.nemomobile.lipstick 0.1 diff --git a/usr/lib/qt5/qml/Sailfish/Lipstick/NotificationGroupHeader.qml b/usr/lib/qt5/qml/Sailfish/Lipstick/NotificationGroupHeader.qml index fe2ed65d..61175111 100644 --- a/usr/lib/qt5/qml/Sailfish/Lipstick/NotificationGroupHeader.qml +++ b/usr/lib/qt5/qml/Sailfish/Lipstick/NotificationGroupHeader.qml @@ -5,7 +5,7 @@ ** ****************************************************************************/ -import QtQuick 2.0 +import QtQuick 2.6 import Sailfish.Silica 1.0 import Sailfish.Silica.Background 1.0 import org.nemomobile.lipstick 0.1 diff --git a/usr/lib/qt5/qml/Sailfish/Lipstick/NotificationGroupItem.qml b/usr/lib/qt5/qml/Sailfish/Lipstick/NotificationGroupItem.qml index c6e13fd5..517fda04 100644 --- a/usr/lib/qt5/qml/Sailfish/Lipstick/NotificationGroupItem.qml +++ b/usr/lib/qt5/qml/Sailfish/Lipstick/NotificationGroupItem.qml @@ -5,7 +5,7 @@ ** ****************************************************************************/ -import QtQuick 2.0 +import QtQuick 2.6 import Sailfish.Silica 1.0 import Sailfish.Silica.private 1.0 as Private import org.nemomobile.lipstick 0.1 diff --git a/usr/lib/qt5/qml/Sailfish/Lipstick/NotificationGroupMember.qml b/usr/lib/qt5/qml/Sailfish/Lipstick/NotificationGroupMember.qml index 9e472b71..3328daa5 100644 --- a/usr/lib/qt5/qml/Sailfish/Lipstick/NotificationGroupMember.qml +++ b/usr/lib/qt5/qml/Sailfish/Lipstick/NotificationGroupMember.qml @@ -5,7 +5,7 @@ ** ****************************************************************************/ -import QtQuick 2.0 +import QtQuick 2.6 import Sailfish.Silica 1.0 import Sailfish.Silica.Background 1.0 diff --git a/usr/lib/qt5/qml/Sailfish/Lipstick/NotificationIndicator.qml b/usr/lib/qt5/qml/Sailfish/Lipstick/NotificationIndicator.qml index 31774c93..44b1bdea 100644 --- a/usr/lib/qt5/qml/Sailfish/Lipstick/NotificationIndicator.qml +++ b/usr/lib/qt5/qml/Sailfish/Lipstick/NotificationIndicator.qml @@ -5,7 +5,7 @@ ** ****************************************************************************/ -import QtQuick 2.0 +import QtQuick 2.6 import Sailfish.Silica 1.0 import Sailfish.Lipstick 1.0 diff --git a/usr/lib/qt5/qml/Sailfish/Lipstick/NotificationRemoveAnimation.qml b/usr/lib/qt5/qml/Sailfish/Lipstick/NotificationRemoveAnimation.qml index e17411fd..b0a6129f 100644 --- a/usr/lib/qt5/qml/Sailfish/Lipstick/NotificationRemoveAnimation.qml +++ b/usr/lib/qt5/qml/Sailfish/Lipstick/NotificationRemoveAnimation.qml @@ -5,7 +5,7 @@ ** ****************************************************************************/ -import QtQuick 2.0 +import QtQuick 2.6 import Sailfish.Silica 1.0 import com.jolla.lipstick 0.1 diff --git a/usr/lib/qt5/qml/Sailfish/Lipstick/Pannable.qml b/usr/lib/qt5/qml/Sailfish/Lipstick/Pannable.qml index e3372a47..090c1f91 100644 --- a/usr/lib/qt5/qml/Sailfish/Lipstick/Pannable.qml +++ b/usr/lib/qt5/qml/Sailfish/Lipstick/Pannable.qml @@ -1,4 +1,4 @@ -import QtQuick 2.2 +import QtQuick 2.6 import QtQuick.Window 2.1 as QtQuick import Sailfish.Silica 1.0 import Sailfish.Silica.private 1.0 as Private diff --git a/usr/lib/qt5/qml/Sailfish/Lipstick/PannableItem.qml b/usr/lib/qt5/qml/Sailfish/Lipstick/PannableItem.qml index 7f4fd0c1..12950ae4 100644 --- a/usr/lib/qt5/qml/Sailfish/Lipstick/PannableItem.qml +++ b/usr/lib/qt5/qml/Sailfish/Lipstick/PannableItem.qml @@ -1,4 +1,4 @@ -import QtQuick 2.2 +import QtQuick 2.6 import Sailfish.Silica 1.0 import Sailfish.Silica.private 1.0 as Private import Sailfish.Lipstick 1.0 diff --git a/usr/lib/qt5/qml/Sailfish/Lipstick/PartnerspaceModel.qml b/usr/lib/qt5/qml/Sailfish/Lipstick/PartnerspaceModel.qml index f1f3a725..811f4fef 100644 --- a/usr/lib/qt5/qml/Sailfish/Lipstick/PartnerspaceModel.qml +++ b/usr/lib/qt5/qml/Sailfish/Lipstick/PartnerspaceModel.qml @@ -1,6 +1,6 @@ -import QtQuick 2.0 +import QtQuick 2.6 import org.nemomobile.lipstick 0.1 -import org.nemomobile.configuration 1.0 +import Nemo.Configuration 1.0 import Nemo.DBus 2.0 LauncherWatcherModel { diff --git a/usr/lib/qt5/qml/Sailfish/Lipstick/ShutDownItem.qml b/usr/lib/qt5/qml/Sailfish/Lipstick/ShutDownItem.qml index 23f84ed4..2f825fd0 100644 --- a/usr/lib/qt5/qml/Sailfish/Lipstick/ShutDownItem.qml +++ b/usr/lib/qt5/qml/Sailfish/Lipstick/ShutDownItem.qml @@ -5,7 +5,7 @@ * License: Proprietary */ -import QtQuick 2.0 +import QtQuick 2.6 import Sailfish.Silica 1.0 import Sailfish.Lipstick 1.0 import org.nemomobile.systemsettings 1.0 diff --git a/usr/lib/qt5/qml/Sailfish/Lipstick/SwitcherGrid.qml b/usr/lib/qt5/qml/Sailfish/Lipstick/SwitcherGrid.qml index 6d6945a9..61f46e96 100644 --- a/usr/lib/qt5/qml/Sailfish/Lipstick/SwitcherGrid.qml +++ b/usr/lib/qt5/qml/Sailfish/Lipstick/SwitcherGrid.qml @@ -1,4 +1,4 @@ -import QtQuick 2.0 +import QtQuick 2.6 import Sailfish.Silica 1.0 Grid { diff --git a/usr/lib/qt5/qml/Sailfish/Lipstick/SystemDialog.qml b/usr/lib/qt5/qml/Sailfish/Lipstick/SystemDialog.qml index 4ca34476..eb30d483 100644 --- a/usr/lib/qt5/qml/Sailfish/Lipstick/SystemDialog.qml +++ b/usr/lib/qt5/qml/Sailfish/Lipstick/SystemDialog.qml @@ -5,12 +5,12 @@ ** ****************************************************************************/ -import QtQuick 2.0 +import QtQuick 2.6 import QtQuick.Window 2.1 as QtQuick import Sailfish.Silica 1.0 import Sailfish.Lipstick 1.0 import org.nemomobile.lipstick 0.1 -import org.nemomobile.configuration 1.0 +import Nemo.Configuration 1.0 SystemDialogWindow { id: dialog @@ -63,6 +63,8 @@ SystemDialogWindow { SystemDialogApplicationWindow { id: window + property var __systemDialogAppWindow // for children to search + _backgroundVisible: false _opaque: false cover: null @@ -70,7 +72,8 @@ SystemDialogWindow { focus: true _backgroundRect: { - switch (window._rotatingItem.rotation) { + var orientationOffset = window.QtQuick.Screen.angleBetween(Qt.PortraitOrientation, window.QtQuick.Screen.primaryOrientation) + switch (orientationOffset + window._rotatingItem.rotation) { case 90: case -270: return Qt.rect(width - layout.contentHeight, 0, layout.contentHeight, height) diff --git a/usr/lib/qt5/qml/Sailfish/Lipstick/SystemDialogApplicationWindow.qml b/usr/lib/qt5/qml/Sailfish/Lipstick/SystemDialogApplicationWindow.qml index aa7f9419..936158a6 100644 --- a/usr/lib/qt5/qml/Sailfish/Lipstick/SystemDialogApplicationWindow.qml +++ b/usr/lib/qt5/qml/Sailfish/Lipstick/SystemDialogApplicationWindow.qml @@ -1,4 +1,4 @@ -import QtQuick 2.0 +import QtQuick 2.6 import Sailfish.Silica 1.0 // This declares an ApplicationWindow with an empty page for SystemDialog, it is not declared in diff --git a/usr/lib/qt5/qml/Sailfish/Lipstick/SystemDialogHeader.qml b/usr/lib/qt5/qml/Sailfish/Lipstick/SystemDialogHeader.qml index 63d74f95..da1d0699 100644 --- a/usr/lib/qt5/qml/Sailfish/Lipstick/SystemDialogHeader.qml +++ b/usr/lib/qt5/qml/Sailfish/Lipstick/SystemDialogHeader.qml @@ -1,10 +1,27 @@ -import QtQuick 2.1 +import QtQuick 2.6 import Sailfish.Silica 1.0 Item { + id: root + property alias title: titleLabel.text property alias description: descriptionLabel.text - property real topPadding: 2*Theme.paddingLarge + property real topPadding: { + var padding = 2 * Theme.paddingLarge + if (tight) { + if (Screen.sizeCategory < Screen.Large) { + padding = Theme.paddingLarge + } + } else if (semiTight) { + if (_orientation == Qt.LandscapeOrientation || _orientation == Qt.InvertedLandscapeOrientation) { + padding = Theme.paddingLarge + } + } + + return Math.max(padding, + _orientation == Qt.PortraitOrientation && Screen.topCutout.height > 0 + ? Screen.topCutout.height + Theme.paddingSmall : 0) + } property real bottomPadding: Theme.paddingLarge property alias titleFont: titleLabel.font @@ -13,16 +30,42 @@ Item { property alias titleTextFormat: titleLabel.textFormat + property bool tight // save vertical space by smaller padding + property bool semiTight // save vertical space but not as aggressively as tight + + property Item _systemDialog + property Item _systemWindow + property int _orientation: _systemWindow ? _systemWindow.topmostWindowOrientation + : _systemDialog ? _systemDialog.orientation + : Qt.PortraitOrientation + height: content.height + topPadding + bottomPadding width: (Screen.sizeCategory >= Screen.Large) ? Screen.height / 2 : parent.width anchors.horizontalCenter: parent.horizontalCenter + Component.onCompleted: { + // this can live either in SystemDialog or SystemWindow, figure out where + var parentItem = root.parent + while (parentItem) { + if (parentItem.hasOwnProperty('__systemDialogAppWindow')) { + _systemDialog = parentItem + return + } + if (parentItem.hasOwnProperty('topmostWindowOrientation')) { + _systemWindow = parentItem + return + } + + parentItem = parentItem.parent + } + } + Column { id: content width: parent.width - 2*x x: (Screen.sizeCategory < Screen.Large) ? Theme.horizontalPageMargin : 0 - y: topPadding + y: root.topPadding spacing: Theme.paddingLarge Label { diff --git a/usr/lib/qt5/qml/Sailfish/Lipstick/SystemDialogIconButton.qml b/usr/lib/qt5/qml/Sailfish/Lipstick/SystemDialogIconButton.qml index a320894a..cfaf005b 100644 --- a/usr/lib/qt5/qml/Sailfish/Lipstick/SystemDialogIconButton.qml +++ b/usr/lib/qt5/qml/Sailfish/Lipstick/SystemDialogIconButton.qml @@ -1,4 +1,4 @@ -import QtQuick 2.1 +import QtQuick 2.6 import Sailfish.Silica 1.0 BackgroundItem { diff --git a/usr/lib/qt5/qml/Sailfish/Lipstick/SystemDialogLayout.qml b/usr/lib/qt5/qml/Sailfish/Lipstick/SystemDialogLayout.qml index d300486d..84bf20fd 100644 --- a/usr/lib/qt5/qml/Sailfish/Lipstick/SystemDialogLayout.qml +++ b/usr/lib/qt5/qml/Sailfish/Lipstick/SystemDialogLayout.qml @@ -5,7 +5,7 @@ ** ****************************************************************************/ -import QtQuick 2.0 +import QtQuick 2.6 import Sailfish.Silica 1.0 FocusScope { diff --git a/usr/lib/qt5/qml/Sailfish/Mdm/MdmTermsOfUse.qml b/usr/lib/qt5/qml/Sailfish/Mdm/MdmTermsOfUse.qml new file mode 100644 index 00000000..8b392eb0 --- /dev/null +++ b/usr/lib/qt5/qml/Sailfish/Mdm/MdmTermsOfUse.qml @@ -0,0 +1,36 @@ +import QtQuick 2.0 +import Sailfish.Mdm 1.0 +import org.nemomobile.systemsettings 1.0 + +Item { + property var translationIds: { + "title": "sailfish-mdm-he-sailfish_device_manager", + "summary": "sailfish-mdm-la-mdm_installed", + "body": "sailfish-mdm-la-if_remove_mdm", + "triggerAccept": "sailfish-mdm-bt-i_understand" + } + + function translate(textId) { + switch (textId) { + case "title": + //: %1 is operating system name without OS suffix + //% "%1 Device Manager" + return qsTrId("sailfish-mdm-he-sailfish_device_manager").arg(aboutSettings.baseOperatingSystemName) + case "summary": + //% "Mobile Device Management (MDM) services have been installed on this device, which can be used to remotely manage the device." + return qsTrId("sailfish-mdm-la-mdm_installed") + case "body": + //% "If you wish to remove the Device Management services please contact a system administrator." + return qsTrId("sailfish-mdm-la-if_remove_mdm") + case "triggerAccept": + //% "I understand" + return qsTrId("sailfish-mdm-bt-i_understand") + default: + return "" + } + } + + AboutSettings { + id: aboutSettings + } +} diff --git a/usr/lib/qt5/qml/Sailfish/Mdm/qmldir b/usr/lib/qt5/qml/Sailfish/Mdm/qmldir new file mode 100644 index 00000000..87cd8e6a --- /dev/null +++ b/usr/lib/qt5/qml/Sailfish/Mdm/qmldir @@ -0,0 +1,4 @@ +module Sailfish.Mdm +plugin sailfishmdmplugin +typeinfo plugins.qmltypes +MdmTermsOfUse 1.0 MdmTermsOfUse.qml diff --git a/usr/lib/qt5/qml/Sailfish/Media/MediaListItem.qml b/usr/lib/qt5/qml/Sailfish/Media/MediaListItem.qml index c95871bd..14b75973 100644 --- a/usr/lib/qt5/qml/Sailfish/Media/MediaListItem.qml +++ b/usr/lib/qt5/qml/Sailfish/Media/MediaListItem.qml @@ -3,6 +3,11 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 +/*! + \qmltype MediaListItem + \inqmlmodule Sailfish.Media + \inherits Sailfish.Silica.ListItem +*/ ListItem { id: mediaListItem diff --git a/usr/lib/qt5/qml/Sailfish/Media/MediaPlayerControlsPanel.qml b/usr/lib/qt5/qml/Sailfish/Media/MediaPlayerControlsPanel.qml index 2af98c47..0e0b8de7 100644 --- a/usr/lib/qt5/qml/Sailfish/Media/MediaPlayerControlsPanel.qml +++ b/usr/lib/qt5/qml/Sailfish/Media/MediaPlayerControlsPanel.qml @@ -2,12 +2,26 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 import Sailfish.Media 1.0 +/*! + * \inqmlmodule Sailfish.Media + * \inherits Sailfish.Silica.DockedPanel + */ DockedPanel { id: panel property bool active property bool playing + /*! + \value MediaPlayerControls.NoRepeat (default) + \value MediaPlayerControls.RepeatTrack + \value MediaPlayerControls.RepeatPlayList + */ property int repeat: MediaPlayerControls.NoRepeat + /*! + \value MediaPlayerControls.NoShuffle (default) + \value MediaPlayerControls.ShuffleTracks + \value MediaPlayerControls.ShufflePlaylists + */ property int shuffle: MediaPlayerControls.NoShuffle property bool showAddToPlaylist: true property bool showMenu: true @@ -19,6 +33,9 @@ DockedPanel { property int durationScalar: 1 property alias forwardEnabled: forwardButton.enabled + /*! + \internal + */ property bool _isLandscape: pageStack && pageStack.currentPage && pageStack.currentPage.isLandscape signal previousClicked() @@ -27,6 +44,9 @@ DockedPanel { signal repeatClicked() signal shuffleClicked() + /*! + Emitted when \c AddToPlaylist button is clicked + */ signal addToPlaylist() signal sliderReleased(int value) diff --git a/usr/lib/qt5/qml/Sailfish/Media/MediaPlayerPanelBackground.qml b/usr/lib/qt5/qml/Sailfish/Media/MediaPlayerPanelBackground.qml index ed324f4c..8ec28cee 100644 --- a/usr/lib/qt5/qml/Sailfish/Media/MediaPlayerPanelBackground.qml +++ b/usr/lib/qt5/qml/Sailfish/Media/MediaPlayerPanelBackground.qml @@ -1,6 +1,10 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 +/*! + \qmltype MediaPlayerPanelBackground + \inqmlmodule Sailfish.Media +*/ Rectangle { id: root diff --git a/usr/lib/qt5/qml/Sailfish/Media/MprisControls.qml b/usr/lib/qt5/qml/Sailfish/Media/MprisControls.qml index 75d0d87c..477a88a1 100644 --- a/usr/lib/qt5/qml/Sailfish/Media/MprisControls.qml +++ b/usr/lib/qt5/qml/Sailfish/Media/MprisControls.qml @@ -6,6 +6,8 @@ MouseArea { property bool isPlaying property alias artistAndSongText: artistAndSong.artistAndSongText + property alias applicationName: appName.text + property alias albumArtSource: albumArt.sourceUrl property bool nextEnabled property bool previousEnabled property bool playEnabled @@ -19,7 +21,7 @@ MouseArea { signal nextRequested() signal previousRequested() - height: artistAndSong.height + playerButtons.height + height: playerButtons.y + playerButtons.height MouseArea { id: artistSongArea @@ -37,7 +39,7 @@ MouseArea { property var artistAndSongText: { "artist": "", "song": "" } - width: parent.width + width: parent.width - (albumArt.width > 0 ? (albumArt.width + Theme.paddingMedium) : 0) onArtistAndSongTextChanged: { if (artistAndSongFadeAnimation.running) { @@ -65,7 +67,6 @@ MouseArea { width: parent.width font.pixelSize: Theme.fontSizeMedium truncationMode: TruncationMode.Fade - horizontalAlignment: implicitWidth > width ? Text.AlignHLeft : Text.AlignHCenter color: artistSongArea.pressed ? Theme.highlightColor : mprisControls.textColor maximumLineCount: 1 } @@ -74,12 +75,51 @@ MouseArea { id: artistLabel width: parent.width - font.pixelSize: Theme.fontSizeMedium + font.pixelSize: Theme.fontSizeSmall truncationMode: TruncationMode.Fade - horizontalAlignment: implicitWidth > width ? Text.AlignHLeft : Text.AlignHCenter color: songLabel.color maximumLineCount: 1 } + Label { + id: appName + + visible: songLabel.text !== "" || artistLabel.text !== "" + || mprisControls.previousEnabled || playPauseButton.enabled || mprisControls.nextEnabled + width: parent.width + font.pixelSize: Theme.fontSizeSmall + truncationMode: TruncationMode.Fade + maximumLineCount: 1 + color: Theme.secondaryHighlightColor + } + } + + Image { + id: albumArt + + property url sourceUrl + + anchors.right: parent.right + width: status == Image.Ready ? Theme.itemSizeLarge : 0 + height: width + sourceSize.width: Theme.itemSizeLarge + sourceSize.height: Theme.itemSizeLarge + + fillMode: Image.PreserveAspectCrop + + onSourceUrlChanged: { + if (artFadeAnimation.running) { + artFadeAnimation.complete() + } + artFadeAnimation.running = true + } + } + + SequentialAnimation { + id: artFadeAnimation + + FadeAnimation { target: albumArt; properties: "opacity"; to: 0.0 } + ScriptAction { script: { albumArt.source = albumArt.sourceUrl } } + FadeAnimation { target: albumArt; properties: "opacity"; to: 1.0 } } Row { @@ -87,7 +127,7 @@ MouseArea { spacing: mprisControls.width / 3 - mprisControls._squareSize anchors.horizontalCenter: parent.horizontalCenter - anchors.top: artistAndSong.bottom + y: Math.max(Theme.itemSizeLarge, artistAndSong.height) IconButton { enabled: mprisControls.previousEnabled @@ -95,7 +135,7 @@ MouseArea { Behavior on opacity { FadeAnimation {} } width: mprisControls._squareSize height: width - icon.source: "image://theme/icon-m-previous" + icon.source: "image://theme/icon-m-simple-previous" onClicked: mprisControls.previousRequested() } @@ -103,8 +143,8 @@ MouseArea { IconButton { id: playPauseButton - property string iconSource: enabled ? (mprisControls.isPlaying ? "image://theme/icon-m-pause" - : "image://theme/icon-m-play") + property string iconSource: enabled ? (mprisControls.isPlaying ? "image://theme/icon-m-simple-pause" + : "image://theme/icon-m-simple-play") : "" enabled: mprisControls.isPlaying ? mprisControls.pauseEnabled : mprisControls.playEnabled @@ -141,7 +181,7 @@ MouseArea { Behavior on opacity { FadeAnimation {} } width: mprisControls._squareSize height: width - icon.source: "image://theme/icon-m-next" + icon.source: "image://theme/icon-m-simple-next" onClicked: mprisControls.nextRequested() } diff --git a/usr/lib/qt5/qml/Sailfish/Media/MprisManagerControls.qml b/usr/lib/qt5/qml/Sailfish/Media/MprisManagerControls.qml index 0d8c02bb..0c71ef87 100644 --- a/usr/lib/qt5/qml/Sailfish/Media/MprisManagerControls.qml +++ b/usr/lib/qt5/qml/Sailfish/Media/MprisManagerControls.qml @@ -1,47 +1,40 @@ import QtQuick 2.0 import Sailfish.Media 1.0 -import org.nemomobile.policy 1.0 -import org.nemomobile.mpris 1.0 +import Nemo.Policy 1.0 +import Amber.Mpris 1.0 MprisControls { id: controls - property MprisManager mprisManager + property MprisController mprisController property int _playPauseClicks opacity: enabled ? 1.0 : 0.0 - isPlaying: mprisManager.currentService && mprisManager.playbackStatus == Mpris.Playing - artistAndSongText: { - var artist = "" - var song = "" + isPlaying: mprisController.playbackStatus == Mpris.Playing + artistAndSongText: ({ + "artist": (mprisController.metaData.contributingArtist || '').toString(), + "song": mprisController.metaData.title || '', + }) + applicationName: mprisController.identity + albumArtSource: mprisController.metaData.artUrl || '' - if (mprisManager.currentService) { - var artistTag = Mpris.metadataToString(Mpris.Artist) - var titleTag = Mpris.metadataToString(Mpris.Title) - - artist = (artistTag in mprisManager.metadata) ? mprisManager.metadata[artistTag].toString() : "" - song = (titleTag in mprisManager.metadata) ? mprisManager.metadata[titleTag].toString() : "" - } - - return { "artist": artist, "song": song } - } - nextEnabled: mprisManager.currentService && mprisManager.canGoNext - previousEnabled: mprisManager.currentService && mprisManager.canGoPrevious - playEnabled: mprisManager.currentService && mprisManager.canPlay - pauseEnabled: mprisManager.currentService && mprisManager.canPause + nextEnabled: mprisController.canGoNext + previousEnabled: mprisController.canGoPrevious + playEnabled: mprisController.canPlay + pauseEnabled: mprisController.canPause onPlayPauseRequested: { - if (mprisManager.playbackStatus == Mpris.Playing && mprisManager.canPause) { - mprisManager.playPause() - } else if (mprisManager.playbackStatus != Mpris.Playing && mprisManager.canPlay) { - mprisManager.playPause() + if (mprisController.playbackStatus == Mpris.Playing && mprisController.canPause) { + mprisController.playPause() + } else if (mprisController.playbackStatus != Mpris.Playing && mprisController.canPlay) { + mprisController.playPause() } } - onNextRequested: if (mprisManager.canGoNext) mprisManager.next() - onPreviousRequested: if (mprisManager.canGoPrevious) mprisManager.previous() + onNextRequested: if (mprisController.canGoNext) mprisController.next() + onPreviousRequested: if (mprisController.canGoPrevious) mprisController.previous() Permissions { - enabled: !!mprisManager.currentService + enabled: !!mprisController.currentService applicationClass: "player" Resource { @@ -52,24 +45,24 @@ MprisControls { } MediaKey { - enabled: keysResource.acquired && (controls.playEnabled || controls.pauseEnabled) + enabled: keysResource.acquired && controls.playEnabled key: Qt.Key_MediaTogglePlayPause onReleased: controls.playPauseRequested() } MediaKey { enabled: keysResource.acquired && controls.playEnabled key: Qt.Key_MediaPlay - onReleased: controls.mprisManager.play() + onReleased: controls.mprisController.play() } MediaKey { enabled: keysResource.acquired && controls.pauseEnabled key: Qt.Key_MediaPause - onReleased: controls.mprisManager.pause() + onReleased: controls.mprisController.pause() } MediaKey { - enabled: keysResource.acquired && !!controls.mprisManager + enabled: keysResource.acquired && !!controls.mprisController key: Qt.Key_MediaStop - onReleased: controls.mprisManager.stop() + onReleased: controls.mprisController.stop() } MediaKey { enabled: keysResource.acquired && controls.nextEnabled diff --git a/usr/lib/qt5/qml/Sailfish/Media/MprisPlayerControls.qml b/usr/lib/qt5/qml/Sailfish/Media/MprisPlayerControls.qml index 9adbb8e3..c5a9e234 100644 --- a/usr/lib/qt5/qml/Sailfish/Media/MprisPlayerControls.qml +++ b/usr/lib/qt5/qml/Sailfish/Media/MprisPlayerControls.qml @@ -1,14 +1,18 @@ import QtQuick 2.0 -import org.nemomobile.mpris 1.0 +import Amber.Mpris 1.0 +/*! + \qmltype MprisPlayerControls + \inqmlmodule Sailfish.Media +*/ Loader { id: controlsLoader - active: mprisManager.availableServices.length > 0 + active: mprisController.availableServices.length > 0 - Component.onCompleted: setSource("MprisManagerControls.qml", { "mprisManager": mprisManager, "parent": Qt.binding(function() { return controlsLoader.parent }) }) + Component.onCompleted: setSource("MprisManagerControls.qml", { "mprisController": mprisController, "parent": Qt.binding(function() { return controlsLoader.parent }) }) - MprisManager { - id: mprisManager + MprisController { + id: mprisController } } diff --git a/usr/lib/qt5/qml/Sailfish/Media/qmldir b/usr/lib/qt5/qml/Sailfish/Media/qmldir index ea79b48e..4c8ab73d 100644 --- a/usr/lib/qt5/qml/Sailfish/Media/qmldir +++ b/usr/lib/qt5/qml/Sailfish/Media/qmldir @@ -1,5 +1,6 @@ module Sailfish.Media plugin sailfishmediaplugin +typeinfo plugins.qmltypes MediaListItem 1.0 MediaListItem.qml MediaPlayerControlsPanel 1.0 MediaPlayerControlsPanel.qml MprisPlayerControls 1.0 MprisPlayerControls.qml diff --git a/usr/lib/qt5/qml/Sailfish/Messages/ChatTextInput.qml b/usr/lib/qt5/qml/Sailfish/Messages/ChatTextInput.qml index 72dad35c..9a6e061d 100644 --- a/usr/lib/qt5/qml/Sailfish/Messages/ChatTextInput.qml +++ b/usr/lib/qt5/qml/Sailfish/Messages/ChatTextInput.qml @@ -5,7 +5,7 @@ import Sailfish.Telephony 1.0 import Sailfish.Contacts 1.0 import org.nemomobile.commhistory 1.0 import org.nemomobile.contacts 1.0 -import org.nemomobile.configuration 1.0 +import Nemo.Configuration 1.0 InverseMouseArea { id: chatInputArea diff --git a/usr/lib/qt5/qml/Sailfish/Messages/MessageUtils.qml b/usr/lib/qt5/qml/Sailfish/Messages/MessageUtils.qml index dbf85843..242f2c99 100644 --- a/usr/lib/qt5/qml/Sailfish/Messages/MessageUtils.qml +++ b/usr/lib/qt5/qml/Sailfish/Messages/MessageUtils.qml @@ -12,8 +12,8 @@ import Sailfish.Silica 1.0 import Sailfish.Contacts 1.0 import Sailfish.Telephony 1.0 import Sailfish.AccessControl 1.0 -import MeeGo.QOfono 0.2 -import org.nemomobile.dbus 2.0 +import QOfono 0.2 +import Nemo.DBus 2.0 import org.nemomobile.ofono 1.0 import org.nemomobile.contacts 1.0 import org.nemomobile.messages.internal 1.0 diff --git a/usr/lib/qt5/qml/Sailfish/Office/CalligraDocumentPage.qml b/usr/lib/qt5/qml/Sailfish/Office/CalligraDocumentPage.qml new file mode 100644 index 00000000..a899d2e2 --- /dev/null +++ b/usr/lib/qt5/qml/Sailfish/Office/CalligraDocumentPage.qml @@ -0,0 +1,139 @@ +/* + * Copyright (c) 2013 - 2019 Jolla Ltd. + * Copyright (c) 2020 Open Mobile Platform LLC. + * + * 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; version 2 only. + * + * 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. + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Sailfish.Silica.private 1.0 +import org.kde.calligra 1.0 as Calligra + +DocumentPage { + id: page + + property alias document: doc + property alias contents: contentsModel + property int coverAlignment: Qt.AlignLeft | Qt.AlignTop + property int coverFillMode: Image.PreserveAspectCrop + + function currentIndex() { + // This is a function which can be shadowed because the currentIndex property doesn't + // notify reliably and nothing needs to bind to it. + return doc.currentIndex + } + + backNavigation: !busy // During loading the UI is unresponsive, don't show page indicator as back-stepping is not possible + busyIndicator._forceAnimation: busy // Start animation before the main thread gets blocked by loading + icon: "image://theme/icon-m-file-formatted" + busy: doc.status !== Calligra.DocumentStatus.Loaded + && doc.status !== Calligra.DocumentStatus.Failed + + Timer { + interval: 1 + running: status === PageStatus.Active + // Delay loading the document until the page has been activated + onTriggered: document.source = page.source + } + + Timer { + id: previewDelay + interval: 100 + running: doc.status === Calligra.DocumentStatus.Loaded + // We're not using a binding for the preview because calligra is sensitive to the order + // of evaluation and by binding directly to the document status it's possible to attempt + // to get a thumbnail from the contents model after the document has loaded but before the + // model is populated. + onTriggered: page.preview = previewComponent + } + + Component { + id: previewComponent + + Rectangle { + id: preview + + color: page.backgroundColor + + Calligra.ImageDataItem { + x: { + if (page.coverAlignment & Qt.AlignHCenter) { + return (preview.width - width) / 2 + } else if (page.coverAlignment & Qt.AlignRight) { + return preview.width - width + } else { + return 0 + } + } + + y: { + if (page.coverAlignment & Qt.AlignVCenter) { + return (preview.height - height) / 2 + } else if (page.coverAlignment & Qt.AlignBottom) { + return preview.height - height + } else { + return 0 + } + } + + width: { + if (implicitHeight > 0 && page.coverFillMode === Image.PreserveAspectCrop) { + return Math.max( + preview.width, + Math.round(implicitWidth * preview.height / implicitHeight)) + } else if (implicitHeight > 0 && page.coverFillMode === Image.PreserveAspectFit) { + return Math.min( + preview.width, + Math.round(implicitWidth * preview.height / implicitHeight)) + } else { + return preview.width + } + } + + height: implicitWidth > 0 + ? Math.round(implicitHeight * width / implicitWidth) + : preview.height + + Component.onCompleted: { + data = contentsModel.thumbnail(page.currentIndex(), preview.height) + } + } + } + } + + Calligra.ContentsModel { + id: contentsModel + + document: doc + thumbnailSize: Theme.coverSizeLarge + } + + Calligra.Document { + id: doc + + readonly property bool failure: status === Calligra.DocumentStatus.Failed + readOnly: true + onStatusChanged: { + if (status === Calligra.DocumentStatus.Failed) { + errorLoader.setSource(Qt.resolvedUrl("FullscreenError.qml"), { error: lastError }) + } + } + } + + Loader { + id: errorLoader + anchors.fill: parent + } +} diff --git a/usr/lib/qt5/qml/Sailfish/Office/ContextMenuHook.qml b/usr/lib/qt5/qml/Sailfish/Office/ContextMenuHook.qml new file mode 100644 index 00000000..75ea5088 --- /dev/null +++ b/usr/lib/qt5/qml/Sailfish/Office/ContextMenuHook.qml @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2015 Caliste Damien. + * Contact: Damien Caliste + * + * 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; version 2 only. + * + * 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. + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 + +Item { + id: hook + + property bool active: _menu ? _menu.active : false + property alias backgroundColor: background.color + property alias backgroundOpacity: background.opacity + + property real _flickableContentHeight + property real _flickableContentYAtOpen + property bool _opened: _menu ? _menu._open : false + + property int _hookHeight + property var _menu + + // Used to emulate the MouseArea that trigger a ContextMenu + property bool pressed: true + property bool preventStealing + signal positionChanged(point mouse) + signal released(bool mouse) + + function setTarget(targetY, targetHeight) { + y = targetY + _hookHeight = targetHeight + } + + function showMenu(menu) { + _menu = menu + menu.open(hook) + _flickableContentHeight = _menu._flickable.contentHeight + } + + // Ensure that flickable position is restored after context menu + // has been closed. We cannot trust the value that will be restored + // automatically when the state of the menu changes because the + // contentHeight of the flickable may have changed in-between due to + // device rotation for instance. + on_OpenedChanged: { + if (!_opened) { + // Limit the flickable going back to previous y position + // if the device has been rotated and the link would be sent + // out of screen. + _menu._flickable.contentY = + Math.max(_flickableContentYAtOpen, + hook.y + _hookHeight + Theme.paddingSmall - _menu._flickable.height) + // Reset menu flickable after menu is closed to avoid initialisation + // issues next time showMenu() is called. + _menu._flickable = null + } else { + _flickableContentYAtOpen = _menu._flickable.contentY + } + } + Connections { + target: _menu && _menu._flickable ? _menu._flickable : null + onContentHeightChanged: { + // Update the initial opening position with the zoom factor + // if the contentHeight is changed while menu was displayed. + _flickableContentYAtOpen *= _menu._flickable.contentHeight / _flickableContentHeight + _flickableContentHeight = _menu._flickable.contentHeight + } + } + + width: _menu && _menu._flickable ? _menu._flickable.width : 0 + x: _menu && _menu._flickable ? _menu._flickable.contentX : 0 + height: _hookHeight + (_menu ? Theme.paddingSmall + _menu.height : 0.) + + Rectangle { + id: background + parent: _menu ? _menu : null + anchors.fill: parent ? parent : undefined + color: Theme.highlightDimmerColor + opacity: 0.91 + z: -1 + } +} diff --git a/usr/lib/qt5/qml/Sailfish/Office/ControllerFlickable.qml b/usr/lib/qt5/qml/Sailfish/Office/ControllerFlickable.qml new file mode 100644 index 00000000..2a6c9ab0 --- /dev/null +++ b/usr/lib/qt5/qml/Sailfish/Office/ControllerFlickable.qml @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2019 Jolla Ltd. + * Contact: Joona Petrell + * + * 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; version 2 only. + * + * 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. + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 + +DocumentFlickable { + id: flickable + + property QtObject controller + + pinchArea.onPinchUpdated: { + var oldWidth = contentWidth + var oldHeight = contentHeight + var oldZoom = controller.zoom + controller.zoomAroundPoint(controller.zoom * (pinch.scale - pinch.previousScale), 0, 0) + + if (controller.zoom === oldZoom) return + + var multiplier = (1.0 + pinch.scale - pinch.previousScale) + var newWidth = multiplier * oldWidth + var newHeight = multiplier * oldHeight + + contentX += pinch.previousCenter.x - pinch.center.x + contentY += pinch.previousCenter.y - pinch.center.y + + // zoom about center + if (newWidth > width) + contentX -= (oldWidth - newWidth)/(oldWidth/pinch.previousCenter.x) + if (newHeight > height) + contentY -= (oldHeight - newHeight)/(oldHeight/pinch.previousCenter.y) + } + + function zoomOut() { + var scale = controller.zoom / controller.minimumZoom + zoomOutContentYAnimation.to = Math.max(-topMargin, + Math.min(flickable.contentHeight - flickable.height, + (flickable.contentY + flickable.height/2) / scale - flickable.height/2)) + zoomOutAnimation.start() + } + + ParallelAnimation { + id: zoomOutAnimation + + onStopped: flickable.returnToBounds() + NumberAnimation { + target: controller + property: "zoom" + to: controller.minimumZoom + easing.type: Easing.InOutQuad + duration: 200 + } + NumberAnimation { + target: flickable + properties: "contentX" + to: 0 + easing.type: Easing.InOutQuad + duration: 200 + } + NumberAnimation { + id: zoomOutContentYAnimation + target: flickable + properties: "contentY" + easing.type: Easing.InOutQuad + duration: 200 + } + } +} diff --git a/usr/lib/qt5/qml/Sailfish/Office/DeleteButton.qml b/usr/lib/qt5/qml/Sailfish/Office/DeleteButton.qml new file mode 100644 index 00000000..4b37bc44 --- /dev/null +++ b/usr/lib/qt5/qml/Sailfish/Office/DeleteButton.qml @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2019 Jolla Ltd. + * Contact: Joona Petrell + * + * 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; version 2 only. + * + * 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. + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 + +IconButton { + property DocumentPage page + readonly property url source: page.source + + onClicked: window._mainPage.deleteSource(page.source) + + icon.source: "image://theme/icon-m-delete" + anchors.verticalCenter: parent.verticalCenter + visible: page.source != "" +} diff --git a/usr/lib/qt5/qml/Sailfish/Office/DetailsPage.qml b/usr/lib/qt5/qml/Sailfish/Office/DetailsPage.qml new file mode 100644 index 00000000..22e40fd2 --- /dev/null +++ b/usr/lib/qt5/qml/Sailfish/Office/DetailsPage.qml @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2013-2014 Jolla Ltd. + * Contact: Robin Burchell + * + * 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; version 2 only. + * + * 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. + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Sailfish.Office 1.0 +import Nemo.FileManager 1.0 + +Page { + id: page + + property QtObject document + property url source + property string mimeType + default property alias children: contentColumn.data + + FileInfo { + id: info + url: page.source + } + + SilicaFlickable { + id: flickable + anchors.fill: parent + contentHeight: contentColumn.height + Theme.paddingLarge + + Column { + id: contentColumn + width: parent.width + + PageHeader { + id: detailsHeader + //: Details page title + //% "Details" + title: qsTrId("sailfish-office-he-details") + } + + DetailItem { + //: File path detail of the document + //% "File path" + label: qsTrId("sailfish-office-la-filepath") + value: info.file + alignment: Qt.AlignLeft + } + + DetailItem { + //: File size detail of the document + //% "Size" + label: qsTrId("sailfish-office-la-filesize") + value: Format.formatFileSize(info.size) + alignment: Qt.AlignLeft + } + + DetailItem { + //: File type detail of the document + //% "Type" + label: qsTrId("sailfish-office-la-filetype") + value: info.mimeTypeComment + alignment: Qt.AlignLeft + } + + DetailItem { + //: Last modified date of the document + //% "Last modified" + label: qsTrId("sailfish-office-la-lastmodified") + value: Format.formatDate(info.lastModified, Format.DateFull) + alignment: Qt.AlignLeft + } + } + + VerticalScrollDecorator {} + } +} diff --git a/usr/lib/qt5/qml/Sailfish/Office/DocumentFlickable.qml b/usr/lib/qt5/qml/Sailfish/Office/DocumentFlickable.qml new file mode 100644 index 00000000..d441bfc6 --- /dev/null +++ b/usr/lib/qt5/qml/Sailfish/Office/DocumentFlickable.qml @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2019 Jolla Ltd. + * Contact: Joona Petrell + * + * 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; version 2 only. + * + * 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. + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Sailfish.Silica.private 1.0 + +SilicaFlickable { + id: flickable + + readonly property bool zoomed: contentWidth > width + property alias pinchArea: pinchArea + default property alias foreground: pinchArea.data + + // Make sure that _noGrabbing will be reset back to false (JB#42531) + Component.onDestruction: if (!visible) pageStack._noGrabbing = false + + // Override SilicaFlickable's pressDelay because otherwise it will + // block touch events going to PinchArea in certain cases. + pressDelay: 0 + interactive: !dragDetector.horizontalDragUnused + ScrollDecorator { color: Theme.highlightDimmerColor } + + Binding { // Allow page navigation when panning the document near the top or bottom edge + target: pageStack + when: flickable.visible + property: "_noGrabbing" + value: dragDetector.horizontalDragUnused + } + + Connections { + target: pageStack + onDragInProgressChanged: { + if (pageStack.dragInProgress && pageStack._noGrabbing) { + pageStack._grabMouse() + } + } + } + + DragDetectorItem { + id: dragDetector + flickable: flickable + anchors.fill: parent + PinchArea { + id: pinchArea + + onPinchFinished: flickable.returnToBounds() + anchors.fill: parent + enabled: !pageStack.dragInProgress + } + } +} diff --git a/usr/lib/qt5/qml/Sailfish/Office/DocumentHeader.qml b/usr/lib/qt5/qml/Sailfish/Office/DocumentHeader.qml new file mode 100644 index 00000000..3723e5b7 --- /dev/null +++ b/usr/lib/qt5/qml/Sailfish/Office/DocumentHeader.qml @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2019 Jolla Ltd. + * Contact: Joona Petrell + * + * 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; version 2 only. + * + * 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. + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 + +MouseArea { + property string detailsPage: "DetailsPage.qml" + property int indexCount + property DocumentPage page + property color color: Theme.primaryColor + readonly property bool down: pressed && containsMouse + + onClicked: pageStack.animatorPush(Qt.resolvedUrl(detailsPage), { + document: page.document, + source: page.source, + mimeType: page.mimeType + }) + + width: parent.width + height: pageHeader.height + enabled: !page.busy && !page.error + + PageHeader { + id: pageHeader + title: page.title + titleColor: parent.down ? Theme.highlightColor : parent.color + rightMargin: Theme.horizontalPageMargin + detailsImage.width + Theme.paddingMedium + } + + HighlightImage { + id: detailsImage + color: parent.color + source: "image://theme/icon-m-about" + highlighted: parent.down + Behavior on opacity { FadeAnimator {}} + opacity: parent.enabled ? 1.0 : Theme.opacityHigh + + anchors { + right: parent.right + rightMargin: Theme.horizontalPageMargin + verticalCenter: parent.verticalCenter + } + } +} diff --git a/usr/lib/qt5/qml/Sailfish/Office/DocumentPage.qml b/usr/lib/qt5/qml/Sailfish/Office/DocumentPage.qml new file mode 100644 index 00000000..7e1bacfc --- /dev/null +++ b/usr/lib/qt5/qml/Sailfish/Office/DocumentPage.qml @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2013-2014 Jolla Ltd. + * Contact: Robin Burchell + * + * 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; version 2 only. + * + * 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. + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 + +Page { + id: page + + property string title + property url source + property bool error + property string mimeType + property alias busy: busyIndicator.running + property QtObject document + property QtObject provider + property Component preview: defaultPreview + property alias placeholderPreview: defaultPreview + property url icon: "image://theme/icon-m-file-other" + property alias busyIndicator: busyIndicator + + allowedOrientations: Orientation.All + clip: status !== PageStatus.Active || pageStack.dragInProgress + + PageBusyIndicator { + id: busyIndicator + z: 101 + } + + Component { + id: defaultPreview + + CoverPlaceholder { + icon.source: page.icon + text: page.title + } + } +} diff --git a/usr/lib/qt5/qml/Sailfish/Office/FullscreenError.qml b/usr/lib/qt5/qml/Sailfish/Office/FullscreenError.qml new file mode 100644 index 00000000..518d2138 --- /dev/null +++ b/usr/lib/qt5/qml/Sailfish/Office/FullscreenError.qml @@ -0,0 +1,134 @@ +/* + * Copyright (C) 2019 Jolla Ltd. + * Contact: Pekka Vuorela + * + * 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; version 2 only. + * + * 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. + */ + +import QtQuick 2.6 +import Sailfish.Silica 1.0 + +TouchBlocker { + id: root + + property string error + property string localizedError: { + // Match hard-coded error string from calligra / KoDocument.cpp + // Ideally there would be Calligra localization available, but it + // a) is stored in separate kde subversion repository together with all kinds of kde things + // b) weights some 2-3MB per language + // So since this is should be the only place really showing Calligra strings, let's just + // hack a separate translation for the few known cases. Likely even out of these many won't be + // ever shown to the user. + var re = new RegExp("Could not open file://(.*)\\.\\nReason: (.*)\\.\\n(.*)") + var matches = re.exec(error) + if (matches && matches.length == 4) { + //% "Could not open file:" + return qsTrId("sailfish-calligra_open_error") + "\n" + matches[1] + + "\n\n" + localizeOpenError(matches[2]) + } else { + console.log("Unable to parse Calligra error string", error) + return error + } + } + + anchors.fill: parent + + function localizeOpenError(error) { + switch(error) { + case "Could not create the filter plugin": + //% "Could not create the filter plugin" + return qsTrId("office_calligra_error-could_not_create_filter_plugin") + case "Could not create the output document": + //% "Could not create the output document" + return qsTrId("office_calligra_error-could_not_create_output_document") + case "File not found": + //% "File not found" + return qsTrId("office_calligra_error-file_not_found") + case "Cannot create storage": + //% "Cannot create storage" + return qsTrId("office_calligra_error-cannot_create_storage") + case "Bad MIME type": + //% "Bad MIME type" + return qsTrId("office_calligra_error-bad_mime_type") + case "Error in embedded document": + //% "Error in embedded document" + return qsTrId("office_calligra_error-error_in_embedded_document") + case "Format not recognized": + //% "Format not recognized" + return qsTrId("office_calligra_error-format_not_recognized") + case "Not implemented": + //% "Not implemented" + return qsTrId("office_calligra_error-not_implemented") + case "Parsing error": + //% "Parsing error" + return qsTrId("office_calligra_error-parsing_error") + case "Document is password protected": + //% "Document is password protected" + return qsTrId("office_calligra_error-password_protected_file") + case "Invalid file format": + //% "Invalid file format" + return qsTrId("office_calligra_error-invalid_file_format") + case "Internal error": + //% "Internal error" + return qsTrId("office_calligra_error-internal_error") + case "Out of memory": + //% "Out of memory" + return qsTrId("office_calligra_error-out_of_memory") + case "Empty Filter Plugin": + //% "Empty Filter Plugin" + return qsTrId("office_calligra_error-empty_filter_plugin") + case "Trying to load into the wrong kind of document": + //% "Trying to load into the wrong kind of document" + return qsTrId("office_calligra_error-wrong_kind_of_document") + case "Failed to download remote file": + //% "Failed to download remote file" + return qsTrId("office_calligra_error-faile_to_download_remote_file") + case "Unknown error": + //% "Unknown error" + return qsTrId("office_calligra_error-unknown") + } + + return error + } + + Rectangle { + anchors.fill: parent + opacity: Theme.opacityLow + color: Theme.highlightDimmerColor + } + + Column { + x: Theme.horizontalPageMargin + width: parent.width - 2*x + spacing: Theme.paddingMedium + anchors.verticalCenter: parent.verticalCenter + + HighlightImage { + id: warningIcon + + anchors.horizontalCenter: parent.horizontalCenter + source: "image://theme/icon-l-attention" + highlighted: true + } + + Label { + width: parent.width + text: localizedError + wrapMode: Text.Wrap + color: Theme.highlightColor + horizontalAlignment: Text.AlignHCenter + } + } +} diff --git a/usr/lib/qt5/qml/Sailfish/Office/IndexButton.qml b/usr/lib/qt5/qml/Sailfish/Office/IndexButton.qml new file mode 100644 index 00000000..23d323aa --- /dev/null +++ b/usr/lib/qt5/qml/Sailfish/Office/IndexButton.qml @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2019 Jolla Ltd. + * Contact: Joona Petrell + * + * 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; version 2 only. + * + * 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. + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 + +MouseArea { + id: root + property bool allowed: true + property color color: Theme.primaryColor + property int index + property int count + readonly property bool highlighted: pressed && containsMouse + + enabled: count > 1 && allowed + opacity: count > 0 && allowed ? (count > 1 ? 1.0 : Theme.opacityHigh) : 0.0 + width: Math.min(Theme.itemSizeMedium, label.implicitWidth + Theme.paddingSmall) + height: parent.height + + Label { + id: label + anchors.centerIn: parent + width: parent.width - Theme.paddingSmall + fontSizeMode: Text.HorizontalFit + color: root.highlighted ? Theme.highlightColor : parent.color + text: index + " | " + count + } +} diff --git a/usr/lib/qt5/qml/Sailfish/Office/OverlayToolbar.qml b/usr/lib/qt5/qml/Sailfish/Office/OverlayToolbar.qml new file mode 100644 index 00000000..b32a9e41 --- /dev/null +++ b/usr/lib/qt5/qml/Sailfish/Office/OverlayToolbar.qml @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2019 Jolla Ltd. + * Contact: Joona Petrell + * + * 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; version 2 only. + * + * 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. + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Sailfish.Silica.private 1.0 + +FadeGradient { + default property alias buttons: row.data + height: row.height + 2 * row.anchors.bottomMargin + width: parent.width + anchors.bottom: parent.bottom + + Row { + id: row + + anchors { + bottom: parent.bottom + bottomMargin: Theme.paddingLarge + horizontalCenter: parent.horizontalCenter + } + spacing: Theme.paddingLarge + } +} diff --git a/usr/lib/qt5/qml/Sailfish/Office/PDF/qmldir b/usr/lib/qt5/qml/Sailfish/Office/PDF/qmldir new file mode 100644 index 00000000..412d9098 --- /dev/null +++ b/usr/lib/qt5/qml/Sailfish/Office/PDF/qmldir @@ -0,0 +1,2 @@ +module Sailfish.Office.PDF +plugin sailfishofficepdfplugin diff --git a/usr/lib/qt5/qml/Sailfish/Office/PDFAnnotationEdit.qml b/usr/lib/qt5/qml/Sailfish/Office/PDFAnnotationEdit.qml new file mode 100644 index 00000000..393060f9 --- /dev/null +++ b/usr/lib/qt5/qml/Sailfish/Office/PDFAnnotationEdit.qml @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2016 Caliste Damien. + * Contact: Damien Caliste + * + * 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; version 2 only. + * + * 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. + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Sailfish.Office.PDF 1.0 as PDF + +Page { + id: root + + property variant annotation + + property bool _isText: annotation && (annotation.type == PDF.Annotation.Text + || annotation.type == PDF.Annotation.Caret) + + signal remove() + + SilicaFlickable { + id: flickable + anchors.fill: parent + contentHeight: content.height + + PullDownMenu { + MenuItem { + //% "Delete" + text: qsTrId("sailfish-office-mi-delete-annotation") + onClicked: root.remove() + } + } + + Column { + id: content + width: parent.width + PageHeader { + id: pageHeader + title: annotation && annotation.author != "" + ? annotation.author + : (_isText + //% "Note" + ? qsTrId("sailfish-office-hd-text-annotation") + //% "Comment" + : qsTrId("sailfish-office-hd-comment-annotation")) + } + TextArea { + id: areaContents + width: parent.width + height: Math.max(flickable.height - pageHeader.height, implicitHeight) + background: null + focus: false + text: annotation ? annotation.contents : "" + placeholderText: _isText + //% "Write a note…" + ? qsTrId("sailfish-office-ta-text-annotation-edit") + //% "Write a comment…" + : qsTrId("sailfish-office-ta-comment-annotation-edit") + onTextChanged: { + if (annotation) { + annotation.contents = text + } + } + } + } + VerticalScrollDecorator { flickable: flickable } + } +} diff --git a/usr/lib/qt5/qml/Sailfish/Office/PDFAnnotationNew.qml b/usr/lib/qt5/qml/Sailfish/Office/PDFAnnotationNew.qml new file mode 100644 index 00000000..fd611730 --- /dev/null +++ b/usr/lib/qt5/qml/Sailfish/Office/PDFAnnotationNew.qml @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2016 Caliste Damien. + * Contact: Damien Caliste + * + * 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; version 2 only. + * + * 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. + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 + +Dialog { + id: root + property alias text: areaContents.text + property bool isTextAnnotation + + SilicaFlickable { + id: flickable + anchors.fill: parent + contentHeight: content.height + + Column { + id: content + width: parent.width + DialogHeader { + id: dialogHeader + //% "Save" + acceptText: qsTrId("sailfish-office-he-txt-anno-save") + //% "Cancel" + cancelText: qsTrId("sailfish-office-he-txt-anno-cancel") + } + TextArea { + id: areaContents + width: parent.width + height: Math.max(flickable.height - dialogHeader.height, implicitHeight) + placeholderText: isTextAnnotation + //% "Write a note…" + ? qsTrId("sailfish-office-ta-text-annotation") + //% "Write a comment…" + : qsTrId("sailfish-office-ta-comment-annotation") + background: null + focus: true + } + } + VerticalScrollDecorator { flickable: flickable } + } +} diff --git a/usr/lib/qt5/qml/Sailfish/Office/PDFContextMenuHighlight.qml b/usr/lib/qt5/qml/Sailfish/Office/PDFContextMenuHighlight.qml new file mode 100644 index 00000000..3a0feb66 --- /dev/null +++ b/usr/lib/qt5/qml/Sailfish/Office/PDFContextMenuHighlight.qml @@ -0,0 +1,134 @@ +/* + * Copyright (C) 2016 Caliste Damien. + * Contact: Damien Caliste + * + * 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; version 2 only. + * + * 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. + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Sailfish.Office.PDF 1.0 + +ContextMenu { + id: contextMenuHighlight + + property Annotation annotation + + InfoLabel { + id: infoContents + visible: infoContents.text != "" + width: parent.width + height: implicitHeight + 2 * Theme.paddingSmall + font.pixelSize: Theme.fontSizeSmall + verticalAlignment: Text.AlignVCenter + wrapMode: Text.Wrap + elide: Text.ElideRight + maximumLineCount: 2 + color: Theme.highlightColor + opacity: .6 + text: { + if (contextMenuHighlight.annotation + && contextMenuHighlight.annotation.contents != "") { + return (contextMenuHighlight.annotation.author != "" + ? "(" + contextMenuHighlight.annotation.author + ") " : "") + + contextMenuHighlight.annotation.contents + } else { + return "" + } + } + } + Row { + height: Theme.itemSizeExtraSmall + Repeater { + id: colors + model: ["#db431c", "#ffff00", "#8afa72", "#00ffff", + "#3828f9", "#a328c7", "#ffffff", "#989898", + "#000000"] + delegate: Rectangle { + width: contextMenuHighlight.width / colors.model.length + height: parent.height + color: modelData + MouseArea { + anchors.fill: parent + onClicked: { + contextMenuHighlight.close() + contextMenuHighlight.annotation.color = color + highlightColorConfig.value = modelData + } + } + } + } + } + Row { + height: Theme.itemSizeExtraSmall + Repeater { + id: styles + model: [{"style": HighlightAnnotation.Highlight, + "label": "abc"}, + {"style": HighlightAnnotation.Squiggly, + "label": "a̰b̰c̰"}, + {"style": HighlightAnnotation.Underline, + "label": "abc"}, + {"style": HighlightAnnotation.StrikeOut, + "label": "abc"}] + delegate: BackgroundItem { + id: bgStyle + width: contextMenuHighlight.width / styles.model.length + height: parent.height + onClicked: { + contextMenuHighlight.close() + contextMenuHighlight.annotation.style = modelData["style"] + highlightStyleConfig.value = highlightStyleConfig.fromEnum(modelData["style"]) + } + Label { + anchors.centerIn: parent + text: modelData["label"] + textFormat: Text.RichText + color: bgStyle.highlighted + || (contextMenuHighlight.annotation + && contextMenuHighlight.annotation.style == modelData["style"]) + ? Theme.highlightColor : Theme.primaryColor + Rectangle { + visible: modelData["style"] == HighlightAnnotation.Highlight + anchors.fill: parent + color: bgStyle.highlighted ? Theme.highlightColor : Theme.primaryColor + opacity: Theme.opacityLow + z: -1 + } + } + } + } + } + MenuItem { + visible: contextMenuHighlight.annotation + text: contextMenuHighlight.annotation + && contextMenuHighlight.annotation.contents == "" + //% "Add a comment" + ? qsTrId("sailfish-office-me-pdf-hl-anno-comment") + //% "Edit the comment" + : qsTrId("sailfish-office-me-pdf-hl-anno-comment-edit") + onClicked: { + if (contextMenuHighlight.annotation.contents == "") { + doc.create(contextMenuHighlight.annotation) + } else { + doc.edit(contextMenuHighlight.annotation) + } + } + } + MenuItem { + //% "Clear" + text: qsTrId("sailfish-office-me-pdf-hl-anno-clear") + onClicked: contextMenuHighlight.annotation.remove() + } +} diff --git a/usr/lib/qt5/qml/Sailfish/Office/PDFContextMenuLinks.qml b/usr/lib/qt5/qml/Sailfish/Office/PDFContextMenuLinks.qml new file mode 100644 index 00000000..da2126c4 --- /dev/null +++ b/usr/lib/qt5/qml/Sailfish/Office/PDFContextMenuLinks.qml @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2016 Caliste Damien. + * Contact: Damien Caliste + * + * 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; version 2 only. + * + * 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. + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 + +ContextMenu { + id: contextMenuLinks + property alias url: linkTarget.text + + InfoLabel { + id: linkTarget + font.pixelSize: Theme.fontSizeSmall + wrapMode: Text.Wrap + elide: Text.ElideRight + maximumLineCount: 4 + color: Theme.highlightColor + opacity: .6 + } + MenuItem { + text: (contextMenuLinks.url.indexOf("http:") === 0 + || contextMenuLinks.url.indexOf("https:") === 0) + //% "Open in browser" + ? qsTrId("sailfish-office-me-pdf-open-browser") + //% "Open in external application" + : qsTrId("sailfish-office-me-pdf-open-external") + onClicked: Qt.openUrlExternally(contextMenuLinks.url) + } + MenuItem { + //% "Copy to clipboard" + text: qsTrId("sailfish-office-me-pdf-copy-link") + onClicked: Clipboard.text = contextMenuLinks.url + } +} diff --git a/usr/lib/qt5/qml/Sailfish/Office/PDFContextMenuText.qml b/usr/lib/qt5/qml/Sailfish/Office/PDFContextMenuText.qml new file mode 100644 index 00000000..2f9a5d54 --- /dev/null +++ b/usr/lib/qt5/qml/Sailfish/Office/PDFContextMenuText.qml @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2016 Caliste Damien. + * Contact: Damien Caliste + * + * 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; version 2 only. + * + * 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. + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Sailfish.Office.PDF 1.0 + +ContextMenu { + id: contextMenuText + property Annotation annotation + property point at + + MenuItem { + visible: !contextMenuText.annotation + //% "Add note" + text: qsTrId("sailfish-office-me-pdf-txt-anno-add") + onClicked: { + var annotation = textComponent.createObject(contextMenuText) + annotation.color = "#202020" + doc.create(annotation, + function() { + var at = view.getPositionAt(contextMenuText.at) + annotation.attachAt(doc, at[0], at[2], at[1]) + }) + } + Component { + id: textComponent + TextAnnotation { } + } + } + MenuItem { + visible: contextMenuText.annotation + //% "Edit" + text: qsTrId("sailfish-office-me-pdf-txt-anno-edit") + onClicked: doc.edit(contextMenuText.annotation) + } + MenuItem { + visible: contextMenuText.annotation + //% "Delete" + text: qsTrId("sailfish-office-me-pdf-txt-anno-clear") + onClicked: contextMenuText.annotation.remove() + } +} diff --git a/usr/lib/qt5/qml/Sailfish/Office/PDFDetailsPage.qml b/usr/lib/qt5/qml/Sailfish/Office/PDFDetailsPage.qml new file mode 100644 index 00000000..e6088da0 --- /dev/null +++ b/usr/lib/qt5/qml/Sailfish/Office/PDFDetailsPage.qml @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2024 Damien Caliste + * Contact: Damien Caliste + * + * 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; version 2 only. + * + * 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. + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Sailfish.Office 1.0 + +DetailsPage { + readonly property bool storePassword: document.password.length > 0 + + DetailItem { + //: Page count of the PDF document + //% "Page Count" + label: qsTrId("sailfish-office-la-pdf-pagecount") + value: document.pageCount + alignment: Qt.AlignLeft + visible: !document.locked + } + SectionHeader { + visible: document.passwordProtected + //% "Read protection" + text: qsTrId("sailfish-office-la-pdf-readprotection") + } + Label { + visible: document.passwordProtected + width: parent.width - 2*x + x: Theme.horizontalPageMargin + //% "This document is protected by a password" + text: qsTrId("sailfish-office-la-pdf-passwordprotected") + color: palette.secondaryHighlightColor + font.pixelSize: Theme.fontSizeSmall + textFormat: Text.PlainText + wrapMode: Text.Wrap + } + Item { + visible: storePassword + width: parent.width + height: Theme.paddingLarge + } + PasswordField { + opacity: storePassword ? 1 : 0 + Behavior on opacity {FadeAnimator {}} + x: Theme.horizontalPageMargin + width: parent.width - 2*x + text: document.password + readOnly: true + } + Item { + visible: storePassword + width: parent.width + height: Theme.paddingLarge + } + Button { + opacity: storePassword ? 1 : 0 + Behavior on opacity {FadeAnimator {}} + anchors.horizontalCenter: parent.horizontalCenter + //% "Clear stored password" + text: qsTrId("sailfish-office-la-pdf-clearpassword") + onClicked: document.clearCachedPassword() + } +} diff --git a/usr/lib/qt5/qml/Sailfish/Office/PDFDocumentPage.qml b/usr/lib/qt5/qml/Sailfish/Office/PDFDocumentPage.qml new file mode 100644 index 00000000..522084f4 --- /dev/null +++ b/usr/lib/qt5/qml/Sailfish/Office/PDFDocumentPage.qml @@ -0,0 +1,592 @@ +/* + * Copyright (C) 2013-2021 Jolla Ltd. + * Copyright (C) 2021 Open Mobile Platform LLC. + * Contact: Robin Burchell + * + * 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; version 2 only. + * + * 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. + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Sailfish.Share 1.0 +import Sailfish.Office.PDF 1.0 as PDF +import Nemo.Configuration 1.0 +import Nemo.Notifications 1.0 +import QtQuick.LocalStorage 2.0 +import "PDFStorage.js" as PDFStorage + +DocumentPage { + id: page + + property var _settings // Handle save and restore the view settings using PDFStorage + property ContextMenu contextMenuLinks + property ContextMenu contextMenuText + property ContextMenu contextMenuHighlight + + icon: "image://theme/icon-m-file-pdf" + busy: (!doc.loaded && !doc.failure) || doc.searching + error: doc.failure + source: doc.source + document: doc + + preview: doc.loaded + ? previewComponent + : placeholderPreview + + function savePageSettings() { + if (!rememberPositionConfig.value || doc.failure || doc.locked) { + return + } + + if (!_settings) { + _settings = new PDFStorage.Settings(doc.source) + } + var last = view.getPagePosition() + _settings.setLastPage(last[0] + 1, last[1], last[2], view.itemWidth) + } + + // Save and restore view settings when needed. + onStatusChanged: if (status == PageStatus.Inactive) { savePageSettings() } + + Connections { + target: Qt.application + onAboutToQuit: savePageSettings() + } + Connections { + target: view + onPageSizesReady: { + if (rememberPositionConfig.value) { + if (!_settings) { + _settings = new PDFStorage.Settings(doc.source) + } + var last = _settings.getLastPage() + if (last[3] > 0) { + view.itemWidth = last[3] + view.adjust() + } + view.goToPage( last[0] - 1, last[1], last[2] ) + } + } + } + + Component { + id: previewComponent + + Item { + id: coverPreview + + PDF.Canvas { + width: coverPreview.width + + objectName: "cover" + + document: doc + flickable: coverPreview + linkColor: Theme.highlightColor + + onPageLayoutChanged: { + var pageRect = pageRectangle(view.currentPage - 1) + + y = -pageRect.y + ((coverPreview.height - pageRect.height) / 2) + } + } + } + } + + PDFView { + id: view + + // for cover state + property bool contentAvailable: doc.loaded && !(doc.failure || doc.locked) + property alias title: page.title + property alias mimeType: page.mimeType + + anchors { + fill: parent + bottomMargin: toolbar.offset + } + document: doc + header: header + clip: anchors.bottomMargin > 0 + + enabled: doc.loaded + opacity: enabled ? 1.0 : 0.0 + Behavior on opacity { FadeAnimator { duration: 400 }} + + onCanMoveBackChanged: if (canMoveBack) toolbar.show() + + onLinkClicked: { + if (!contextMenuLinks) { + contextMenuLinks = contextMenuLinksComponent.createObject(page) + } + contextMenuLinks.url = linkTarget + hook.showMenu(contextMenuLinks) + } + + onAnnotationClicked: { + switch (annotation.type) { + case PDF.Annotation.Highlight: + if (!contextMenuHighlight) { + contextMenuHighlight = contextMenuHighlightComponent.createObject(page) + } + contextMenuHighlight.annotation = annotation + hook.showMenu(contextMenuHighlight) + break + case PDF.Annotation.Caret: + case PDF.Annotation.Text: + doc.edit(annotation) + break + default: + } + } + + onAnnotationLongPress: { + switch (annotation.type) { + case PDF.Annotation.Highlight: + if (!contextMenuHighlight) { + contextMenuHighlight = contextMenuHighlightComponent.createObject(page) + } + contextMenuHighlight.annotation = annotation + hook.showMenu(contextMenuHighlight) + break + case PDF.Annotation.Caret: + case PDF.Annotation.Text: + if (!contextMenuText) { + contextMenuText = contextMenuTextComponent.createObject(page) + } + contextMenuText.annotation = annotation + hook.showMenu(contextMenuText) + break + default: + } + } + + onLongPress: { + if (!contextMenuText) { + contextMenuText = contextMenuTextComponent.createObject(page) + } + contextMenuText.at = pressAt + contextMenuText.annotation = null + hook.showMenu(contextMenuText) + } + + PullDownMenu { + MenuItem { + //% "Delete" + text: qsTrId("sailfish-office-me-delete") + + onClicked: window._mainPage.deleteSource(page.source) + } + MenuItem { + //% "Share" + text: qsTrId("sailfish-office-me-share") + onClicked: { + shareAction.resources = [page.source] + shareAction.trigger() + } + ShareAction { + id: shareAction + mimeType: page.mimeType + } + } + } + + DocumentHeader { + id: header + page: page + detailsPage: "PDFDetailsPage.qml" + indexCount: doc.pageCount + width: page.width + x: view.contentX + } + } + + ToolBar { + id: toolbar + + property bool active: indexButton.highlighted + || linkBack.visible + || search.highlighted + || search.active + || textTool.highlighted + || highlightTool.highlighted + || view.selection.selected + + property Item activeItem + + function toggle(item) { + if (toolbar.notice) toolbar.notice.hide() + view.selection.unselect() + if (toolbar.activeItem === item) { + toolbar.activeItem = null + } else { + toolbar.activeItem = item + } + } + + flickable: view + anchors.top: view.bottom + enabled: doc.loaded + opacity: enabled ? 1.0 : 0.0 + Behavior on opacity { FadeAnimator { duration: 400 }} + + forceHidden: doc.failure || doc.locked + || (contextMenuLinks && contextMenuLinks.active) + || (contextMenuHighlight && contextMenuHighlight.active) + || (contextMenuText && contextMenuText.active) + autoShowHide: !toolbar.active + + Connections { + target: view.selection + onSelectedChanged: if (view.selection.selected) toolbar.show() + } + + + SearchBarItem { + id: search + width: textTool.width + expandedWidth: page.width + height: parent.height + + searching: doc.searching + searchProgress: doc.searchModel ? doc.searchModel.fraction : 0. + matchCount: doc.searchModel ? doc.searchModel.count : -1 + + onRequestSearch: doc.search(text, view.currentPage - 1) + onRequestPreviousMatch: view.prevSearchMatch() + onRequestNextMatch: view.nextSearchMatch() + onRequestCancel: doc.cancelSearch(!doc.searching) + onClicked: toolbar.toggle(search) + } + + IconButton { + id: textTool + property bool first: true + + onClicked: { + toolbar.toggle(textTool) + if (textTool.first) { + //% "Tap where you want to add a note" + noticeShow(qsTrId("sailfish-office-la-notice-anno-text")) + textTool.first = false + } + } + + anchors.verticalCenter: parent.verticalCenter + highlighted: pressed || toolbar.activeItem === textTool + icon.source: toolbar.activeItem === textTool ? "image://theme/icon-m-annotation-selected" + : "image://theme/icon-m-annotation" + MouseArea { + parent: toolbar.activeItem === textTool ? view : null + anchors.fill: parent + onClicked: { + var annotation = textComponent.createObject(textTool) + var pt = Qt.point(view.contentX + mouse.x, view.contentY + mouse.y) + doc.create(annotation, + function() { + var at = view.getPositionAt(pt) + annotation.attachAt(doc, + at[0], at[2], at[1]) + }) + toolbar.toggle(textTool) + } + Component { + id: textComponent + PDF.TextAnnotation { } + } + } + } + + IconButton { + id: highlightTool + property bool first: true + + function highlightSelection() { + var anno = highlightComponent.createObject(highlightTool) + anno.color = highlightColorConfig.value + anno.style = highlightStyleConfig.toEnum(highlightStyleConfig.value) + anno.attach(doc, view.selection) + toolbar.hide() + } + + onClicked: { + if (view.selection.selected) { + highlightSelection() + view.selection.unselect() + return + } + toolbar.toggle(highlightTool) + if (first) { + //% "Tap and move your finger over the area" + noticeShow(qsTrId("sailfish-office-la-notice-anno-highlight")) + first = false + } + } + + Component { + id: highlightComponent + PDF.HighlightAnnotation { } + } + + anchors.verticalCenter: parent.verticalCenter + highlighted: pressed || toolbar.activeItem === highlightTool + icon.source: toolbar.activeItem === highlightTool ? "image://theme/icon-m-edit-selected" + : "image://theme/icon-m-edit" + MouseArea { + parent: toolbar.activeItem === highlightTool ? view : null + anchors.fill: parent + preventStealing: true + onPressed: { + var pt = mapToItem(view.canvas, mouse.x, mouse.y) + view.selection.selectAt(pt) + } + onPositionChanged: { + var pt = mapToItem(view.canvas, mouse.x, mouse.y) + if (view.selection.count < 1) { + view.selection.selectAt(pt) + } else { + view.selection.handle2 = pt + } + } + onReleased: { + if (view.selection.selected) highlightTool.highlightSelection() + toolbar.toggle(highlightTool) + } + Binding { + target: view + property: "selectionDraggable" + value: toolbar.activeItem !== highlightTool + } + } + } + + IconButton { + id: linkBack + anchors.verticalCenter: parent.verticalCenter + opacity: view.canMoveBack ? 1. : 0. + visible: opacity > 0 + Behavior on opacity { FadeAnimator { duration: 400 } } + icon.source: "image://theme/icon-m-back" + onClicked: { + toolbar.toggle(linkBack) + view.moveBack() + toolbar.hide() + } + } + + IndexButton { + id: indexButton + onClicked: { + toolbar.toggle(indexButton) + var obj = pageStack.animatorPush(Qt.resolvedUrl("PDFDocumentToCPage.qml"), + { tocModel: doc.tocModel, pageCount: doc.pageCount }) + + obj.pageCompleted.connect(function(page) { + page.onPageSelected.connect(function(pageNumber) { view.goToPage(pageNumber) } ) + }) + } + + index: view.currentPage + count: doc.pageCount + allowed: !doc.failure && !doc.locked + } + } + + PDF.Document { + id: doc + source: page.source + autoSavePath: page.source + + function create(annotation, callback) { + var isText = (annotation.type == PDF.Annotation.Text + || annotation.type == PDF.Annotation.Caret) + var obj = pageStack.animatorPush(Qt.resolvedUrl("PDFAnnotationNew.qml"), + {"isTextAnnotation": isText}) + obj.pageCompleted.connect(function(dialog) { + dialog.accepted.connect(function() { + annotation.contents = dialog.text + }) + if (callback !== undefined) dialog.accepted.connect(callback) + }) + } + function edit(annotation) { + var obj = pageStack.animatorPush(Qt.resolvedUrl("PDFAnnotationEdit.qml"), + {"annotation": annotation}) + obj.pageCompleted.connect(function(edit) { + edit.remove.connect(function() { + annotation.remove() + pageStack.pop() + }) + }) + } + } + + Loader { + parent: page + active: doc.failure || doc.locked + sourceComponent: placeholderComponent + anchors.verticalCenter: parent.verticalCenter + } + + property Notification notice + + function noticeShow(message) { + if (!notice) { + notice = noticeComponent.createObject(toolbar) + } + notice.show(message) + } + + Component { + id: noticeComponent + Notification { + property bool published + function show(info) { + previewSummary = info + if (published) close() + publish() + published = true + } + function hide() { + if (published) close() + published = false + } + } + } + + Component { + id: placeholderComponent + + Column { + width: page.width + + InfoLabel { + text: doc.failure ? //% "Broken file" + qsTrId("sailfish-office-me-broken-pdf") + : //% "Locked file" + qsTrId("sailfish-office-me-locked-pdf") + } + + InfoLabel { + font.pixelSize: Theme.fontSizeLarge + color: Theme.rgba(Theme.highlightColor, Theme.opacityLow) + text: doc.failure ? //% "Cannot read the PDF document" + qsTrId("sailfish-office-me-broken-pdf-hint") + : //% "Enter password to unlock" + qsTrId("sailfish-office-me-locked-pdf-hint") + } + + Item { + visible: password.visible + width: 1 + height: Theme.paddingLarge + } + + PasswordField { + id: password + + visible: doc.locked + x: Theme.horizontalPageMargin + width: parent.width - 2*x + EnterKey.enabled: text + EnterKey.iconSource: "image://theme/icon-m-enter-accept" + EnterKey.onClicked: { + focus = false + doc.requestUnLock(text, storePassword.checked) + text = "" + } + + Component.onCompleted: { + if (visible) + forceActiveFocus() + } + } + + TextSwitch { + id: storePassword + visible: doc.locked + x: Theme.horizontalPageMargin + width: parent.width - 2*x + //% "Remember password" + text: qsTrId("sailfish-office-lbl-remember-password") + } + } + } + + Component { + id: contextMenuLinksComponent + PDFContextMenuLinks { } + } + + Component { + id: contextMenuTextComponent + PDFContextMenuText { } + } + + Component { + id: contextMenuHighlightComponent + PDFContextMenuHighlight { } + } + + ConfigurationValue { + id: rememberPositionConfig + + key: "/apps/sailfish-office/settings/rememberPosition" + defaultValue: true + } + ConfigurationValue { + id: highlightColorConfig + key: "/apps/sailfish-office/settings/highlightColor" + defaultValue: "#ffff00" + } + ConfigurationValue { + id: highlightStyleConfig + key: "/apps/sailfish-office/settings/highlightStyle" + defaultValue: "highlight" + + function toEnum(configVal) { + if (configVal == "highlight") { + return PDF.HighlightAnnotation.Highlight + } else if (configVal == "squiggly") { + return PDF.HighlightAnnotation.Squiggly + } else if (configVal == "underline") { + return PDF.HighlightAnnotation.Underline + } else if (configVal == "strike") { + return PDF.HighlightAnnotation.StrikeOut + } else { + return PDF.HighlightAnnotation.Highlight + } + } + function fromEnum(enumVal) { + switch (enumVal) { + case PDF.HighlightAnnotation.Highlight: + return "highlight" + case PDF.HighlightAnnotation.Squiggly: + return "squiggly" + case PDF.HighlightAnnotation.Underline: + return "underline" + case PDF.HighlightAnnotation.StrikeOut: + return "strike" + default: + return "highlight" + } + } + } + + Timer { + id: updateSourceSizeTimer + interval: 5000 + onTriggered: linkArea.sourceSize = Qt.size(page.width, pdfCanvas.height) + } +} diff --git a/usr/lib/qt5/qml/Sailfish/Office/PDFDocumentToCPage.qml b/usr/lib/qt5/qml/Sailfish/Office/PDFDocumentToCPage.qml new file mode 100644 index 00000000..f7a99345 --- /dev/null +++ b/usr/lib/qt5/qml/Sailfish/Office/PDFDocumentToCPage.qml @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2013-2014 Jolla Ltd. + * Contact: Robin Burchell + * + * 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; version 2 only. + * + * 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. + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 + +Page { + id: page + + property int pageCount + property alias tocModel: tocListView.model + + signal pageSelected(int pageNumber) + + allowedOrientations: Orientation.All + + onTocModelChanged: tocModel.requestToc() + + SilicaListView { + id: tocListView + + width: parent.width + height: parent.height - gotoPage.height + clip: true + + //: Page with PDF index + //% "Index" + header: PageHeader { title: qsTrId("sailfish-office-he-pdf_index") } + + ViewPlaceholder { + id: placeholder + enabled: tocListView.model + && tocListView.model.ready + && tocListView.model.count == 0 + //% "Document has no table of content" + text: qsTrId("sailfish-office-me-no-toc") + } + PageBusyIndicator { + running: !tocListView.model || !tocListView.model.ready + z: 1 + } + + delegate: BackgroundItem { + id: bg + + Label { + anchors { + left: parent.left + leftMargin: Theme.horizontalPageMargin + (Theme.paddingLarge * model.level) + right: pageNumberLabel.left + rightMargin: Theme.paddingLarge + verticalCenter: parent.verticalCenter + } + elide: Text.ElideRight + text: (model.title === undefined) ? "" : model.title + color: bg.highlighted ? Theme.highlightColor : Theme.primaryColor + truncationMode: TruncationMode.Fade + } + Label { + id: pageNumberLabel + anchors { + right: parent.right + rightMargin: Theme.horizontalPageMargin + verticalCenter: parent.verticalCenter + } + text: (model.pageNumber === undefined) ? "" : model.pageNumber + color: bg.highlighted ? Theme.highlightColor : Theme.primaryColor + } + + onClicked: { + page.pageSelected(model.pageNumber - 1) + pageStack.navigateBack(PageStackAction.Animated) + } + } + + VerticalScrollDecorator { } + } + + PanelBackground { + id: gotoPage + + anchors.top: tocListView.bottom + width: parent.width + height: Theme.itemSizeMedium + + TextField { + property IntValidator _validator: IntValidator {bottom: 1; top: page.pageCount } + + x: Theme.paddingLarge + width: parent.width - Theme.paddingMedium - Theme.paddingLarge + anchors.verticalCenter: parent.verticalCenter + + //% "Go to page" + placeholderText: qsTrId("sailfish-office-lb-goto-page") + //% "document has %n pages" + label: qsTrId("sailfish-office-lb-%n-pages", page.pageCount) + + // We enter page numbers + validator: text.length ? _validator : null + inputMethodHints: Qt.ImhDigitsOnly + EnterKey.enabled: text.length > 0 && acceptableInput + EnterKey.iconSource: "image://theme/icon-m-enter-accept" + EnterKey.onClicked: { + page.pageSelected(Math.round(text) - 1) + pageStack.navigateBack(PageStackAction.Animated) + } + } + } +} diff --git a/usr/lib/qt5/qml/Sailfish/Office/PDFSelectionDrag.qml b/usr/lib/qt5/qml/Sailfish/Office/PDFSelectionDrag.qml new file mode 100644 index 00000000..9c81b960 --- /dev/null +++ b/usr/lib/qt5/qml/Sailfish/Office/PDFSelectionDrag.qml @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2016 Caliste Damien. + * Contact: Damien Caliste + * + * 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; version 2 only. + * + * 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. + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 + +MouseArea { + id: root + + property point handle + property Item flickable + + property real _contentY0 + property real _contentY: flickable ? flickable.contentY : 0.0 + property real _dragX0 + property real _dragY0 + property real dragX + property real dragY + + signal dragged(point at) + + function reset() { + dragX = 0 + dragY = 0 + } + + width: Theme.itemSizeSmall + height: width + + enabled: visible + preventStealing: true + onPressed: { + _dragX0 = mouseX + _dragY0 = mouseY + _contentY0 = (flickable ? flickable.contentY : 0.0) + } + onCanceled: reset() + onReleased: reset() + + Binding { + target: root + property: "x" + value: root.handle.x - root.width / 2 + when: !root.pressed + } + Binding { + target: root + property: "y" + value: root.handle.y - root.height / 2 + when: !root.pressed + } + onMouseXChanged: dragX = pressed ? mouseX - _dragX0 : 0. + onMouseYChanged: dragY = pressed ? mouseY - _dragY0 - _contentY + _contentY0 : 0. + onDragXChanged: { + if (pressed) { + root.dragged(Qt.point(x + width / 2 + dragX, y + height / 2 + dragY)) + } + } + onDragYChanged: { + if (pressed) { + root.dragged(Qt.point(x + width / 2 + dragX, y + height / 2 + dragY)) + } + } + + Rectangle { + x: (root.width - width) / 2 + dragX + y: (root.height - height) / 2 + dragY + width: Theme.iconSizeSmall / 2 * 1.414 + height: width + visible: opacity > 0. + opacity: root.pressed ? 0.25 : 0. + Behavior on opacity { FadeAnimator {} } + radius: width / 2 + color: Qt.rgba(1. - Theme.highlightDimmerColor.r, + 1. - Theme.highlightDimmerColor.g, + 1. - Theme.highlightDimmerColor.b, + 1.) + Rectangle { + anchors.centerIn: parent + color: Theme.highlightDimmerColor + width: Theme.iconSizeSmall / 2 + height: width + radius: width / 2 + } + } +} diff --git a/usr/lib/qt5/qml/Sailfish/Office/PDFSelectionHandle.qml b/usr/lib/qt5/qml/Sailfish/Office/PDFSelectionHandle.qml new file mode 100644 index 00000000..5a9bc823 --- /dev/null +++ b/usr/lib/qt5/qml/Sailfish/Office/PDFSelectionHandle.qml @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2016 Caliste Damien. + * Contact: Damien Caliste + * + * 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; version 2 only. + * + * 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. + */ + +import QtQuick 2.6 +import Sailfish.Silica 1.0 + +Rectangle { + id: root + + property alias attachX: translationMove.from + property point handle + property bool dragged + property real dragHeight + + x: handle.x - width / 2 + y: handle.y - height / 2 + opacity: 0.5 + color: Theme.highlightDimmerColor + width: Math.round(Theme.iconSizeSmall / 4) * 2 // ensure even number + height: width + radius: width / 2 + + states: State { + when: dragged + name: "dragged" + PropertyChanges { + target: root + width: Theme.paddingSmall / 2 + height: dragHeight + radius: 0 + } + } + + transitions: Transition { + to: "dragged" + reversible: true + SequentialAnimation { + NumberAnimation { property: "width"; duration: 100 } + PropertyAction { property: "radius" } + NumberAnimation { property: "height"; duration: 100 } + } + } + + ParallelAnimation { + id: appearingMove + FadeAnimator { + target: root + from: 0.0 + to: 0.5 + } + XAnimator { + id: translationMove + duration: 200 + easing.type: Easing.InOutQuad + target: root + to: root.x + } + } + + onVisibleChanged: { + if (visible) { + appearingMove.start() + } + } +} diff --git a/usr/lib/qt5/qml/Sailfish/Office/PDFSelectionView.qml b/usr/lib/qt5/qml/Sailfish/Office/PDFSelectionView.qml new file mode 100644 index 00000000..a690b9c1 --- /dev/null +++ b/usr/lib/qt5/qml/Sailfish/Office/PDFSelectionView.qml @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2016 Caliste Damien. + * Contact: Damien Caliste + * + * 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; version 2 only. + * + * 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. + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 + +Repeater { + id: root + + property Item flickable + property bool draggable: true + property alias dragHandle1: handle1.dragged + property alias dragHandle2: handle2.dragged + + visible: (model !== undefined && model.count > 0) + + delegate: Rectangle { + opacity: 0.5 + color: Theme.highlightColor + x: rect.x + y: rect.y + width: rect.width + height: rect.height + } + + children: [ + PDFSelectionHandle { + id: handle1 + visible: root.draggable + attachX: root.flickable !== undefined + ? flickable.contentX + : handle.x - Theme.itemSizeExtraLarge + handle: root.model.handle1 + dragHeight: root.model.handle1Height + }, + PDFSelectionHandle { + id: handle2 + visible: root.draggable + attachX: root.flickable !== undefined + ? flickable.contentX + flickable.width + : handle.x + Theme.itemSizeExtraLarge + handle: root.model.handle2 + dragHeight: root.model.handle2Height + } + ] +} diff --git a/usr/lib/qt5/qml/Sailfish/Office/PDFStorage.js b/usr/lib/qt5/qml/Sailfish/Office/PDFStorage.js new file mode 100644 index 00000000..a02d9383 --- /dev/null +++ b/usr/lib/qt5/qml/Sailfish/Office/PDFStorage.js @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2015 Caliste Damien. + * Contact: Damien Caliste + * + * 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; version 2 only. + * + * 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. + */ + +var Settings = function(file) { + this.db = LocalStorage.openDatabaseSync("sailfish-office", "1.0", + "Local storage for the document viewer.", 10000); + this.source = file +} + +/* Different tables. */ +function createTableLastViewSettings(tx) { + /* Currently store the last page, may be altered later to store + zoom level or page position. */ + tx.executeSql("CREATE TABLE IF NOT EXISTS LastViewSettings(" + + "file TEXT NOT NULL," + + "page INT NOT NULL," + + "top REAL ," + + "left REAL ," + + "width INT CHECK(width > 0))"); + tx.executeSql('CREATE UNIQUE INDEX IF NOT EXISTS idx_file ON LastViewSettings(file)'); +} + +/* Get and set operations. */ +Settings.prototype.getLastPage = function() { + var page = 0 + var top = 0 + var left = 0 + var width = 0 + var file = this.source + this.db.transaction(function(tx) { + createTableLastViewSettings(tx); + var rs = tx.executeSql('SELECT page, top, left, width FROM LastViewSettings WHERE file = ?', [file]); + if (rs.rows.length > 0) { + page = rs.rows.item(0).page; + top = rs.rows.item(0).top; + left = rs.rows.item(0).left; + width = rs.rows.item(0).width; + } + }); + // Return page is in [1:] + return [page, top, left, width]; +} +Settings.prototype.setLastPage = function(page, top, left, width) { + // page is in [1:] + var file = this.source + this.db.transaction(function(tx) { + createTableLastViewSettings(tx); + var rs = tx.executeSql('INSERT OR REPLACE INTO LastViewSettings(file, page, top, left, width) VALUES (?,?,?,?,?)', [file, page, top, left, width]); + }); +} diff --git a/usr/lib/qt5/qml/Sailfish/Office/PDFView.qml b/usr/lib/qt5/qml/Sailfish/Office/PDFView.qml new file mode 100644 index 00000000..c3a03c94 --- /dev/null +++ b/usr/lib/qt5/qml/Sailfish/Office/PDFView.qml @@ -0,0 +1,534 @@ +/* + * Copyright (C) 2013-2014 Jolla Ltd. + * Contact: Robin Burchell + * + * 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; version 2 only. + * + * 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. + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Sailfish.Silica.private 1.0 +import Sailfish.Office.PDF 1.0 as PDF +import Nemo.Configuration 1.0 + +DocumentFlickable { + id: root + + property alias itemWidth: pdfCanvas.width + property alias itemHeight: pdfCanvas.height + + property Item header + property alias canvas: pdfCanvas + property alias document: pdfCanvas.document + property int currentPage: !quickScrollAnimation.running + ? pdfCanvas.currentPage : quickScrollAnimation.pageTo + property alias selection: pdfSelection + property alias selectionDraggable: selectionView.draggable + property bool canMoveBack: (_contentYAtGotoLink >= 0) + + property QtObject _feedbackEffect + + property int _pageAtLinkTarget + property int _pageAtGotoLink + property real _contentXAtGotoLink: -1. + property real _contentYAtGotoLink: -1. + + property int _searchIndex + + signal clicked() + signal linkClicked(string linkTarget, Item hook) + signal selectionClicked(variant selection, Item hook) + signal annotationClicked(variant annotation, Item hook) + signal annotationLongPress(variant annotation, Item hook) + signal longPress(point pressAt, Item hook) + signal pageSizesReady() + signal updateSize(real newWidth, real newHeight) + + function clamp(value) { + var maximumZoom = Math.min(Screen.height, Screen.width) * maxZoomLevelConfig.value + return Math.max(width, Math.min(value, maximumZoom)) + } + + function zoom(amount, center) { + var oldWidth = pdfCanvas.width + var oldHeight = pdfCanvas.height + var oldContentX = contentX + var oldContentY = contentY + + pdfCanvas.width = clamp(pdfCanvas.width * amount) + + /* One cannot use += here because changing contentX will change contentY + to adjust to new height, so we use saved values. */ + contentX = oldContentX + (center.x * pdfCanvas.width / oldWidth) - center.x + contentY = oldContentY + (center.y * pdfCanvas.height / oldHeight) - center.y + + _contentXAtGotoLink = -1. + _contentYAtGotoLink = -1. + } + + onClicked: { + if (zoomed) { + var scale = pdfCanvas.width / width + zoomOutContentYAnimation.to = Math.max(0, Math.min(contentHeight - height, + (contentY + height/2) / scale - height/2)) + zoomOutAnimation.start() + } + } + + function adjust() { + var oldWidth = pdfCanvas.width + var oldHeight = pdfCanvas.height + var oldContentX = contentX + var oldContentY = contentY + + pdfCanvas.width = zoomed ? clamp(pdfCanvas.width) : width + + contentX = oldContentX * pdfCanvas.width / oldWidth + if (!contextHook.active) { + contentY = oldContentY * pdfCanvas.height / oldHeight + } + } + + function moveToSearchMatch(index) { + if (index < 0 || index >= searchDisplay.count) return + + _searchIndex = index + + var match = searchDisplay.itemAt(index) + var cX = match.x + match.width / 2. - width / 2. + cX = Math.max(0, Math.min(cX, pdfCanvas.width - width)) + var cY = match.y + match.height / 2. - height / 2. + cY = Math.max(0, Math.min(cY, pdfCanvas.height - height)) + + scrollTo(Qt.point(cX, cY), match.page, match) + } + + function nextSearchMatch() { + if (_searchIndex + 1 >= searchDisplay.count) { + moveToSearchMatch(0) + } else { + moveToSearchMatch(_searchIndex + 1) + } + } + + function prevSearchMatch() { + if (_searchIndex < 1) { + moveToSearchMatch(searchDisplay.count - 1) + } else { + moveToSearchMatch(_searchIndex - 1) + } + } + + function scrollTo(pt, pageId, focusItem) { + if ((pt.y < root.contentY + root.height && pt.y > root.contentY - root.height) + && (pt.x < root.contentX + root.width && pt.x > root.contentX - root.width)) { + scrollX.to = pt.x + scrollY.to = pt.y + scrollAnimation.focusItem = (focusItem !== undefined) ? focusItem : null + scrollAnimation.start() + } else { + var deltaY = pt.y - root.contentY + if (deltaY < 0) { + deltaY = Math.max(deltaY / 2., -root.height / 2.) + } else { + deltaY = Math.min(deltaY / 2., root.height / 2.) + } + leaveX.to = (root.contentX + pt.x) / 2 + leaveY.to = root.contentY + deltaY + returnX.to = pt.x + returnY.from = pt.y - deltaY + returnY.to = pt.y + quickScrollAnimation.pageTo = pageId + quickScrollAnimation.focusItem = (focusItem !== undefined) ? focusItem : null + quickScrollAnimation.start() + } + } + + function moveBack() { + if (!canMoveBack) { + return + } + + scrollTo(Qt.point(_contentXAtGotoLink, _contentYAtGotoLink), _pageAtGotoLink) + + _pageAtLinkTarget = 0 + _contentXAtGotoLink = -1. + _contentYAtGotoLink = -1. + } + + pinchArea.enabled: false // TODO: remove duplicate + contentWidth: pdfCanvas.width + contentHeight: pdfCanvas.height + header.height + + SequentialAnimation { + id: focusAnimation + property Item targetItem + NumberAnimation { target: focusAnimation.targetItem; property: "scale"; duration: 200; to: 3.; easing.type: Easing.InOutCubic } + NumberAnimation { target: focusAnimation.targetItem; property: "scale"; duration: 200; to: 1.; easing.type: Easing.InOutCubic } + } + SequentialAnimation { + id: scrollAnimation + property Item focusItem + ParallelAnimation { + NumberAnimation { id: scrollX; target: root; property: "contentX"; duration: 300; easing.type: Easing.InOutQuad } + NumberAnimation { id: scrollY; target: root; property: "contentY"; duration: 300; easing.type: Easing.InOutQuad } + } + ScriptAction { + script: { + if (scrollAnimation.focusItem) { + focusAnimation.targetItem = scrollAnimation.focusItem + focusAnimation.start() + } + } + } + } + SequentialAnimation { + id: quickScrollAnimation + property int pageTo + property Item focusItem + ParallelAnimation { + NumberAnimation { id: leaveX; target: root; property: "contentX"; duration: 300; easing.type: Easing.InQuad } + NumberAnimation { id: leaveY; target: root; property: "contentY"; duration: 300; easing.type: Easing.InQuad } + NumberAnimation { target: root; property: "opacity"; duration: 300; to: 0.; easing.type: Easing.InQuad } + } + PauseAnimation { duration: 100 } + ParallelAnimation { + NumberAnimation { id: returnX; target: root; property: "contentX"; duration: 300; easing.type: Easing.OutQuad } + NumberAnimation { id: returnY; target: root; property: "contentY"; duration: 300; easing.type: Easing.OutQuad } + NumberAnimation { target: root; property: "opacity"; duration: 300; to: 1.; easing.type: Easing.OutQuad } + } + ScriptAction { + script: if (quickScrollAnimation.focusItem) { + focusAnimation.targetItem = quickScrollAnimation.focusItem + focusAnimation.start() + } + } + } + NumberAnimation { + id: selectionOffset + property real start + duration: 200 + easing.type: Easing.InOutCubic + target: root + property: "contentY" + } + + // Ensure proper zooming level when device is rotated. + onWidthChanged: adjust() + Component.onCompleted: { + // Avoid hard dependency to feedback + _feedbackEffect = Qt.createQmlObject("import QtQuick 2.0; import QtFeedback 5.0; ThemeEffect { effect: ThemeEffect.PressWeak }", + root, 'ThemeEffect') + if (_feedbackEffect && !_feedbackEffect.supported) { + _feedbackEffect = null + } + } + + Connections { + target: document + onSearchModelChanged: moveToFirstMatch.done = false + } + + Connections { + id: moveToFirstMatch + property bool done + target: document.searchModel + onCountChanged: { + if (done) return + + moveToSearchMatch(0) + done = true + } + } + + PDF.Selection { + id: pdfSelection + + property bool dragging: drag1.pressed || drag2.pressed + property bool selected: count > 0 + + canvas: pdfCanvas + wiggle: Theme.itemSizeSmall / 2 + + onDraggingChanged: { + if (dragging) { + if (!selectionOffset.running) + selectionOffset.start = root.contentY + + // Limit offset when being at the bottom of the view. + selectionOffset.to = selectionOffset.start + + Math.min(Theme.itemSizeSmall, + Math.max(0, root.itemHeight - root.height - pdfCanvas.y - root.contentY)) + // Limit offset when being at the top of screen + selectionOffset.to = + Math.max(root.contentY, + Math.min(selectionOffset.to, + (drag1.pressed ? handle1.y : handle2.y) + - Theme.itemSizeSmall / 2) + ) + } else { + selectionOffset.to = selectionOffset.start + } + selectionOffset.restart() + + // Copy selection to clipboard when dragging finishes + if (!dragging) Clipboard.text = text + } + // Copy selection to clipboard on first selection + onSelectedChanged: if (selected) Clipboard.text = text + } + + PDF.Canvas { + id: pdfCanvas + + property bool _pageSizesReady + + objectName: "application" + + y: header.height + width: root.width + spacing: Theme.paddingLarge + flickable: root + linkWiggle: Theme.itemSizeMedium / 2 + linkColor: Theme.highlightColor + pagePlaceholderColor: "white" + + onPageLayoutChanged: { + if (!_pageSizesReady) { + _pageSizesReady = true + root.pageSizesReady() + } + } + + onCurrentPageChanged: { + // If the document is moved than more than one page + // the back move is cancelled. + if (_pageAtLinkTarget > 0 + && !scrollAnimation.running + && !quickScrollAnimation.running + && (currentPage > _pageAtLinkTarget + 1 + || currentPage < _pageAtLinkTarget - 1)) { + _pageAtLinkTarget = 0 + _contentXAtGotoLink = -1. + _contentYAtGotoLink = -1. + } + } + + PinchArea { + anchors.fill: parent + enabled: !pageStack.dragInProgress + onPinchUpdated: { + var newCenter = mapToItem(pdfCanvas, pinch.center.x, pinch.center.y) + root.zoom(1.0 + (pinch.scale - pinch.previousScale), newCenter) + } + onPinchFinished: root.returnToBounds() + + PDF.LinkArea { + id: linkArea + anchors.fill: parent + onClickedBoxChanged: { + if (clickedBox.width > 0) { + contextHook.setTarget(clickedBox.y, clickedBox.height) + } + } + + canvas: pdfCanvas + selection: pdfSelection + + onLinkClicked: root.linkClicked(linkTarget, contextHook) + onGotoClicked: { + var pt = root.contentAt(page - 1, top, left, + Theme.paddingLarge, Theme.paddingLarge) + _pageAtLinkTarget = page + _pageAtGotoLink = pdfCanvas.currentPage + _contentXAtGotoLink = root.contentX + _contentYAtGotoLink = root.contentY + scrollTo(pt, page) + } + onSelectionClicked: root.selectionClicked(selection, contextHook) + onAnnotationClicked: root.annotationClicked(annotation, contextHook) + onClicked: root.clicked() + onAnnotationLongPress: root.annotationLongPress(annotation, contextHook) + onLongPress: { + contextHook.setTarget(pressAt.y, Theme.itemSizeSmall / 2) + root.longPress(pressAt, contextHook) + } + } + } + + Rectangle { + x: linkArea.clickedBox.x + y: linkArea.clickedBox.y + width: linkArea.clickedBox.width + height: linkArea.clickedBox.height + radius: Theme.paddingSmall + color: Theme.highlightColor + opacity: linkArea.pressed ? 0.75 : 0. + visible: opacity > 0. + Behavior on opacity { FadeAnimator { duration: 100 } } + } + + Repeater { + id: searchDisplay + + model: pdfCanvas.document.searchModel + + delegate: Rectangle { + property int page: model.page + property rect pageRect: model.rect + property rect match: pdfCanvas.fromPageToItem(page, pageRect) + Connections { + target: pdfCanvas + onPageLayoutChanged: match = pdfCanvas.fromPageToItem(page, pageRect) + } + + opacity: 0.5 + color: Theme.highlightColor + x: match.x - Theme.paddingSmall / 2 + y: match.y - Theme.paddingSmall / 4 + width: match.width + Theme.paddingSmall + height: match.height + Theme.paddingSmall / 2 + } + } + + PDFSelectionView { + id: selectionView + model: pdfSelection + flickable: root + dragHandle1: drag1.pressed + dragHandle2: drag2.pressed + onVisibleChanged: if (visible && _feedbackEffect) _feedbackEffect.play() + } + PDFSelectionDrag { + id: drag1 + visible: pdfSelection.selected && selectionView.draggable + flickable: root + handle: pdfSelection.handle1 + onDragged: pdfSelection.handle1 = at + } + PDFSelectionDrag { + id: drag2 + visible: pdfSelection.selected && selectionView.draggable + flickable: root + handle: pdfSelection.handle2 + onDragged: pdfSelection.handle2 = at + } + ContextMenuHook { + id: contextHook + Connections { + target: linkArea + onPositionChanged: { + if (contextHook.active) { + var local = linkArea.mapToItem(contextHook, at.x, at.y) + contextHook.positionChanged(Qt.point(local.x, local.y)) + } + } + onReleased: if (contextHook.active) contextHook.released(true) + } + } + } + + children: [ + HorizontalScrollDecorator { color: Theme.highlightDimmerColor }, + VerticalScrollDecorator { color: Theme.highlightDimmerColor } + ] + + ConfigurationValue { + id: maxZoomLevelConfig + + key: "/apps/sailfish-office/settings/maxZoomLevel" + defaultValue: 10. + } + + function pageRectangle(pageNumber) { + var rect = pdfCanvas.pageRectangle( pageNumber ) + rect.y = rect.y + pdfCanvas.y + return rect + } + + function contentAt(pageNumber, top, left, topSpacing, leftSpacing) { + var rect = pageRectangle( pageNumber ) + + var scrollX, scrollY + // Adjust horizontal position if required. + scrollX = root.contentX + if (left !== undefined && left >= 0.) { + scrollX = rect.x + left * rect.width - ( leftSpacing !== undefined ? leftSpacing : 0.) + } + if (scrollX > contentWidth - width) { + scrollX = contentWidth - width + } + // Adjust vertical position. + scrollY = rect.y + (top === undefined ? 0. : top * rect.height) - ( topSpacing !== undefined ? topSpacing : 0.) + if (scrollY > contentHeight - height) { + scrollY = contentHeight - height + } + return Qt.point(Math.max(0, scrollX), Math.max(0, scrollY)) + } + function goToPage(pageNumber, top, left, topSpacing, leftSpacing) { + var pt = contentAt(pageNumber, top, left, topSpacing, leftSpacing) + contentX = pt.x + contentY = pt.y + } + // This function is the inverse of goToPage(), returning (pageNumber, top, left). + function getPagePosition() { + // Find the page on top + var i = currentPage - 1 + var rect = pageRectangle( i ) + while (rect.y > contentY && i > 0) { + rect = pageRectangle( --i ) + } + var top = (contentY - rect.y) / rect.height + var left = (contentX - rect.x) / rect.width + return [i, top, left] + } + function getPositionAt(at) { + // Find the page that contains at + var i = Math.max(0, currentPage - 2) + var rect = pageRectangle( i ) + while ((rect.y + rect.height) < at.y + && i < pdfCanvas.document.pageCount) { + rect = pageRectangle( ++i ) + } + var top = Math.max(0, at.y - rect.y) / rect.height + var left = (at.x - rect.x) / rect.width + return [i, top, left] + } + + ParallelAnimation { + id: zoomOutAnimation + + NumberAnimation { + target: pdfCanvas + property: "width" + to: root.width + easing.type: Easing.InOutQuad + duration: 200 + } + NumberAnimation { + target: root + properties: "contentX" + to: 0 + easing.type: Easing.InOutQuad + duration: 200 + } + NumberAnimation { + id: zoomOutContentYAnimation + target: root + properties: "contentY" + easing.type: Easing.InOutQuad + duration: 200 + } + } +} diff --git a/usr/lib/qt5/qml/Sailfish/Office/PlainTextDocumentPage.qml b/usr/lib/qt5/qml/Sailfish/Office/PlainTextDocumentPage.qml new file mode 100644 index 00000000..5cc1b965 --- /dev/null +++ b/usr/lib/qt5/qml/Sailfish/Office/PlainTextDocumentPage.qml @@ -0,0 +1,189 @@ +/* + * Copyright (C) 2013-2014 Jolla Ltd. + * Contact: Robin Burchell + * + * 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; version 2 only. + * + * 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. + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Sailfish.Office 1.0 +import Sailfish.TextLinking 1.0 + +DocumentPage { + id: documentPage + + property real fontSize: Theme.fontSizeMedium + property color linkColor: Theme.highlightFromColor(Theme.highlightColor, Theme.DarkOnLight) + property real maximumWidth + property bool wrap: true + + icon: "image://theme/icon-m-file-formatted" + busy: documentModel.status === PlainTextModel.Loading && documentModel.count === 0 + onStatusChanged: { + //Delay loading the document until the page has been activated. + if (status === PageStatus.Active) { + documentModel.source = documentPage.source + } + } + + preview: documentModel.status === PlainTextModel.Ready && documentModel.lineCount > 0 + ? previewComponent + : placeholderPreview + + Component { + id: previewComponent + + Rectangle { + color: "white" + + ListView { + id: previewView + + Component.onCompleted: positionViewAtIndex(Math.max(0, documentView.indexAt(0, documentView.contentY)), ListView.Beginning) + + anchors.fill: parent + model: documentModel + delegate: Rectangle { + width: previewView.width + height: previewLine.y + previewLine.height + + color: "white" + + Text { + id: previewLine + + x: Theme.paddingLarge + y: index === 0 ? Theme.paddingLarge : 0 + + width: previewView.width - (2 * x) + height: index === previewView.count - 1 + ? implicitHeight + Theme.paddingSmall + : implicitHeight + + color: Theme.darkPrimaryColor + linkColor: documentPage.linkColor + font.pixelSize: Theme.fontSizeTiny + + text: lineText + } + } + } + } + } + + LinkHandler { + id: linkHandler + } + + Flickable { + id: horizontalFlickable + + width: documentPage.width + height: documentPage.height - toolbar.offset + + boundsBehavior: Flickable.StopAtBounds + + contentWidth: documentPage.wrap + ? width + : Math.max(width, documentPage.maximumWidth + (2 * Theme.horizontalPageMargin)) + + flickableDirection: Flickable.HorizontalFlick + + clip: toolbar.offset > 0 + + SilicaListView { + id: documentView + + width: horizontalFlickable.contentWidth + height: documentPage.height + + _quickScrollItem.rightMargin: horizontalFlickable.contentWidth - horizontalFlickable.width - horizontalFlickable.contentX + + model: PlainTextModel { + id: documentModel + } + + header: DocumentHeader { + x: horizontalFlickable.contentX + page: documentPage + width: documentPage.width + } + + delegate: Rectangle { + width: horizontalFlickable.contentWidth + height: line.implicitHeight + + (index == 0 ? Theme.paddingLarge : 0) + + (index == documentView.count - 1 ? Theme.paddingLarge : 0) + + Text { + id: line + x: Theme.horizontalPageMargin + y: index == 0 ? Theme.paddingLarge : 0 + width: parent.width - (2 * x) + wrapMode: documentPage.wrap ? Text.Wrap : Text.NoWrap + color: Theme.darkPrimaryColor + linkColor: documentPage.linkColor + font.pixelSize: Math.round(documentPage.fontSize) + text: lineText + textFormat: Text.StyledText + + onImplicitWidthChanged: { + if (implicitWidth > documentPage.maximumWidth) { + documentPage.maximumWidth = Math.ceil(implicitWidth) + } + } + + onLinkActivated: linkHandler.handleLink(link) + } + } + + ViewPlaceholder { + enabled: documentModel.lineCount === 0 + && (documentModel.status === PlainTextModel.Ready || documentModel.status === PlainTextModel.Error) + text: documentModel.status === PlainTextModel.Error + //% "Error loading text file" + ? qsTrId("sailfish-office-la-plain_text_error") + //% "Empty text file" + : qsTrId("sailfish-office-la-plain_text_empty") + } + + VerticalScrollDecorator { + color: Theme.highlightDimmerColor + anchors.rightMargin: horizontalFlickable.contentWidth - horizontalFlickable.width - horizontalFlickable.contentX + } + + } + HorizontalScrollDecorator { color: Theme.highlightDimmerColor } + } + + ToolBar { + id: toolbar + + y: horizontalFlickable.height + + flickable: documentView + enabled: !documentPage.busy + opacity: enabled ? 1.0 : 0.0 + Behavior on opacity { FadeAnimator { duration: 400 }} + + DeleteButton { + page: documentPage + } + + ShareButton { + page: documentPage + } + } +} diff --git a/usr/lib/qt5/qml/Sailfish/Office/PresentationDetailsPage.qml b/usr/lib/qt5/qml/Sailfish/Office/PresentationDetailsPage.qml new file mode 100644 index 00000000..3c949272 --- /dev/null +++ b/usr/lib/qt5/qml/Sailfish/Office/PresentationDetailsPage.qml @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2024 Damien Caliste + * Contact: Damien Caliste + * + * 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; version 2 only. + * + * 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. + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Sailfish.Office 1.0 + +DetailsPage { + DetailItem { + //: Slide count detail of the presentation + //% "Slides" + label: qsTrId("sailfish-office-la-slidecount") + value: document.indexCount + alignment: Qt.AlignLeft + } +} diff --git a/usr/lib/qt5/qml/Sailfish/Office/PresentationPage.qml b/usr/lib/qt5/qml/Sailfish/Office/PresentationPage.qml new file mode 100644 index 00000000..44de88e5 --- /dev/null +++ b/usr/lib/qt5/qml/Sailfish/Office/PresentationPage.qml @@ -0,0 +1,159 @@ +/* + * Copyright (c) 2013-2020 Jolla Ltd. + * Copyright (c) 2020 Open Mobile Platform LLC. + * + * 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; version 2 only. + * + * 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. + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Sailfish.Silica.private 1.0 +import org.kde.calligra 1.0 as Calligra + +CalligraDocumentPage { + id: page + + icon: "image://theme/icon-m-file-presentation" + backgroundColor: "black" + coverAlignment: Qt.AlignCenter + coverFillMode: Image.PreserveAspectFit + busyIndicator.y: Math.round(page.height/2 - busyIndicator.height/2) + + function currentIndex() { + return view.currentIndex >= 0 ? view.currentIndex : document.currentIndex + } + + contents.thumbnailSize { + width: page.width + height: page.width * 0.75 + } + + SlideshowView { + id: view + + property bool contentAvailable: !page.busy + + anchors.fill: parent + orientation: Qt.Vertical + currentIndex: page.document.currentIndex + + enabled: !page.busy + opacity: enabled ? 1.0 : 0.0 + Behavior on opacity { FadeAnimator { duration: 400 }} + + model: page.contents + + delegate: ZoomableFlickable { + id: flickable + + readonly property bool active: PathView.isCurrentItem || viewMoving + onActiveChanged: { + if (!active) { + resetZoom() + largeThumb.data = page.contents.thumbnail(-1, 0) + } + } + + onZoomedChanged: overlay.active = !zoomed + onZoomFinished: if (largeThumb.implicitWidth === 0) largeThumb.data = page.contents.thumbnail(model.index, 3264) + + width: view.width + height: view.height + viewMoving: view.moving + scrollDecoratorColor: Theme.highlightDimmerFromColor(Theme.highlightDimmerColor, Theme.DarkOnLight) + implicitContentWidth: thumb.implicitWidth + implicitContentHeight: thumb.implicitHeight + + MouseArea { + anchors.fill: parent + onClicked: { + if (zoomed) { + zoomOut() + } else { + overlay.active = !overlay.active + } + } + } + + Calligra.ImageDataItem { + id: thumb + + property bool initialized + property bool ready: initialized && !viewMoving + + Component.onCompleted: initialized = true + onReadyChanged: { + if (ready) { + ready = true // remove binding + data = page.contents.thumbnail(model.index, Screen.height) + } + } + + anchors.fill: parent + } + Calligra.ImageDataItem { + id: largeThumb + visible: implicitWidth > 0 + anchors.fill: parent + } + } + } + + Item { + id: overlay + property bool active: true + + enabled: active + anchors.fill: parent + opacity: enabled ? 1.0 : 0.0 + Behavior on opacity { FadeAnimator {}} + + FadeGradient { + topDown: true + width: parent.width + height: header.height + Theme.paddingLarge + } + + DocumentHeader { + id: header + detailsPage: "PresentationDetailsPage.qml" + color: Theme.lightPrimaryColor + page: page + } + + OverlayToolbar { + enabled: page.document.status == Calligra.DocumentStatus.Loaded + opacity: enabled ? 1.0 : 0.0 + Behavior on opacity { FadeAnimator { duration: 400 }} + + DeleteButton { + page: page + icon.color: Theme.lightPrimaryColor + } + + ShareButton { + page: page + icon.color: Theme.lightPrimaryColor + } + + IndexButton { + onClicked: pageStack.animatorPush(Qt.resolvedUrl("PresentationThumbnailPage.qml"), { document: page.document }) + + index: Math.max(1, view.currentIndex + 1) + count: page.document.indexCount + color: Theme.lightPrimaryColor + } + } + } +} diff --git a/usr/lib/qt5/qml/Sailfish/Office/PresentationThumbnailPage.qml b/usr/lib/qt5/qml/Sailfish/Office/PresentationThumbnailPage.qml new file mode 100644 index 00000000..41e5bf21 --- /dev/null +++ b/usr/lib/qt5/qml/Sailfish/Office/PresentationThumbnailPage.qml @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2013-2014 Jolla Ltd. + * Contact: Robin Burchell + * + * 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; version 2 only. + * + * 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. + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import org.kde.calligra 1.0 as Calligra + +Page { + id: page + + property QtObject document + + allowedOrientations: Orientation.All + + SilicaGridView { + id: grid + + anchors.fill: parent + + cellWidth: page.width / 3 + cellHeight: cellWidth * 0.75 + + currentIndex: page.document.currentIndex + + //: Page with slide overview + //% "Slides" + header: PageHeader { title: qsTrId("sailfish-office-he-slide_index") } + + model: Calligra.ContentsModel { + document: page.document + thumbnailSize.width: grid.cellWidth + thumbnailSize.height: grid.cellHeight + } + + delegate: Item { + id: root + width: GridView.view.cellWidth + height: GridView.view.cellHeight + + Rectangle { + anchors.fill: parent + border.width: 1 + + Calligra.ImageDataItem { + anchors.fill: parent + data: model.thumbnail + } + + Rectangle { + anchors.centerIn: parent + width: label.width + Theme.paddingMedium + height: label.height + radius: Theme.paddingSmall + color: root.GridView.isCurrentItem ? Theme.highlightColor : Theme.darkPrimaryColor + } + + Label { + id: label + anchors.centerIn: parent + text: model.contentIndex + 1 + color: Theme.lightPrimaryColor + } + + Rectangle { + anchors.fill: parent + color: mouseArea.pressed && mouseArea.containsMouse ? Theme.highlightBackgroundColor : "transparent" + opacity: Theme.highlightBackgroundOpacity + } + + } + + MouseArea { + id: mouseArea + anchors.fill: parent + onClicked: { + page.document.currentIndex = model.contentIndex + pageStack.navigateBack(PageStackAction.Animated) + } + } + } + } +} diff --git a/usr/lib/qt5/qml/Sailfish/Office/SearchBarItem.qml b/usr/lib/qt5/qml/Sailfish/Office/SearchBarItem.qml new file mode 100644 index 00000000..0f3b6f15 --- /dev/null +++ b/usr/lib/qt5/qml/Sailfish/Office/SearchBarItem.qml @@ -0,0 +1,265 @@ +/**************************************************************************************** +** +** Copyright (C) 2013-2016 Jolla Ltd., Damien Caliste +** Contact: Raine Makelainen +** Contact: Damien Caliste +** All rights reserved. +** +** This file is part of Sailfish Office package and is a modified +** version of SearchField.qml from Sailfish Silica package to add +** next and previous button, modify the clear button action to support +** cancellation and also introduce a new iconized state. +** +** You may use this file under the terms of BSD license as follows: +** +** Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in the +** documentation and/or other materials provided with the distribution. +** * Neither the name of the Jolla Ltd nor the +** names of its contributors may be used to endorse or promote products +** derived from this software without specific prior written permission. +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +** ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +** WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +** DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR +** ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +** (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +** LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +** ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +** SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +** +****************************************************************************************/ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 + +Item { + id: root + + property bool active + property int matchCount: -1 + property real expandedWidth + property bool searching + property alias searchProgress: progressBar.progress + property alias highlighted: searchIcon.highlighted + + property real _margin: Math.max((width - searchIcon.width) / 2., 0.) + + signal clicked() + signal requestSearch(string text) + signal requestPreviousMatch() + signal requestNextMatch() + signal requestCancel() + + onActiveChanged: if (active) searchField.forceActiveFocus() + + states: State { + name: "expanded" + when: root.active + PropertyChanges { + target: root + _margin: Theme.horizontalPageMargin + width: expandedWidth + } + } + transitions: Transition { + NumberAnimation { + properties: "_margin, width" + easing.type: Easing.InOutQuad + duration: 400 + } + } + + Rectangle { + id: progressBar + property real progress: 0.0 + height: parent.height + width: progress * parent.width + gradient: Gradient { + GradientStop { position: 0.0; color: Theme.rgba(Theme.highlightBackgroundColor, 0.5) } + GradientStop { position: 1.0; color: Theme.rgba(Theme.highlightBackgroundColor, 0.0) } + } + opacity: searching ? 1. : 0. + visible: opacity > 0. + + Behavior on width { + enabled: progressBar.visible + SmoothedAnimation { velocity: 480; duration: 200 } + } + Behavior on opacity { FadeAnimation {} } + } + + IconButton { + id: searchIcon + anchors { + left: parent.left + leftMargin: _margin + } + width: icon.width + height: parent.height + icon.source: "image://theme/icon-m-search" + highlighted: down || searchField.activeFocus + + onClicked: { + root.active = true + root.clicked() + } + } + + TextField { + id: searchField + + property string _searchText + + height: Math.max(Theme.itemSizeMedium, _editor.height + Theme.paddingMedium + Theme.paddingSmall) + + anchors { + verticalCenter: parent.verticalCenter + left: searchIcon.right + right: parent.right + rightMargin: Theme.horizontalPageMargin + } + + focusOutBehavior: FocusBehavior.ClearPageFocus + font { + // visible label doesn't leave much room. match count might go away if heavy full-document search is replaced + // with more incremental approach, so should be good for now + pixelSize: labelVisible ? Theme.fontSizeMediumBase : Theme.fontSizeLarge + family: Theme.fontFamilyHeading + } + + textRightMargin: clearButton.width + + (searchPrev.visible ? searchPrev.width + Theme.paddingLarge : 0.) + + (searchNext.visible ? searchNext.width + Theme.paddingLarge : 0.) + textTopMargin: labelVisible ? Theme.paddingSmall : (height/2 - _editor.implicitHeight/2) + + labelVisible: root.matchCount > 0 && !searchField.activeFocus + //% "%n item(s) found" + label: qsTrId("sailfish-office-lb-%n-matches", root.matchCount) + + placeholderText: (root.matchCount == 0 && !activeFocus) + //% "No result" + ? qsTrId("sailfish-office-search-no-result") + //% "Search on document" + : qsTrId("sailfish-office-search-document") + + Connections { + target: root + onSearchingChanged: { + if (!searching && matchCount == 0) { + searchField.text = "" // Allow the placeholder + } + } + } + on_SearchTextChanged: root.requestSearch(_searchText) + + onActiveFocusChanged: { + if (activeFocus) { + text = _searchText + cursorPosition = text.length + } else { + if (!text) { + root.active = false + } else if (matchCount == 0 && !searching) { + text = "" + } + } + } + + inputMethodHints: Qt.ImhNoAutoUppercase | Qt.ImhPreferLowercase | Qt.ImhNoPredictiveText + EnterKey.iconSource: text != "" ? "image://theme/icon-m-enter-accept" + : "image://theme/icon-m-enter-close" + EnterKey.onClicked: { + if (text != "") { + _searchText = text + } + focus = false + } + + background: null + + visible: root.active && opacity > 0. + + opacity: root._margin == Theme.horizontalPageMargin ? 1. : 0. + Behavior on opacity { FadeAnimation {} } + + Item { + parent: searchField + anchors.right: parent.right + + height: parent.height + + IconButton { + id: searchPrev + anchors { + right: searchNext.left + rightMargin: Theme.paddingLarge + } + width: icon.width + height: parent.height + icon.source: "image://theme/icon-m-left" + + visible: opacity > 0. + + opacity: root.matchCount > 0 && !searchField.activeFocus ? 1 : 0 + Behavior on opacity { FadeAnimation {} } + + onClicked: root.requestPreviousMatch() + } + + IconButton { + id: searchNext + anchors { + right: clearButton.left + rightMargin: Theme.paddingLarge + } + width: icon.width + height: parent.height + icon.source: "image://theme/icon-m-right" + + visible: opacity > 0. + + opacity: root.matchCount > 0 && !searchField.activeFocus ? 1 : 0 + Behavior on opacity { FadeAnimation {} } + + onClicked: root.requestNextMatch() + } + + IconButton { + id: clearButton + anchors.right: parent.right + + width: icon.width + height: parent.height + icon.source: "image://theme/icon-m-clear" + + onClicked: { + var _searching = root.searching + + // Cancel any pending search. + root.requestCancel() + + // Cancel case, nothing to do further. + if (_searching) return + + searchField._searchText = "" + if (!searchField.activeFocus || searchField.text == "") { + // Close case. + root.active = false + searchField.focus = false + } else { + // Clear case. + searchField.text = "" + searchField.forceActiveFocus() + } + } + } + } + } +} diff --git a/usr/lib/qt5/qml/Sailfish/Office/ShareButton.qml b/usr/lib/qt5/qml/Sailfish/Office/ShareButton.qml new file mode 100644 index 00000000..30152f54 --- /dev/null +++ b/usr/lib/qt5/qml/Sailfish/Office/ShareButton.qml @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2019 Jolla Ltd. + * Copyright (C) 2021 Open Mobile Platform LLC. + * + * 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; version 2 only. + * + * 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. + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Sailfish.Share 1.0 + +IconButton { + property DocumentPage page + icon.source: "image://theme/icon-m-share" + visible: page.source != "" && !page.error + anchors.verticalCenter: parent.verticalCenter + onClicked: { + shareAction.trigger() + } + + ShareAction { + id: shareAction + + resources: [page.source] + mimeType: page.mimeType + } +} diff --git a/usr/lib/qt5/qml/Sailfish/Office/SpreadsheetDetailsPage.qml b/usr/lib/qt5/qml/Sailfish/Office/SpreadsheetDetailsPage.qml new file mode 100644 index 00000000..75cf7efd --- /dev/null +++ b/usr/lib/qt5/qml/Sailfish/Office/SpreadsheetDetailsPage.qml @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2024 Damien Caliste + * Contact: Damien Caliste + * + * 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; version 2 only. + * + * 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. + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Sailfish.Office 1.0 + +DetailsPage { + DetailItem { + //: Sheet count of the spreadsheet + //% "Sheets" + label: qsTrId("sailfish-office-la-sheetcount") + value: document.indexCount + alignment: Qt.AlignLeft + } +} diff --git a/usr/lib/qt5/qml/Sailfish/Office/SpreadsheetListPage.qml b/usr/lib/qt5/qml/Sailfish/Office/SpreadsheetListPage.qml new file mode 100644 index 00000000..90fcc2e7 --- /dev/null +++ b/usr/lib/qt5/qml/Sailfish/Office/SpreadsheetListPage.qml @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2013-2014 Jolla Ltd. + * Contact: Robin Burchell + * + * 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; version 2 only. + * + * 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. + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import org.kde.calligra 1.0 as Calligra + +Page { + id: page + + property QtObject document + + allowedOrientations: Orientation.All + + SilicaListView { + id: view + + anchors.fill: parent + + //: Page with sheet selector + //% "Sheets" + header: PageHeader { title: qsTrId("sailfish-office-he-sheet_index") } + + model: Calligra.ContentsModel { + document: page.document + thumbnailSize.width: Theme.itemSizeLarge + thumbnailSize.height: Theme.itemSizeLarge + } + + delegate: BackgroundItem { + Calligra.ImageDataItem { + id: thumbnail + + anchors { + left: parent.left + verticalCenter: parent.verticalCenter + } + + width: parent.height + height: parent.height + + data: model.thumbnail + } + + Label { + anchors { + left: thumbnail.right + leftMargin: Theme.paddingLarge + verticalCenter: parent.verticalCenter + } + + text: model.title + color: (model.contentIndex == page.document.currentIndex || highlighted) ? Theme.highlightColor + : Theme.primaryColor + truncationMode: TruncationMode.Fade + } + + onClicked: { + page.document.currentIndex = model.contentIndex + pageStack.navigateBack() + } + } + } +} diff --git a/usr/lib/qt5/qml/Sailfish/Office/SpreadsheetPage.qml b/usr/lib/qt5/qml/Sailfish/Office/SpreadsheetPage.qml new file mode 100644 index 00000000..c0061ee5 --- /dev/null +++ b/usr/lib/qt5/qml/Sailfish/Office/SpreadsheetPage.qml @@ -0,0 +1,137 @@ +/* + * Copyright (c) 2013 - 2019 Jolla Ltd. + * Copyright (c) 2019 Open Mobile Platform LLC. + * + * Contact: Robin Burchell + * + * 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; version 2 only. + * + * 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. + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Sailfish.Silica.private 1.0 +import org.kde.calligra 1.0 as Calligra + +CalligraDocumentPage { + id: page + + onStatusChanged: { + //Reset the position when we change sheets + if (status === PageStatus.Activating) { + flickable.contentX = 0 + flickable.contentY = 0 + } + } + + icon: "image://theme/icon-m-file-spreadsheet" + backgroundColor: "white" + + document.onStatusChanged: { + if (document.status === Calligra.DocumentStatus.Loaded) { + viewController.zoomToFitWidth(page.width) + } + } + + Calligra.View { + id: documentView + + property bool contentAvailable: !page.busy + + anchors.fill: parent + document: page.document + } + + ControllerFlickable { + id: flickable + + onZoomedChanged: overlay.active = !zoomed + + controller: viewController + anchors.fill: parent + enabled: !page.busy + opacity: enabled ? 1.0 : 0.0 + Behavior on opacity { FadeAnimator { duration: 400 }} + + Calligra.ViewController { + id: viewController + view: documentView + flickable: flickable + useZoomProxy: false + maximumZoom: Math.max(10.0, 2.0 * minimumZoom) + minimumZoomFitsWidth: true + } + + Calligra.LinkArea { + anchors.fill: parent + document: page.document + onClicked: { + if (flickable.zoomed) { + flickable.zoomOut() + } else { + overlay.active = !overlay.active + } + } + onLinkClicked: Qt.openUrlExternally(linkTarget) + controllerZoom: viewController.zoom + } + } + + Item { + id: overlay + property bool active: true + + enabled: active + anchors.fill: parent + opacity: enabled ? 1.0 : 0.0 + Behavior on opacity { FadeAnimator {}} + + FadeGradient { + topDown: true + width: parent.width + height: header.height + Theme.paddingLarge + color: page.backgroundColor + } + + DocumentHeader { + id: header + detailsPage: "SpreadsheetDetailsPage.qml" + color: Theme.darkPrimaryColor + page: page + } + + OverlayToolbar { + enabled: page.document.status === Calligra.DocumentStatus.Loaded + opacity: enabled ? 1.0 : 0.0 + color: page.backgroundColor + Behavior on opacity { FadeAnimator { duration: 400 }} + + DeleteButton { + page: page + icon.color: Theme.darkPrimaryColor + } + + ShareButton { + page: page + icon.color: Theme.darkPrimaryColor + } + + IndexButton { + onClicked: pageStack.animatorPush(Qt.resolvedUrl("SpreadsheetListPage.qml"), { document: page.document }) + index: Math.max(1, page.document.currentIndex + 1) + count: page.document.indexCount + color: Theme.darkPrimaryColor + } + } + } +} diff --git a/usr/lib/qt5/qml/Sailfish/Office/TextDetailsPage.qml b/usr/lib/qt5/qml/Sailfish/Office/TextDetailsPage.qml new file mode 100644 index 00000000..0b13628c --- /dev/null +++ b/usr/lib/qt5/qml/Sailfish/Office/TextDetailsPage.qml @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2024 Damien Caliste + * Contact: Damien Caliste + * + * 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; version 2 only. + * + * 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. + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Sailfish.Office 1.0 + +DetailsPage { + DetailItem { + //: Page count of the text document + //% "Page Count" + label: qsTrId("sailfish-office-la-pagecount") + value: document.indexCount + alignment: Qt.AlignLeft + } +} diff --git a/usr/lib/qt5/qml/Sailfish/Office/TextDocumentPage.qml b/usr/lib/qt5/qml/Sailfish/Office/TextDocumentPage.qml new file mode 100644 index 00000000..5bffe6d2 --- /dev/null +++ b/usr/lib/qt5/qml/Sailfish/Office/TextDocumentPage.qml @@ -0,0 +1,131 @@ +/* + * Copyright (c) 2013 - 2019 Jolla Ltd. + * Copyright (c) 2019 Open Mobile Platform LLC. + * + * Contact: Robin Burchell + * + * 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; version 2 only. + * + * 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. + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Sailfish.Silica.private 1.0 +import org.kde.calligra 1.0 as Calligra + +CalligraDocumentPage { + id: page + + icon: "image://theme/icon-m-file-formatted" + + function currentIndex() { + // Text document indexes appear to start at 1, model indexes at the traditional 0. + return document.currentIndex - 1 + } + + document.onStatusChanged: { + if (document.status === Calligra.DocumentStatus.Loaded) { + viewController.zoomToFitWidth(page.width) + } + } + + Calligra.View { + id: documentView + + property bool contentAvailable: !page.busy + + anchors.fill: flickable + opacity: page.busy ? 0.0 : 1.0 + Behavior on opacity { FadeAnimator { duration: 400 }} + document: page.document + } + + ControllerFlickable { + id: flickable + + property bool resetPositionWorkaround + + onContentYChanged: { + if (page.document.status == Calligra.DocumentStatus.Loaded + && !resetPositionWorkaround) { + // Calligra is not Flickable.topMargin aware + contentY = -topMargin + contentX = 0 + viewController.useZoomProxy = false + resetPositionWorkaround = true + } + } + + controller: viewController + topMargin: header.height + clip: anchors.bottomMargin > 0 + anchors { + fill: parent + bottomMargin: toolbar.offset + } + + + Calligra.ViewController { + id: viewController + view: documentView + flickable: flickable + maximumZoom: Math.max(5.0, 2.0 * minimumZoom) + minimumZoomFitsWidth: true + } + + Calligra.LinkArea { + anchors.fill: parent + document: page.document + onLinkClicked: Qt.openUrlExternally(linkTarget) + onClicked: flickable.zoomOut() + + controllerZoom: viewController.zoom + } + + DocumentHeader { + id: header + detailsPage: "TextDetailsPage.qml" + page: page + width: page.width + x: flickable.contentX + y: -height + } + } + + ToolBar { + id: toolbar + + flickable: flickable + anchors.top: flickable.bottom + forceHidden: page.document.status === Calligra.DocumentStatus.Failed + enabled: page.document.status === Calligra.DocumentStatus.Loaded + opacity: enabled ? 1.0 : 0.0 + Behavior on opacity { FadeAnimator { duration: 400 }} + + DeleteButton { + page: page + } + + ShareButton { + page: page + } + + IndexButton { + onClicked: pageStack.animatorPush(Qt.resolvedUrl("TextDocumentToCPage.qml"), { document: page.document, contents: page.contents }) + + index: Math.max(1, page.document.currentIndex) + count: page.document.indexCount + allowed: page.document.status !== Calligra.DocumentStatus.Failed + } + } +} diff --git a/usr/lib/qt5/qml/Sailfish/Office/TextDocumentToCPage.qml b/usr/lib/qt5/qml/Sailfish/Office/TextDocumentToCPage.qml new file mode 100644 index 00000000..0ad90908 --- /dev/null +++ b/usr/lib/qt5/qml/Sailfish/Office/TextDocumentToCPage.qml @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2013-2014 Jolla Ltd. + * Contact: Robin Burchell + * + * 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; version 2 only. + * + * 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. + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import org.kde.calligra 1.0 as Calligra + +Page { + id: page + + property QtObject document + property alias contents: view.model + + allowedOrientations: Orientation.All + + SilicaListView { + id: view + anchors.fill: parent + + //: Page with Text document index + //% "Index" + header: PageHeader { title: qsTrId("sailfish-office-he-index") } + + delegate: BackgroundItem { + property bool isCurrentItem: model.contentIndex + 1 == page.document.currentIndex + Label { + x: Theme.horizontalPageMargin + width: parent.width - 2*x + anchors.verticalCenter: parent.verticalCenter + //% "Page %1" + text: qsTrId("sailfish_office-la-page_number").arg(model.contentIndex + 1) + color: highlighted || isCurrentItem ? Theme.highlightColor : Theme.primaryColor + truncationMode: TruncationMode.Fade + } + onClicked: { + page.document.currentIndex = model.contentIndex + pageStack.navigateBack(PageStackAction.Animated) + } + } + } +} diff --git a/usr/lib/qt5/qml/Sailfish/Office/ToolBar.qml b/usr/lib/qt5/qml/Sailfish/Office/ToolBar.qml new file mode 100644 index 00000000..a8ae25a5 --- /dev/null +++ b/usr/lib/qt5/qml/Sailfish/Office/ToolBar.qml @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2015 Caliste Damien. + * Contact: Damien Caliste + * + * 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; version 2 only. + * + * 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. + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 + +PanelBackground { + id: toolbar + + property Item flickable + property bool forceHidden + property bool autoShowHide: true + property int offset: _active && !forceHidden && !_pulleyActive ? height : 0 + + property bool _active: true + property int _previousContentY + readonly property bool _pulleyActive: flickable && flickable.pullDownMenu && flickable.pullDownMenu.active + default property alias _data: contentItem.data + + width: parent.width + height: isPortrait ? Theme.itemSizeLarge : Theme.itemSizeSmall + + function show() { + if (forceHidden) { + return + } + autoHideTimer.stop() + _active = true + if (autoShowHide) autoHideTimer.restart() + } + function hide() { + _active = false + autoHideTimer.stop() + } + + onAutoShowHideChanged: { + if (autoShowHide) { + if (_active) { + autoHideTimer.start() + } + } else { + autoHideTimer.stop() + // Keep a transiting (and a not transited yet) toolbar visible. + _active = _active || (offset > 0) + } + } + + onForceHiddenChanged: { + // Avoid showing back the toolbar when forceHidden becomes false again. + if (forceHidden && autoShowHide) { + _active = false + autoHideTimer.stop() + } + } + + Behavior on offset { NumberAnimation { duration: 400; easing.type: Easing.InOutQuad } } + + + Row { + id: contentItem + + spacing: Theme.paddingLarge + x: Math.max(0, parent.width/2 - width/2) + height: parent.height + } + + Connections { + target: flickable + onContentYChanged: { + if (!flickable.movingVertically) { + return + } + + if (autoShowHide) { + _active = flickable.contentY < _previousContentY + + if (_active) { + autoHideTimer.restart() + } + } + + _previousContentY = flickable.contentY + } + } + + Timer { + id: autoHideTimer + interval: 4000 + onTriggered: _active = false + } +} diff --git a/usr/lib/qt5/qml/Sailfish/Office/qmldir b/usr/lib/qt5/qml/Sailfish/Office/qmldir new file mode 100644 index 00000000..08c6c62c --- /dev/null +++ b/usr/lib/qt5/qml/Sailfish/Office/qmldir @@ -0,0 +1,7 @@ +module Sailfish.Office +PDFDocumentPage 1.0 PDFDocumentPage.qml +PresentationPage 1.0 PresentationPage.qml +SpreadsheetPage 1.0 SpreadsheetPage.qml +TextDocumentPage 1.0 TextDocumentPage.qml +PlainTextDocumentPage 1.0 PlainTextDocumentPage.qml +plugin sailfishofficeplugin diff --git a/usr/lib/qt5/qml/Sailfish/Pickers/private/qmldir b/usr/lib/qt5/qml/Sailfish/Pickers/private/qmldir index 8239f2d8..a57129af 100644 --- a/usr/lib/qt5/qml/Sailfish/Pickers/private/qmldir +++ b/usr/lib/qt5/qml/Sailfish/Pickers/private/qmldir @@ -1,5 +1,6 @@ module Sailfish.Pickers.private plugin sailfishcomponentspickersplugin +typeinfo plugins.qmltypes AvatarPickerPage 1.0 AvatarPickerPage.qml DocumentModel 1.0 DocumentModel.qml ImageModel 1.0 ImageModel.qml diff --git a/usr/lib/qt5/qml/Sailfish/Pickers/qmldir b/usr/lib/qt5/qml/Sailfish/Pickers/qmldir index 7a46ff45..a4161d70 100644 --- a/usr/lib/qt5/qml/Sailfish/Pickers/qmldir +++ b/usr/lib/qt5/qml/Sailfish/Pickers/qmldir @@ -1,5 +1,6 @@ module Sailfish.Pickers plugin sailfishcomponentspickersplugin +typeinfo plugins.qmltypes ContentPickerPage 1.0 ContentPickerPage.qml DocumentPickerPage 1.0 DocumentPickerPage.qml DownloadPickerPage 1.0 DownloadPickerPage.qml diff --git a/usr/lib/qt5/qml/Sailfish/Secrets/InteractionView.qml b/usr/lib/qt5/qml/Sailfish/Secrets/InteractionView.qml new file mode 100644 index 00000000..667038e1 --- /dev/null +++ b/usr/lib/qt5/qml/Sailfish/Secrets/InteractionView.qml @@ -0,0 +1,110 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Sailfish.Secrets 1.0 as Secrets + +/*! + \qmltype InteractionView + \brief Interface for implementing in-app authentication + \note A concrete implementation of InteractionView is provided + as \l {ApplicationInteractionView} + \inqmlmodule Sailfish.Secrets + */ + +// TODO: replace this with "actual UI" which allows user to confirm/deny or enter a custom password! +Item { + Rectangle { + id: deleteConfirmationItem + visible: adapter.interactionParameters.inputType == Secrets.InteractionParameters.ConfirmationInput + && adapter.interactionParameters.operation == Secrets.InteractionParameters.DeleteSecret + enabled: visible + anchors.fill: parent + color: "blue" + Text { + anchors.centerIn: parent + text: "PRESS ME TO DELETE/CONTINUE" + } + MouseArea { + enabled: parent.enabled + anchors.fill: parent + onClicked: adapter.confirmation = Secrets.ApplicationInteractionView.Allow + } + } + Rectangle { + id: modifyConfirmationItem + visible: adapter.interactionParameters.inputType == Secrets.InteractionParameters.ConfirmationInput + && adapter.interactionParameters.operation == Secrets.InteractionParameters.StoreSecret + enabled: visible + anchors.fill: parent + color: "green" + Text { + anchors.centerIn: parent + text: "PRESS ME TO MODIFY/CONTINUE" + } + MouseArea { + enabled: parent.enabled + anchors.fill: parent + onClicked: adapter.confirmation = Secrets.ApplicationInteractionView.Allow + } + } + Rectangle { + id: userVerificationConfirmationItem + visible: adapter.interactionParameters.inputType == Secrets.InteractionParameters.AuthenticationInput + enabled: visible + anchors.fill: parent + color: "yellow" + Text { + anchors.centerIn: parent + text: "PRESS ME TO AUTHENTICATE/CONTINUE" + } + MouseArea { + enabled: parent.enabled + anchors.fill: parent + onClicked: adapter.confirmation = Secrets.ApplicationInteractionView.Allow + } + } + Rectangle { + id: encryptionPasswordItem + visible: adapter.interactionParameters.inputType == Secrets.InteractionParameters.AlphaNumericInput + enabled: visible + anchors.fill: parent + color: "red" + Text { + anchors.centerIn: parent + text: "PRESS ME TO SUPPLY PASSWORD/CONTINUE" + } + MouseArea { + enabled: parent.enabled + anchors.fill: parent + onClicked: { + console.log("returning custom encryption password!") + adapter.password = "example custom password" + } + } + } + + Column { + y: Theme.paddingLarge + width: parent.width + spacing: Theme.paddingLarge + + Text { + width: parent.width + horizontalAlignment: Text.AlignHCenter + + text: adapter.interactionParameters.promptText.message + wrapMode: Text.Wrap + } + Text { + width: parent.width + horizontalAlignment: Text.AlignHCenter + text: adapter.interactionParameters.promptText.instruction + wrapMode: Text.Wrap + } + + Text { + width: parent.width + text: adapter.interactionParameters.promptText.newInstruction + wrapMode: Text.Wrap + } + } +} diff --git a/usr/lib/qt5/qml/Sailfish/Secrets/qmldir b/usr/lib/qt5/qml/Sailfish/Secrets/qmldir new file mode 100644 index 00000000..3791549d --- /dev/null +++ b/usr/lib/qt5/qml/Sailfish/Secrets/qmldir @@ -0,0 +1,4 @@ +module Sailfish.Secrets +plugin sailfishsecretsplugin +typeinfo plugins.qmltypes +InteractionView 1.0 InteractionView.qml diff --git a/usr/lib/qt5/qml/Sailfish/Settings/Networking/AddNetworkNotifications.qml b/usr/lib/qt5/qml/Sailfish/Settings/Networking/AddNetworkNotifications.qml index 650bf26f..64749e65 100644 --- a/usr/lib/qt5/qml/Sailfish/Settings/Networking/AddNetworkNotifications.qml +++ b/usr/lib/qt5/qml/Sailfish/Settings/Networking/AddNetworkNotifications.qml @@ -7,7 +7,7 @@ ****************************************************************************/ import QtQuick 2.0 -import MeeGo.Connman 0.2 +import Connman 0.2 import Nemo.Notifications 1.0 NetworkService { diff --git a/usr/share/jolla-settings/pages/wlan/AdvancedSettingsColumn.qml b/usr/lib/qt5/qml/Sailfish/Settings/Networking/AdvancedSettingsColumn.qml similarity index 91% rename from usr/share/jolla-settings/pages/wlan/AdvancedSettingsColumn.qml rename to usr/lib/qt5/qml/Sailfish/Settings/Networking/AdvancedSettingsColumn.qml index 8d2e12c6..505aa7bc 100644 --- a/usr/share/jolla-settings/pages/wlan/AdvancedSettingsColumn.qml +++ b/usr/lib/qt5/qml/Sailfish/Settings/Networking/AdvancedSettingsColumn.qml @@ -1,10 +1,9 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 -import MeeGo.Connman 0.2 +import Connman 0.2 import com.jolla.settings.system 1.0 import Sailfish.Policy 1.0 import Sailfish.Settings.Networking 1.0 -import "../netproxy" Column { id: root @@ -14,7 +13,9 @@ Column { property Item firstFocusableItem: proxyForm.currentIndex > 0 ? proxyForm.proxyLoader : !ipv4Switch.checked ? ipv4FormLoader : null - property alias globalProxyButtonVisible: globalProxyButton.visible + // A button linking to this page is made available if the proxy config + // is being overridden. If no value is set the button will be hidden + property url globalProxyConfigPage width: parent.width SectionHeader { @@ -50,12 +51,13 @@ Column { Button { id: globalProxyButton + visible: globalProxyConfigPage.toString().length enabled: parent.enabled && !disabledByMdmBanner.active anchors.horizontalCenter: parent.horizontalCenter //: Button which opens the advanced settings page containing the global proxy config //% "Configure global proxy" text: qsTrId("settings_network-bt-configure_global_proxy") - onClicked: pageStack.animatorPush(Qt.resolvedUrl("../advanced-networking/mainpage.qml")) + onClicked: pageStack.animatorPush(globalProxyConfigPage) } } diff --git a/usr/lib/qt5/qml/Sailfish/Settings/Networking/AnonymousIdentityField.qml b/usr/lib/qt5/qml/Sailfish/Settings/Networking/AnonymousIdentityField.qml index a3b0e7b8..e9c37ab9 100644 --- a/usr/lib/qt5/qml/Sailfish/Settings/Networking/AnonymousIdentityField.qml +++ b/usr/lib/qt5/qml/Sailfish/Settings/Networking/AnonymousIdentityField.qml @@ -1,6 +1,6 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 -import MeeGo.Connman 0.2 as Connman +import Connman 0.2 as Connman NetworkField { property QtObject network diff --git a/usr/lib/qt5/qml/Sailfish/Settings/Networking/AutoConfigProxyForm.qml b/usr/lib/qt5/qml/Sailfish/Settings/Networking/AutoConfigProxyForm.qml new file mode 100644 index 00000000..95a1e091 --- /dev/null +++ b/usr/lib/qt5/qml/Sailfish/Settings/Networking/AutoConfigProxyForm.qml @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2012 - 2022 Jolla Ltd. + * + * License: Proprietary + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Sailfish.Settings.Networking 1.0 + +ListItem { + property QtObject network + + contentHeight: Theme.itemSizeMedium + + function updateProxyConfig(url) { + var proxyConfig = network.proxyConfig + + proxyConfig["Method"] = "auto" + proxyConfig["URL"] = url + + network.proxyConfig = proxyConfig + } + + Connections { + target: network + ignoreUnknownSignals: true + onProxyChanged: { + if (!network.proxyConfig["URL"] && network.proxy && network.proxy["URL"]) { + urlField.text = network.proxy["URL"] + updateProxyConfig(network.proxy["URL"]) + } + } + } + + NetworkAddressField { + id: urlField + + onActiveFocusChanged: { + if (!activeFocus && acceptableInput) { + updateProxyConfig(text) + } + } + + text: network.proxyConfig["URL"] || "" + + //: Keep short, placeholder label that cannot wrap + //% "E.g. https://example.com/proxy.pac" + placeholderText: qsTrId("settings_network-la-automatic_proxy_address_example") + + //% "PAC URL" + label: qsTrId("settings_network-la-proxy_pac_url") + + EnterKey.iconSource: "image://theme/icon-m-enter-close" + EnterKey.onClicked: parent.focus = true + } +} diff --git a/usr/lib/qt5/qml/Sailfish/Settings/Networking/AutoDetectProxyForm.qml b/usr/lib/qt5/qml/Sailfish/Settings/Networking/AutoDetectProxyForm.qml new file mode 100644 index 00000000..d731f5e1 --- /dev/null +++ b/usr/lib/qt5/qml/Sailfish/Settings/Networking/AutoDetectProxyForm.qml @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2012 - 2022 Jolla Ltd. + * + * License: Proprietary + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 + +Item { + property QtObject network + height: Math.max(Theme.itemSizeMedium, textItem.height + labelItem.height) + + Label { + id: textItem + text: network.proxy && network.proxy["URL"] + ? network.proxy["URL"] + //% "None currently active" + : qsTrId("settings_network-la-auto_proxy_url_none") + truncationMode: TruncationMode.Fade + color: palette.highlightColor + anchors { + top: parent.top + topMargin: Theme.paddingSmall + left: parent.left + right: parent.right + leftMargin: Theme.horizontalPageMargin + rightMargin: Theme.horizontalPageMargin + } + } + + Label { + id: labelItem + //% "Auto-detected PAC URL" + text: qsTrId("settings_network-la-auto_proxy_detected_url") + font.pixelSize: Theme.fontSizeSmall + truncationMode: TruncationMode.Fade + color: palette.secondaryHighlightColor + bottomPadding: Theme.paddingMedium + anchors { + top: textItem.bottom + topMargin: Theme.paddingSmall + left: parent.left + right: parent.right + leftMargin: Theme.horizontalPageMargin + rightMargin: Theme.horizontalPageMargin + } + } +} diff --git a/usr/lib/qt5/qml/Sailfish/Settings/Networking/CACertChooser.qml b/usr/lib/qt5/qml/Sailfish/Settings/Networking/CACertChooser.qml index d0e99a8c..472f646b 100644 --- a/usr/lib/qt5/qml/Sailfish/Settings/Networking/CACertChooser.qml +++ b/usr/lib/qt5/qml/Sailfish/Settings/Networking/CACertChooser.qml @@ -1,11 +1,13 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 -import MeeGo.Connman 0.2 +import Connman 0.2 Column { id: root signal fromFileSelected() + + property int horizontalMargin: Theme.horizontalPageMargin property QtObject network property bool immediateUpdate property alias labelColor: certComboBox.labelColor @@ -26,6 +28,9 @@ Column { ComboBox { id: certComboBox + + leftMargin: root.horizontalMargin + rightMargin: root.horizontalMargin //% "CA Certificate" label: qsTrId("settings_network-la-ca_cert") @@ -77,12 +82,14 @@ Column { visible: certComboBox.currentIndex === 1 color: Theme.errorColor wrapMode: Text.Wrap - x: Theme.horizontalPageMargin - width: parent.width - 2 * Theme.horizontalPageMargin + x: root.horizontalMargin + width: parent.width - 2 * x } TextField { id: domainSuffixField + + textMargin: root.horizontalMargin text: network ? network.domainSuffixMatch : "" visible: root.visible && certComboBox.currentIndex === 0 diff --git a/usr/lib/qt5/qml/Sailfish/Settings/Networking/ClientCertChooser.qml b/usr/lib/qt5/qml/Sailfish/Settings/Networking/ClientCertChooser.qml index 56680d86..2d2642c8 100644 --- a/usr/lib/qt5/qml/Sailfish/Settings/Networking/ClientCertChooser.qml +++ b/usr/lib/qt5/qml/Sailfish/Settings/Networking/ClientCertChooser.qml @@ -1,6 +1,6 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 -import MeeGo.Connman 0.2 +import Connman 0.2 import com.jolla.settings.system 1.0 Column { @@ -8,6 +8,8 @@ Column { signal certFromFileSelected() signal keyFromFileSelected() + + property int horizontalMargin: Theme.horizontalPageMargin property QtObject network property bool immediateUpdate property alias labelColor: keyComboBox.labelColor @@ -34,6 +36,8 @@ Column { label: qsTrId("settings_network-la-client_key") currentIndex: network && network.privateKeyFile ? 1 : 0 + leftMargin: root.horizontalMargin + rightMargin: root.horizontalMargin Binding on currentIndex { when: network @@ -61,6 +65,9 @@ Column { } ComboBox { id: certComboBox + + leftMargin: root.horizontalMargin + rightMargin: root.horizontalMargin //% "Client certificate" label: qsTrId("settings_network-la-client_cert") visible: !isPkcs12 @@ -96,6 +103,7 @@ Column { SystemPasswordField { id: privateKeyPassphraseField + textMargin: root.horizontalMargin visible: root.visible && network.privateKeyFile !== '' text: network && network.privateKeyPassphrase onTextChanged: { diff --git a/usr/lib/qt5/qml/Sailfish/Settings/Networking/EapComboBox.qml b/usr/lib/qt5/qml/Sailfish/Settings/Networking/EapComboBox.qml index 7e05bc5a..8eb7aac5 100644 --- a/usr/lib/qt5/qml/Sailfish/Settings/Networking/EapComboBox.qml +++ b/usr/lib/qt5/qml/Sailfish/Settings/Networking/EapComboBox.qml @@ -1,6 +1,6 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 -import MeeGo.Connman 0.2 as Connman +import Connman 0.2 as Connman ComboBox { property bool immediateUpdate: true diff --git a/usr/lib/qt5/qml/Sailfish/Settings/Networking/EncryptionComboBox.qml b/usr/lib/qt5/qml/Sailfish/Settings/Networking/EncryptionComboBox.qml index ab3c9baa..282a552b 100644 --- a/usr/lib/qt5/qml/Sailfish/Settings/Networking/EncryptionComboBox.qml +++ b/usr/lib/qt5/qml/Sailfish/Settings/Networking/EncryptionComboBox.qml @@ -1,6 +1,6 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 -import MeeGo.Connman 0.2 +import Connman 0.2 import "WlanUtils.js" as WlanUtils ComboBox { diff --git a/usr/share/jolla-settings/pages/wlan/IPv4AddressField.qml b/usr/lib/qt5/qml/Sailfish/Settings/Networking/IPv4AddressField.qml similarity index 96% rename from usr/share/jolla-settings/pages/wlan/IPv4AddressField.qml rename to usr/lib/qt5/qml/Sailfish/Settings/Networking/IPv4AddressField.qml index e4ed7e9d..b9c7a1aa 100644 --- a/usr/share/jolla-settings/pages/wlan/IPv4AddressField.qml +++ b/usr/lib/qt5/qml/Sailfish/Settings/Networking/IPv4AddressField.qml @@ -6,7 +6,7 @@ NetworkField { // Input mask "0-255.0-255.0-255.0-255" property var inputRegExp: new RegExp(/^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/) - property bool emptyInputOk: false + property bool emptyInputOk regExp: (emptyInputOk && length === 0) ? null : inputRegExp diff --git a/usr/share/jolla-settings/pages/wlan/IPv4Form.qml b/usr/lib/qt5/qml/Sailfish/Settings/Networking/IPv4Form.qml similarity index 77% rename from usr/share/jolla-settings/pages/wlan/IPv4Form.qml rename to usr/lib/qt5/qml/Sailfish/Settings/Networking/IPv4Form.qml index 0aee6889..745610bc 100644 --- a/usr/share/jolla-settings/pages/wlan/IPv4Form.qml +++ b/usr/lib/qt5/qml/Sailfish/Settings/Networking/IPv4Form.qml @@ -7,7 +7,20 @@ Column { property QtObject network - property bool _completed + readonly property bool _completed: addressField.populated + && netmaskField.populated + && gatewayField.populated + && primaryDnsField.populated + && secondaryDnsField.populated + && domainsField.populated + on_CompletedChanged: { + if (_completed) { + updateIPv4() + updateDNS() + updateDomains() + } + } + property bool _updating property bool _ipv4UpdateRequired property bool _nameserversUpdateRequired @@ -62,13 +75,10 @@ Column { function updateIPv4() { var config = {"Method": "manual"} - var isOk = true var updateConfig = function(field, key) { if (field.acceptableInput && checkIp(field.text)) { config[key] = field.text - } else { - isOk = false } } @@ -76,11 +86,9 @@ Column { updateConfig(netmaskField, "Netmask") updateConfig(gatewayField, "Gateway") - if (isOk) { - _updating = true - _ipv4UpdateRequired = false - network.ipv4Config = config - } + _updating = true + _ipv4UpdateRequired = false + network.ipv4Config = config } function updateNameserversIfValid(dnsField) { @@ -134,11 +142,11 @@ Column { } } - Component.onCompleted: _completed = true - IPv4AddressField { id: addressField + property bool populated + focus: true text: network ? (network.ipv4Config["Address"] || network.ipv4["Address"] || "") : "" @@ -151,11 +159,14 @@ Column { EnterKey.iconSource: "image://theme/icon-m-enter-next" EnterKey.onClicked: netmaskField.focus = true + Component.onCompleted: populated = true } IPv4AddressField { id: netmaskField + property bool populated + text: network ? (network.ipv4Config["Netmask"] || network.ipv4["Netmask"] || "") : "" onActiveFocusChanged: if (!activeFocus) updateIPv4IfValid(netmaskField) @@ -167,13 +178,25 @@ Column { EnterKey.iconSource: "image://theme/icon-m-enter-next" EnterKey.onClicked: gatewayField.focus = true + Component.onCompleted: populated = true } IPv4AddressField { id: gatewayField + property bool populated + + emptyInputOk: true text: network ? (network.ipv4Config["Gateway"] || network.ipv4["Gateway"] || "") : "" - onActiveFocusChanged: if (!activeFocus) updateIPv4IfValid(gatewayField) + onActiveFocusChanged: { + if (!activeFocus) { + if (!text && !_updating) { + updateIPv4() + } else { + updateIPv4IfValid(gatewayField) + } + } + } //% "E.g. 192.168.1.1" placeholderText: qsTrId("settings_network-la-default_gateway_example") @@ -183,13 +206,20 @@ Column { EnterKey.iconSource: "image://theme/icon-m-enter-next" EnterKey.onClicked: primaryDnsField.focus = true + Component.onCompleted: populated = true + } + + SectionHeader { + //% "DNS servers" + text: qsTrId("settings_network-la-dns_servesr") } IPv4AddressField { id: primaryDnsField + property bool populated + emptyInputOk: true - Component.onCompleted: text = network.nameserversConfig[0] || network.nameservers[0] || "" onActiveFocusChanged: if (!activeFocus) updateNameserversIfValid(primaryDnsField) //% "E.g. 1.2.3.4" @@ -200,13 +230,18 @@ Column { EnterKey.iconSource: "image://theme/icon-m-enter-next" EnterKey.onClicked: secondaryDnsField.focus = true + Component.onCompleted: { + text = network.nameserversConfig[0] || network.nameservers[0] || "" + populated = true + } } IPv4AddressField { id: secondaryDnsField + property bool populated + emptyInputOk: true - Component.onCompleted: text = network.nameserversConfig[1] || network.nameservers[1] || "" onActiveFocusChanged: if (!activeFocus) updateNameserversIfValid(secondaryDnsField) //% "E.g. 5.6.7.8" @@ -217,11 +252,17 @@ Column { EnterKey.iconSource: "image://theme/icon-m-enter-next" EnterKey.onClicked: domainsField.focus = true + Component.onCompleted: { + text = network.nameserversConfig[1] || network.nameservers[1] || "" + populated = true + } } NetworkField { id: domainsField + property bool populated + //: Keep short, placeholder label that cannot wrap //% "E.g. example.com, domain.com" placeholderText: qsTrId("settings_network-ph-default_domain_names_example") @@ -233,7 +274,10 @@ Column { //% "List valid domain names separated by commas" description: errorHighlight ? qsTrId("settings_network_la-default_domain_names_error") : "" - Component.onCompleted: text = WlanUtils.maybeJoin(network.domainsConfig) || WlanUtils.maybeJoin(network.domains) + Component.onCompleted: { + text = WlanUtils.maybeJoin(network.domainsConfig) || WlanUtils.maybeJoin(network.domains) + populated = true + } onActiveFocusChanged: if (!activeFocus) updateDomainsIfValid(domainsField) EnterKey.iconSource: "image://theme/icon-m-enter-close" EnterKey.onClicked: parent.focus = true diff --git a/usr/lib/qt5/qml/Sailfish/Settings/Networking/IdentityField.qml b/usr/lib/qt5/qml/Sailfish/Settings/Networking/IdentityField.qml index bed8ba52..3a57576e 100644 --- a/usr/lib/qt5/qml/Sailfish/Settings/Networking/IdentityField.qml +++ b/usr/lib/qt5/qml/Sailfish/Settings/Networking/IdentityField.qml @@ -1,6 +1,6 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 -import MeeGo.Connman 0.2 +import Connman 0.2 NetworkField { property QtObject network diff --git a/usr/lib/qt5/qml/Sailfish/Settings/Networking/InnerAuthComboBox.qml b/usr/lib/qt5/qml/Sailfish/Settings/Networking/InnerAuthComboBox.qml index dd4ee000..238f8ca1 100644 --- a/usr/lib/qt5/qml/Sailfish/Settings/Networking/InnerAuthComboBox.qml +++ b/usr/lib/qt5/qml/Sailfish/Settings/Networking/InnerAuthComboBox.qml @@ -1,6 +1,6 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 -import MeeGo.Connman 0.2 as Connman +import Connman 0.2 as Connman ComboBox { property bool immediateUpdate: true diff --git a/usr/share/jolla-settings/pages/wlan/IpPortField.qml b/usr/lib/qt5/qml/Sailfish/Settings/Networking/IpPortField.qml similarity index 100% rename from usr/share/jolla-settings/pages/wlan/IpPortField.qml rename to usr/lib/qt5/qml/Sailfish/Settings/Networking/IpPortField.qml diff --git a/usr/lib/qt5/qml/Sailfish/Settings/Networking/ManualProxyDialog.qml b/usr/lib/qt5/qml/Sailfish/Settings/Networking/ManualProxyDialog.qml new file mode 100644 index 00000000..0ff75f3e --- /dev/null +++ b/usr/lib/qt5/qml/Sailfish/Settings/Networking/ManualProxyDialog.qml @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2022 Jolla Ltd. + * + * License: Proprietary + */ + +import QtQuick 2.6 +import Sailfish.Silica 1.0 +import Sailfish.Settings.Networking 1.0 + +Dialog { + property alias address: addressField.text + property alias port: portField.text + property bool edit + + canAccept: !hasErrors() + onAcceptBlocked: { + addressField.updateErrorHighlight() + portField.updateErrorHighlight() + } + onRejected: { + // Prevent fields being highlighting on cancel + addressField.acceptableInput = true + portField.acceptableInput = true + } + + function hasErrors() { + return !addressField.acceptableInput || !portField.acceptableInput + } + + SilicaFlickable { + anchors.fill: parent + contentHeight: column.height + + Column { + id: column + width: parent.width + + DialogHeader { + title: edit + //% "Edit manual proxy" + ? qsTrId("settings_network-he_edit_manual_proxy") + //% "Add manual proxy" + : qsTrId("settings_network-he_add_manual_proxy") + //: Text used for the dialogue save button + //% "Save" + acceptText: qsTrId("settings_network-he_accept_save") + } + + NetworkAddressField { + id: addressField + focus: true + + //: Keep short, placeholder label that cannot wrap + //% "E.g. http://proxy.example.com" + placeholderText: qsTrId("settings_network-la-manual_proxy_address_example") + + //% "Proxy address" + label: qsTrId("settings_network-la-proxy_address") + + text: network.proxyConfig["Servers"] ? network.proxyConfig["URL"] : network.proxy["URL"] + + EnterKey.iconSource: "image://theme/icon-m-enter-next" + EnterKey.onClicked: portField.focus = true + } + + IpPortField { + id: portField + + EnterKey.iconSource: "image://theme/icon-m-enter-accept" + EnterKey.onClicked: accept() + } + } + + VerticalScrollDecorator {} + } +} diff --git a/usr/lib/qt5/qml/Sailfish/Settings/Networking/ManualProxyForm.qml b/usr/lib/qt5/qml/Sailfish/Settings/Networking/ManualProxyForm.qml new file mode 100644 index 00000000..1c5980c7 --- /dev/null +++ b/usr/lib/qt5/qml/Sailfish/Settings/Networking/ManualProxyForm.qml @@ -0,0 +1,193 @@ +/* + * Copyright (c) 2012 - 2022 Jolla Ltd. + * + * License: Proprietary + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Sailfish.Settings.Networking 1.0 +import "WlanUtils.js" as WlanUtils + +Column { + id: manualProxyRoot + + property QtObject network + + function reset() { + var servers = network.proxyConfig["Servers"] + repeater.model.clear() + for (var i = 0; servers && i < servers.length; i++) { + repeater.model.append({ "server": servers[i] }) + } + } + + function updateProxyConfig() { + var proxyExcludes + var proxyConfig = network.proxyConfig + + if (repeater.model.count > 0) { + proxyConfig["Method"] = "manual" + proxyConfig["Servers"] = [] + + for (var i = 0; i < repeater.model.count; i++) { + proxyConfig["Servers"].push(repeater.model.get(i).server) + } + + if (proxyExcludesField.acceptableInput) { + proxyConfig["Excludes"] = proxyExcludesField.text.replace(" ", "").split(",") + } else { + proxyConfig["Excludes"] = [] + } + } else { + proxyConfig["Method"] = "direct" + } + + network.proxyConfig = proxyConfig + } + + function addManualProxy() { + var obj = pageStack.animatorPush('ManualProxyDialog.qml', { address: "", port: "", edit: false }) + obj.pageCompleted.connect(function(page) { + page.accepted.connect(function() { + repeater.model.append({ "server": page.address + ":" + page.port }) + updateProxyConfig() + }) + }) + } + + function setExcludes() { + proxyExcludesField.text = WlanUtils.maybeJoin(network.proxyConfig["Excludes"]) + } + + Repeater { + id: repeater + model: ListModel {} + + Component.onCompleted: { + reset() + } + + delegate: ListItem { + id: manualProxyItem + contentHeight: Theme.itemSizeMedium + + function editManualProxy() { + var pieces = server.split(":") + var address = server + var port = "0" + if (server.length > 0 && !isNaN(parseInt(pieces[pieces.length - 1], 10))) { + port = pieces[pieces.length - 1] + address = address.slice(0, server.length - port.length - 1) + } + + var obj = pageStack.animatorPush('ManualProxyDialog.qml', { address: address, port: port, edit: true }) + obj.pageCompleted.connect(function(page) { + page.accepted.connect(function() { + server = page.address + ":" + page.port + updateProxyConfig() + }) + }) + } + + menu: ContextMenu { + MenuItem { + //: Menu option to edit a manual proxy entry + //% "Edit" + text: qsTrId("settings_network-me-manual_proxy_edit") + onClicked: editManualProxy() + } + MenuItem { + //: Menu option to delete a manual proxy entry + //% "Delete" + text: qsTrId("settings_network-me-manual_proxy_delete") + onDelayedClick: deleteManualProxy.start() + } + } + + PropertyAnimation { + id: deleteManualProxy + target: manualProxyItem + properties: "contentHeight, opacity" + to: 0 + duration: 200 + easing.type: Easing.InOutQuad + onRunningChanged: { + if (running === false) { + // Keep a copy to avoid problems when we delete the item + var temp = manualProxyRoot + repeater.model.remove(model.index) + temp.updateProxyConfig() + } + } + } + + Label { + id: manualProxyTitle + x: Theme.horizontalPageMargin + y: Theme.paddingMedium + width: parent.width - 2 * x + font.pixelSize: Theme.fontSizeMedium + color: manualProxyItem.highlighted ? Theme.highlightColor : Theme.primaryColor + //: Title for the manual proxy "Proxy 1", "Proxy 2", etc. + //% "Proxy %1" + text: qsTrId("settings_network-la-manual_proxy_identifier").arg(index + 1) + } + Label { + anchors.top: manualProxyTitle.bottom + x: Theme.horizontalPageMargin + width: parent.width - 2 * x + font.pixelSize: Theme.fontSizeExtraSmall + color: manualProxyItem.highlighted ? Theme.secondaryHighlightColor : Theme.secondaryColor + text: model.server + } + onClicked: openMenu() + } + } + + BackgroundItem { + id: addManualProxyButton + onClicked: addManualProxy() + highlighted: down + Icon { + x: parent.width - (width + Theme.itemSizeSmall) / 2.0 + anchors.verticalCenter: parent.verticalCenter + source: "image://theme/icon-m-add" + (parent.highlighted ? "?" + Theme.highlightColor : "") + } + Label { + text: repeater.model.count === 0 ? //% "Add a proxy" + qsTrId("settings_network-bu-manual_proxy_add_a_proxy") + : //% "Add another proxy" + qsTrId("settings_network-bu-manual_proxy_add_another_proxy") + width: parent.width - Theme.iconSizeSmall - Theme.horizontalPageMargin + x: Theme.horizontalPageMargin + anchors.verticalCenter: parent.verticalCenter + color: addManualProxyButton.highlighted ? Theme.highlightColor : Theme.primaryColor + } + } + + NetworkField { + id: proxyExcludesField + + regExp: new RegExp( /^[\w- \.,]*$/ ) + Component.onCompleted: setExcludes() + onActiveFocusChanged: { + if (!activeFocus && repeater.model.count > 0) { + updateProxyConfig() + } + } + + //: Keep short, placeholder label that cannot wrap + //% "E.g. example.com, domain.com" + placeholderText: qsTrId("settings_network-la-exclude_domains_example") + + //% "Exclude domains" + label: qsTrId("settings_network-la-exclude_domains") + + //% "List valid domain names separated by commas" + description: errorHighlight ? qsTrId("settings_network_la-exclude_domains_error") : "" + + EnterKey.iconSource: "image://theme/icon-m-enter-close" + EnterKey.onClicked: parent.focus = true + } +} diff --git a/usr/lib/qt5/qml/Sailfish/Settings/Networking/MobileNetworkStatusIndicator.qml b/usr/lib/qt5/qml/Sailfish/Settings/Networking/MobileNetworkStatusIndicator.qml index 85d955d5..7467e24d 100644 --- a/usr/lib/qt5/qml/Sailfish/Settings/Networking/MobileNetworkStatusIndicator.qml +++ b/usr/lib/qt5/qml/Sailfish/Settings/Networking/MobileNetworkStatusIndicator.qml @@ -8,7 +8,7 @@ import QtQuick 2.6 import Sailfish.Silica 1.0 import Sailfish.Telephony 1.0 -import MeeGo.QOfono 0.2 +import QOfono 0.2 Item { id: root @@ -19,13 +19,13 @@ Item { property bool showMaximumStrength property bool showRoamingStatus - property string iconSuffix + property bool highlighted property bool _simPresent: !!simManager && simManager.ready && simManager.modemHasPresentSim(modemPath) property bool _masked: Telephony.multiSimSupported function _imagePath(iconName) { - return "image://theme/icon-status-" + iconName + iconSuffix + return "image://theme/icon-status-" + iconName } height: Theme.iconSizeExtraSmall @@ -36,8 +36,9 @@ Item { bottom: signalStrengthIndicator.bottom left: signalStrengthIndicator.left } + color: root.highlighted ? palette.highlightColor : palette.primaryColor source: showRoamingStatus && networkRegistration.status === "roaming" - ? "image://theme/icon-status-roaming" + iconSuffix + ? "image://theme/icon-status-roaming" : "" } @@ -60,7 +61,7 @@ Item { property var source: img property var maskSource: mask - property color color: palette.primaryColor + property color color: root.highlighted ? palette.highlightColor : palette.primaryColor fragmentShader: " varying highp vec2 qt_TexCoord0; diff --git a/usr/lib/qt5/qml/com/jolla/settings/accounts/NetworkCheckDialog.qml b/usr/lib/qt5/qml/Sailfish/Settings/Networking/NetworkCheckDialog.qml similarity index 87% rename from usr/lib/qt5/qml/com/jolla/settings/accounts/NetworkCheckDialog.qml rename to usr/lib/qt5/qml/Sailfish/Settings/Networking/NetworkCheckDialog.qml index 57c0baef..c63e4a4f 100644 --- a/usr/lib/qt5/qml/com/jolla/settings/accounts/NetworkCheckDialog.qml +++ b/usr/lib/qt5/qml/Sailfish/Settings/Networking/NetworkCheckDialog.qml @@ -1,5 +1,5 @@ /* - * Copyright (c) 2013 - 2019 Jolla Ltd. + * Copyright (c) 2013 - 2022 Jolla Ltd. * Copyright (c) 2020 Open Mobile Platform LLC. * * License: Proprietary @@ -7,10 +7,9 @@ import QtQuick 2.6 import Sailfish.Silica 1.0 -import Sailfish.Accounts 1.0 +import Sailfish.Settings.Networking 1.0 import Nemo.DBus 2.0 import Nemo.Connectivity 1.0 -import com.jolla.settings.accounts 1.0 Dialog { id: root @@ -111,7 +110,7 @@ Dialog { wrapMode: Text.Wrap //: Not connected to the internet //% "Not connected" - text: qsTrId("settings_accounts-la-not_connected") + text: qsTrId("settings_network-la-not_connected") } Label { @@ -124,7 +123,7 @@ Dialog { wrapMode: Text.Wrap //: The user did not select a network connection //% "You must select an internet connection to continue." - text: qsTrId("settings_accounts-la-must_select_conn") + text: qsTrId("settings_network-la-must_select_conn") } Button { @@ -132,14 +131,17 @@ Dialog { anchors.horizontalCenter: parent.horizontalCenter //: Display the dialog to set up the internet connection //% "Connect" - text: qsTrId("settings_accounts-bt-connect") + text: qsTrId("settings_network-bt-connect") onClicked: connectionHelper.attemptToConnectNetwork() } } - ClickableTextLabel { + Label { id: skipLabel + + property bool pressed: mouseArea.pressed + anchors { left: parent.left leftMargin: Theme.horizontalPageMargin @@ -151,14 +153,23 @@ Dialog { verticalAlignment: Text.AlignBottom font.pixelSize: Theme.fontSizeSmall visible: text != "" + width: parent.width + wrapMode: Text.Wrap + textFormat: Text.StyledText + color: Theme.highlightColor // even when other explanatory text is hidden, show the skip label but dim it to still indicate skipping is possible opacity: retryText.display ? 1.0 : Theme.highlightBackgroundOpacity Behavior on opacity { FadeAnimation {} } - onClicked: { - root.forwardNavigation = true - root.skipClicked() + MouseArea { + id: mouseArea + anchors.fill: parent + + onClicked: { + root.forwardNavigation = true + root.skipClicked() + } } } } diff --git a/usr/lib/qt5/qml/Sailfish/Settings/Networking/NetworkConfig.qml b/usr/lib/qt5/qml/Sailfish/Settings/Networking/NetworkConfig.qml index 4b3a8a1d..9caccdfe 100644 --- a/usr/lib/qt5/qml/Sailfish/Settings/Networking/NetworkConfig.qml +++ b/usr/lib/qt5/qml/Sailfish/Settings/Networking/NetworkConfig.qml @@ -1,5 +1,5 @@ import QtQuick 2.0 -import MeeGo.Connman 0.2 +import Connman 0.2 // Matches networkservice.h QtObject { diff --git a/usr/lib/qt5/qml/Sailfish/Settings/Networking/PassphraseField.qml b/usr/lib/qt5/qml/Sailfish/Settings/Networking/PassphraseField.qml index e06f1972..301fae03 100644 --- a/usr/lib/qt5/qml/Sailfish/Settings/Networking/PassphraseField.qml +++ b/usr/lib/qt5/qml/Sailfish/Settings/Networking/PassphraseField.qml @@ -1,6 +1,6 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 -import MeeGo.Connman 0.2 +import Connman 0.2 import com.jolla.settings.system 1.0 SystemPasswordField { diff --git a/usr/lib/qt5/qml/Sailfish/Settings/Networking/ProxyForm.qml b/usr/lib/qt5/qml/Sailfish/Settings/Networking/ProxyForm.qml new file mode 100644 index 00000000..30bd0151 --- /dev/null +++ b/usr/lib/qt5/qml/Sailfish/Settings/Networking/ProxyForm.qml @@ -0,0 +1,157 @@ +/* + * Copyright (c) 2021 - 2022 Jolla Ltd. + * + * License: Proprietary + */ + +import QtQuick 2.6 +import Sailfish.Silica 1.0 + +Column { + id: root + property QtObject network + property alias currentIndex: proxyCombo.currentIndex + property alias proxyLoader: proxyLoader + property alias comboLabel: proxyCombo.label + + width: parent.width + opacity: enabled ? 1.0 : Theme.opacityLow + + function methodStringToInteger(method) { + if (method === "manual") { + return 1 + } else if (method === "auto") { + return 2 + } else { + // "direct" + return 0 + } + } + + function proxyConfigToInteger(currentIndex, proxyConfig, proxy) { + // On initialisation use the readonly proxy values if they exists + var configIndex = methodStringToInteger(proxy ? proxy["Method"] : proxyConfig["Method"]) + + // The "auto" method is either "auto-detect" (2) or "auto-config" (3) depending + // on whether an explicit URL is set, or the combobox was set by the user + if (configIndex === 2) { + if (proxyConfig["URL"] || currentIndex === 3) { + configIndex = 3 + } + } + + // If "manual" is set by the user, it will identify as "none" until some explicit proxy + // details are configured, so we should show it as "direct" in the meantime + if ((configIndex === 0) && (currentIndex === 1)) { + configIndex = 1 + } + return configIndex + } + + Connections { + target: network + onProxyConfigChanged: { + var configIndex = proxyConfigToInteger(proxyCombo.currentIndex, network.proxyConfig, null) + if (proxyCombo.currentIndex !== configIndex) { + proxyLoader.focus = false + proxyCombo.currentIndex = configIndex + } + if (configIndex === 1) { + proxyLoader.item.reset() + } + } + } + + ComboBox { + id: proxyCombo + + onCurrentIndexChanged: { + var proxyConfig = network.proxyConfig + + switch (currentIndex) { + case 0: + proxyConfig["Method"] = "direct" + break + case 1: + proxyConfig["Method"] = "manual" + break + case 2: + proxyConfig["Method"] = "auto" + proxyConfig["URL"] = "" + break + case 3: + proxyConfig["Method"] = "auto" + break + } + network.proxyConfig = proxyConfig + } + + Component.onCompleted: { + currentIndex = proxyConfigToInteger(proxyCombo.currentIndex, network.proxyConfig, network.proxy) + } + + //: Referring to the network proxy method to use for this connection + //% "Proxy configuration" + label: qsTrId("settings_network-la-proxy_configuration") + menu: ContextMenu { + MenuItem { + //% "Disabled" + text: qsTrId("settings_network-me-proxy_disabled") + } + MenuItem { + //% "Manual" + text: qsTrId("settings_network-me-proxy_manual") + } + MenuItem { + //% "Auto-detect" + text: qsTrId("settings_network-me-proxy_auto_detect") + } + MenuItem { + //% "Auto config URL" + text: qsTrId("settings_network-me-proxy_auto_config") + } + } + } + + Loader { + id: proxyLoader + width: parent.width + sourceComponent: { + var index = proxyCombo.currentIndex + if (index === 0) { + return fakeEmptyItem + } else if (index === 1) { + return manualProxy + } else if (index === 2) { + return autoDetectProxy + } else if (index === 3) { + return autoConfigProxy + } + } + } + + // Workaround for Loader not resetting its height when sourceComponent is undefined + Component { + id: fakeEmptyItem + + Item {} + } + + Component { + id: manualProxy + + ManualProxyForm { network: root.network } + } + + Component { + id: autoDetectProxy + + AutoDetectProxyForm { network: root.network } + } + + Component { + id: autoConfigProxy + + AutoConfigProxyForm { network: root.network } + } +} diff --git a/usr/lib/qt5/qml/Sailfish/Settings/Networking/SsidField.qml b/usr/lib/qt5/qml/Sailfish/Settings/Networking/SsidField.qml index a7db7769..252be62a 100644 --- a/usr/lib/qt5/qml/Sailfish/Settings/Networking/SsidField.qml +++ b/usr/lib/qt5/qml/Sailfish/Settings/Networking/SsidField.qml @@ -1,6 +1,6 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 -import MeeGo.Connman 0.2 +import Connman 0.2 TextField { property QtObject network diff --git a/usr/lib/qt5/qml/Sailfish/Settings/Networking/Vpn/VpnAdvancedSettingsPage.qml b/usr/lib/qt5/qml/Sailfish/Settings/Networking/Vpn/VpnAdvancedSettingsPage.qml index cf57179f..b34e4dee 100644 --- a/usr/lib/qt5/qml/Sailfish/Settings/Networking/Vpn/VpnAdvancedSettingsPage.qml +++ b/usr/lib/qt5/qml/Sailfish/Settings/Networking/Vpn/VpnAdvancedSettingsPage.qml @@ -7,7 +7,7 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 -import org.nemomobile.systemsettings 1.0 +import Nemo.Connectivity 1.0 import Sailfish.Settings.Networking 1.0 import Sailfish.Settings.Networking.Vpn 1.0 @@ -21,7 +21,7 @@ Page { property var providerProperties property var userRoutes property ListModel routesModel: ListModel {} - property var _propertiesAlreadySet: {} + property var _propertiesAlreadySet: ({}) signal propertiesUpdated(var connectionProperties, var providerProperties) diff --git a/usr/lib/qt5/qml/Sailfish/Settings/Networking/Vpn/VpnImportDialog.qml b/usr/lib/qt5/qml/Sailfish/Settings/Networking/Vpn/VpnImportDialog.qml index 6a591804..e0ef55a0 100644 --- a/usr/lib/qt5/qml/Sailfish/Settings/Networking/Vpn/VpnImportDialog.qml +++ b/usr/lib/qt5/qml/Sailfish/Settings/Networking/Vpn/VpnImportDialog.qml @@ -23,7 +23,6 @@ Dialog { width: parent.width DialogHeader { - id: pageHeader title: importFailed ? failTitle : root.title acceptText: '' diff --git a/usr/lib/qt5/qml/Sailfish/Settings/Networking/Vpn/VpnPlatformEditDialog.qml b/usr/lib/qt5/qml/Sailfish/Settings/Networking/Vpn/VpnPlatformEditDialog.qml index 4384ba06..d595a47c 100644 --- a/usr/lib/qt5/qml/Sailfish/Settings/Networking/Vpn/VpnPlatformEditDialog.qml +++ b/usr/lib/qt5/qml/Sailfish/Settings/Networking/Vpn/VpnPlatformEditDialog.qml @@ -7,8 +7,8 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 -import MeeGo.Connman 0.2 -import org.nemomobile.systemsettings 1.0 +import Connman 0.2 +import Nemo.Connectivity 1.0 import Sailfish.Settings.Networking 1.0 import Sailfish.Settings.Networking.Vpn 1.0 diff --git a/usr/lib/qt5/qml/Sailfish/Settings/Networking/Vpn/VpnTypes.js b/usr/lib/qt5/qml/Sailfish/Settings/Networking/Vpn/VpnTypes.js index 36213f94..908242b3 100644 --- a/usr/lib/qt5/qml/Sailfish/Settings/Networking/Vpn/VpnTypes.js +++ b/usr/lib/qt5/qml/Sailfish/Settings/Networking/Vpn/VpnTypes.js @@ -8,7 +8,7 @@ .pragma library .import org.nemomobile.systemsettings 1.0 as SystemSettings .import Sailfish.Silica 1.0 as Silica -.import MeeGo.Connman 0.2 as Connman +.import Connman 0.2 as Connman var settingsPath = "/usr/share/sailfish-vpn/" @@ -40,6 +40,10 @@ function stateName(state) { case Connman.VpnConnection.Failure: //% "Failure" return qsTrId("settings_network-me-vpn_state_failure") + case Connman.VpnConnection.Association: + //: Shown during the time the system is waiting for user to input credentials. + //% "Association" + return qsTrId("settings_network-me-vpn_state_association") case Connman.VpnConnection.Configuration: //% "Configuration" return qsTrId("settings_network-me-vpn_state_configuration") @@ -184,9 +188,9 @@ function importFile(pageStack, mainPage, path, vpnType, parser) { if (Object.keys(props).length == 0) { var failureDialog = importDialogPath(vpnType) if (pageStack.currentPage != mainPage) { - pageStack.animatorReplaceAbove(mainPage, failureDialog, { mainPage: mainPage, vpnType: vpnType, importFailed: true }) + pageStack.animatorReplaceAbove(mainPage, failureDialog, { _mainPage: mainPage, _vpnType: vpnType, importFailed: true }) } else { - pageStack.push(failureDialog, { mainPage: mainPage }, Silica.PageStackAction.Immediate) + pageStack.push(failureDialog, { _mainPage: mainPage }, Silica.PageStackAction.Immediate) } } else { var connectionProperties = {} diff --git a/usr/lib/qt5/qml/Sailfish/Settings/Networking/WlanUtils.js b/usr/lib/qt5/qml/Sailfish/Settings/Networking/WlanUtils.js index 58569624..d7402f8a 100644 --- a/usr/lib/qt5/qml/Sailfish/Settings/Networking/WlanUtils.js +++ b/usr/lib/qt5/qml/Sailfish/Settings/Networking/WlanUtils.js @@ -1,4 +1,4 @@ -.import MeeGo.Connman 0.2 as Connman +.import Connman 0.2 as Connman function maybeJoin(strlist) { return strlist && strlist.length > 0 ? strlist.join(",") : "" diff --git a/usr/lib/qt5/qml/Sailfish/Settings/Networking/qmldir b/usr/lib/qt5/qml/Sailfish/Settings/Networking/qmldir index 561c4e43..74f5629b 100644 --- a/usr/lib/qt5/qml/Sailfish/Settings/Networking/qmldir +++ b/usr/lib/qt5/qml/Sailfish/Settings/Networking/qmldir @@ -8,6 +8,7 @@ MobileNetworkStatusIndicator 1.0 MobileNetworkStatusIndicator.qml NetworkingMobileDataConnection 1.0 NetworkingMobileDataConnection.qml NetworkAddressField 1.0 NetworkAddressField.qml NetworkField 1.0 NetworkField.qml +NetworkCheckDialog 1.0 NetworkCheckDialog.qml NetworkConfig 1.0 NetworkConfig.qml PassphraseField 1.0 PassphraseField.qml SsidField 1.0 SsidField.qml @@ -18,3 +19,12 @@ EapComboBox 1.0 EapComboBox.qml InnerAuthComboBox 1.0 InnerAuthComboBox.qml ClientCertChooser 1.0 ClientCertChooser.qml AnonymousIdentityField 1.0 AnonymousIdentityField.qml +AutoDetectProxyForm 1.0 AutoDetectProxyForm.qml +AutoConfigProxyForm 1.0 AutoConfigProxyForm.qml +IPv4AddressField 1.0 IPv4AddressField.qml +IPv4Form 1.0 IPv4Form.qml +IpPortField 1.0 IpPortField.qml +ManualProxyForm 1.0 ManualProxyForm.qml +ProxyForm 1.0 ProxyForm.qml +AdvancedSettingsColumn 1.0 AdvancedSettingsColumn.qml +ManualProxyDialog 1.0 ManualProxyDialog.qml diff --git a/usr/lib/qt5/qml/Sailfish/Shell/Effects/qmldir b/usr/lib/qt5/qml/Sailfish/Shell/Effects/qmldir new file mode 100644 index 00000000..d585f696 --- /dev/null +++ b/usr/lib/qt5/qml/Sailfish/Shell/Effects/qmldir @@ -0,0 +1,3 @@ +module Sailfish.Shell.Effects +plugin SailfishShellEffectsPlugin +classname SailfishShellEffectsPlugin diff --git a/usr/lib/qt5/qml/Sailfish/Shell/Gestures/qmldir b/usr/lib/qt5/qml/Sailfish/Shell/Gestures/qmldir new file mode 100644 index 00000000..3f6ed824 --- /dev/null +++ b/usr/lib/qt5/qml/Sailfish/Shell/Gestures/qmldir @@ -0,0 +1,3 @@ +module Sailfish.Shell.Gestures +plugin SailfishShellGesturesPlugin +classname SailfishShellGesturesPlugin diff --git a/usr/lib/qt5/qml/Sailfish/Shell/Windows/qmldir b/usr/lib/qt5/qml/Sailfish/Shell/Windows/qmldir new file mode 100644 index 00000000..9e33dc3f --- /dev/null +++ b/usr/lib/qt5/qml/Sailfish/Shell/Windows/qmldir @@ -0,0 +1,3 @@ +module Sailfish.Shell.Windows +plugin SailfishShellWindowsPlugin +classname SailfishShellWindowsPlugin diff --git a/usr/lib/qt5/qml/Sailfish/Silica/ApplicationWindow.qml b/usr/lib/qt5/qml/Sailfish/Silica/ApplicationWindow.qml index dbaaabe4..7d9c6047 100644 --- a/usr/lib/qt5/qml/Sailfish/Silica/ApplicationWindow.qml +++ b/usr/lib/qt5/qml/Sailfish/Silica/ApplicationWindow.qml @@ -373,8 +373,8 @@ Private.Window { states: [ State { - when: stack.currentOrientation == Orientation.Portrait || - stack.currentOrientation == Orientation.None + when: stack.currentOrientation == Orientation.Portrait + || stack.currentOrientation == Orientation.None AnchorChanges { target: clippingItem @@ -386,7 +386,7 @@ Private.Window { }, State { - when: stack.currentOrientation == Orientation.PortraitInverted + when: stack.currentOrientation == Orientation.PortraitInverted AnchorChanges { target: clippingItem @@ -398,7 +398,7 @@ Private.Window { }, State { - when: stack.currentOrientation == Orientation.Landscape + when: stack.currentOrientation == Orientation.Landscape AnchorChanges { target: clippingItem @@ -410,7 +410,7 @@ Private.Window { }, State { - when: stack.currentOrientation == Orientation.LandscapeInverted + when: stack.currentOrientation == Orientation.LandscapeInverted AnchorChanges { target: clippingItem @@ -552,7 +552,7 @@ Private.Window { PauseAnimation { id: lowerAnimation - duration: 1000 + duration: 1000 running: false } } diff --git a/usr/lib/qt5/qml/Sailfish/Silica/BusyLabel.qml b/usr/lib/qt5/qml/Sailfish/Silica/BusyLabel.qml index 8cc63e75..6a9b28e2 100644 --- a/usr/lib/qt5/qml/Sailfish/Silica/BusyLabel.qml +++ b/usr/lib/qt5/qml/Sailfish/Silica/BusyLabel.qml @@ -47,7 +47,7 @@ Column { y: Math.round(_portrait ? Screen.height/4 : Screen.width/4) spacing: Theme.paddingLarge - width: parent.width + width: parent && parent.width || 0 opacity: running ? 1.0 : 0.0 Behavior on opacity { FadeAnimator { duration: 400 } } @@ -56,6 +56,7 @@ Column { running: parent.running size: BusyIndicatorSize.Large anchors.horizontalCenter: parent.horizontalCenter + visible: parent.opacity !== 0 } InfoLabel { id: label diff --git a/usr/lib/qt5/qml/Sailfish/Silica/Button.qml b/usr/lib/qt5/qml/Sailfish/Silica/Button.qml index 1c2b00fb..069c4871 100644 --- a/usr/lib/qt5/qml/Sailfish/Silica/Button.qml +++ b/usr/lib/qt5/qml/Sailfish/Silica/Button.qml @@ -67,8 +67,8 @@ SilicaMouseArea { height: implicitHeight implicitHeight: Theme.itemSizeExtraSmall - implicitWidth: image.progress !== 0.0 && text === "" ? Theme.buttonWidthTiny : - Math.max(preferredWidth, content.fullWidth) + implicitWidth: image.progress !== 0.0 && text === "" + ? Theme.buttonWidthTiny : Math.max(preferredWidth, content.fullWidth) highlighted: _showPress @@ -96,8 +96,8 @@ SilicaMouseArea { id: content property bool alignLeft - readonly property real fullWidth: image.implicitWidth + spacing + - buttonText.implicitWidth + 2 * Theme.paddingMedium + readonly property real fullWidth: image.implicitWidth + spacing + + buttonText.implicitWidth + 2 * Theme.paddingMedium anchors.verticalCenter: parent.verticalCenter anchors.horizontalCenter: alignLeft ? undefined : parent.horizontalCenter @@ -109,7 +109,7 @@ SilicaMouseArea { id: image anchors.verticalCenter: parent.verticalCenter - objectName: "image" + objectName: "testimage" } Label { diff --git a/usr/lib/qt5/qml/Sailfish/Silica/ColumnView.qml b/usr/lib/qt5/qml/Sailfish/Silica/ColumnView.qml index 2a1ad50a..54a9f322 100644 --- a/usr/lib/qt5/qml/Sailfish/Silica/ColumnView.qml +++ b/usr/lib/qt5/qml/Sailfish/Silica/ColumnView.qml @@ -51,7 +51,7 @@ Item { property alias currentItem: resultsView.currentItem property alias currentIndex: resultsView.currentIndex - property bool menuOpen: resultsView.__silica_contextmenu_instance && resultsView.__silica_contextmenu_instance._open + property alias menuOpen: resultsView.menuOpen function _listStartPosition() { return y + mapToItem(flickable.contentItem, 0, 0).y @@ -82,9 +82,17 @@ Item { property int __silica_hidden_flickable property Item __silica_contextmenu_instance - property real _menuHeight: __silica_contextmenu_instance && __silica_contextmenu_instance._open + property bool menuOpen: __silica_contextmenu_instance && __silica_contextmenu_instance._open + property real _menuHeight: menuOpen ? __silica_contextmenu_instance._displayHeight : 0 + property real _originYOffset: originY + onOriginYChanged: { + if (!menuOpen) { + _originYOffset = originY + } + } + onMenuOpenChanged: _originYOffset = originY // We have to calculate our own contentHeight, as changing contentY causes contentHeight // to change when the ListView is estimating contentHeight (which occurs when the context @@ -96,7 +104,7 @@ Item { height: Math.min(_contentHeight, _displayHeight) y: Math.max(resultsList._listOffset, 0) - contentY: y + contentY: y + _originYOffset currentIndex: -1 interactive: false diff --git a/usr/lib/qt5/qml/Sailfish/Silica/ComboBox.qml b/usr/lib/qt5/qml/Sailfish/Silica/ComboBox.qml index 8b022c3d..02abd0a1 100644 --- a/usr/lib/qt5/qml/Sailfish/Silica/ComboBox.qml +++ b/usr/lib/qt5/qml/Sailfish/Silica/ComboBox.qml @@ -43,6 +43,7 @@ ValueButton { property alias currentItem: controller.currentItem property alias automaticSelection: controller.automaticSelection readonly property bool _menuOpen: controller.menuOpen + property alias _controller: controller height: _menuOpen ? menu.height + contentItem.height : contentItem.height value: controller.value diff --git a/usr/lib/qt5/qml/Sailfish/Silica/HighlightBar.qml b/usr/lib/qt5/qml/Sailfish/Silica/HighlightBar.qml index cdb54c6c..d83ed3b1 100644 --- a/usr/lib/qt5/qml/Sailfish/Silica/HighlightBar.qml +++ b/usr/lib/qt5/qml/Sailfish/Silica/HighlightBar.qml @@ -101,7 +101,7 @@ Rectangle { Component.onCompleted: { // avoid hard dependency to ngf module - _ngfEffect = Qt.createQmlObject("import org.nemomobile.ngf 1.0; NonGraphicalFeedback { event: 'pulldown_highlight' }", + _ngfEffect = Qt.createQmlObject("import Nemo.Ngf 1.0; NonGraphicalFeedback { event: 'pulldown_highlight' }", highlightItem, 'NonGraphicalFeedback'); } diff --git a/usr/lib/qt5/qml/Sailfish/Silica/Keypad.qml b/usr/lib/qt5/qml/Sailfish/Silica/Keypad.qml index 78411306..28952b44 100644 --- a/usr/lib/qt5/qml/Sailfish/Silica/Keypad.qml +++ b/usr/lib/qt5/qml/Sailfish/Silica/Keypad.qml @@ -106,7 +106,7 @@ SilicaControl { pressedButtonBackground.y = itemCenter.y - pressedButtonBackground.height/2 } - width: parent.width + width: parent ? parent.width : column.implicitWidth implicitHeight: column.implicitHeight Component.onCompleted: { diff --git a/usr/lib/qt5/qml/Sailfish/Silica/MenuItem.qml b/usr/lib/qt5/qml/Sailfish/Silica/MenuItem.qml index 4e137acd..83b22fd1 100644 --- a/usr/lib/qt5/qml/Sailfish/Silica/MenuItem.qml +++ b/usr/lib/qt5/qml/Sailfish/Silica/MenuItem.qml @@ -36,6 +36,7 @@ import Sailfish.Silica 1.0 Label { id: menuItem + property bool down signal earlyClick diff --git a/usr/lib/qt5/qml/Sailfish/Silica/MenuLabel.qml b/usr/lib/qt5/qml/Sailfish/Silica/MenuLabel.qml index ab537a58..10c28104 100644 --- a/usr/lib/qt5/qml/Sailfish/Silica/MenuLabel.qml +++ b/usr/lib/qt5/qml/Sailfish/Silica/MenuLabel.qml @@ -42,8 +42,10 @@ SilicaItem { property real verticalOffset property int __silica_menulabel - height: Math.max(text.height + Theme.paddingSmall*2, Theme.itemSizeExtraSmall - (screen.sizeCategory <= Screen.Medium ? Theme.paddingLarge : Theme.paddingMedium)) + height: Math.max(text.height + Theme.paddingSmall*2, + Theme.itemSizeExtraSmall - (screen.sizeCategory <= Screen.Medium ? Theme.paddingLarge : Theme.paddingMedium)) width: parent ? parent.width : Screen.width + Label { id: text color: palette.secondaryHighlightColor diff --git a/usr/lib/qt5/qml/Sailfish/Silica/MiniComboBox.qml b/usr/lib/qt5/qml/Sailfish/Silica/MiniComboBox.qml index 5f9a7bd8..a51b8602 100644 --- a/usr/lib/qt5/qml/Sailfish/Silica/MiniComboBox.qml +++ b/usr/lib/qt5/qml/Sailfish/Silica/MiniComboBox.qml @@ -64,6 +64,8 @@ Private.SilicaMouseArea { height: _menuOpen ? (menu.height + contentRow.height + Theme.paddingSmall) : contentRow.height implicitWidth: buttonText.implicitWidth + (3 * Theme.paddingSmall) + Theme.iconSizeSmall width: { + if (!parent) return 0 + var leftPadding = parent.leftPadding || parent.padding || 0 var rightPadding = parent.rightPadding || parent.padding || 0 var availableWidth = parent.width - leftPadding - rightPadding diff --git a/usr/lib/qt5/qml/Sailfish/Silica/Page.qml b/usr/lib/qt5/qml/Sailfish/Silica/Page.qml index 6bf3d557..c96530ef 100644 --- a/usr/lib/qt5/qml/Sailfish/Silica/Page.qml +++ b/usr/lib/qt5/qml/Sailfish/Silica/Page.qml @@ -145,7 +145,7 @@ Private.SilicaMouseArea { property alias _windowOpacity: page.opacity property bool _opaqueBackground: background !== null && background != backgroundComponent - readonly property bool _exposed: pageContainer + readonly property bool _exposed: pageContainer && __stack_container && pageContainer.visible && ((pageContainer._currentContainer === __stack_container) @@ -198,7 +198,7 @@ Private.SilicaMouseArea { // bindings will be broken so property changes due to state fast-forwarding aren't propagated. property real width: page.isPortrait ? page._horizontalDimension : page._verticalDimension property real height: page.isPortrait ? page._verticalDimension : page._horizontalDimension - property real orientation: Orientation.Portrait + property real orientation: Orientation.Portrait property real rotation onDesiredPageOrientationChanged: _updatePageOrientation() diff --git a/usr/lib/qt5/qml/Sailfish/Silica/PageBusyIndicator.qml b/usr/lib/qt5/qml/Sailfish/Silica/PageBusyIndicator.qml index cf6f23b1..e53606ea 100644 --- a/usr/lib/qt5/qml/Sailfish/Silica/PageBusyIndicator.qml +++ b/usr/lib/qt5/qml/Sailfish/Silica/PageBusyIndicator.qml @@ -45,6 +45,6 @@ BusyIndicator { y: _page || !parent ? Math.round(_portrait ? Screen.height/4 : Screen.width/4) : parent.height/4 - anchors.horizontalCenter: parent.horizontalCenter + anchors.horizontalCenter: parent && parent.horizontalCenter || undefined size: BusyIndicatorSize.Large -} \ No newline at end of file +} diff --git a/usr/lib/qt5/qml/Sailfish/Silica/PageStack.qml b/usr/lib/qt5/qml/Sailfish/Silica/PageStack.qml index 8280de4f..d208bdcb 100644 --- a/usr/lib/qt5/qml/Sailfish/Silica/PageStack.qml +++ b/usr/lib/qt5/qml/Sailfish/Silica/PageStack.qml @@ -1118,7 +1118,7 @@ PageStackBase { // Ensure we are fully opaque opacity = 1.0 - } else if (status === PageStatus.Deactivating){ + } else if (status === PageStatus.Deactivating) { setStatus(PageStatus.Inactive) hide() if (expired) { diff --git a/usr/lib/qt5/qml/Sailfish/Silica/PasswordField.qml b/usr/lib/qt5/qml/Sailfish/Silica/PasswordField.qml index 1a02bd88..6b1f9aea 100644 --- a/usr/lib/qt5/qml/Sailfish/Silica/PasswordField.qml +++ b/usr/lib/qt5/qml/Sailfish/Silica/PasswordField.qml @@ -59,30 +59,44 @@ TextField { onActiveChanged: if (!Qt.application.active && text.length > 0) _usePasswordEchoMode = true } - rightItem: IconButton { - id: passwordVisibilityButton + rightItem: Row { + visible: root.errorHighlight || showEchoModeToggle + height: Math.max(passwordVisibilityButton.height, errorIconContainer.height) - width: icon.width + 2*Theme.paddingMedium - height: icon.height + Item { + id: errorIconContainer - enabled: showEchoModeToggle - opacity: showEchoModeToggle ? 1.0 : 0.0 - Behavior on opacity { FadeAnimation {}} + visible: root.errorHighlight + width: root._errorIcon.width + height: root._errorIcon.height + } + + IconButton { + id: passwordVisibilityButton + + width: icon.width + 2*Theme.paddingMedium + height: icon.height - onClicked: { - if (_automaticEchoModeToggle) { - root._usePasswordEchoMode = !root._usePasswordEchoMode + enabled: showEchoModeToggle + opacity: showEchoModeToggle ? 1.0 : 0.0 + Behavior on opacity { FadeAnimation {}} + visible: opacity > 0 + + onClicked: { + if (_automaticEchoModeToggle) { + root._usePasswordEchoMode = !root._usePasswordEchoMode + } + _echoModeToggleClicked() } - _echoModeToggleClicked() - } - icon.source: "image://theme/icon-splus-" + (root.echoMode == TextInput.Password ? "show-password" - : "hide-password") + icon.source: "image://theme/icon-splus-" + (root.echoMode == TextInput.Password ? "show-password" + : "hide-password") + } states: State { when: root.errorHighlight PropertyChanges { - target: root - rightItem: root._errorIcon + target: root._errorIcon + parent: errorIconContainer } } } diff --git a/usr/lib/qt5/qml/Sailfish/Silica/PullDownMenu.qml b/usr/lib/qt5/qml/Sailfish/Silica/PullDownMenu.qml index 2b8b72bb..57b2ce18 100644 --- a/usr/lib/qt5/qml/Sailfish/Silica/PullDownMenu.qml +++ b/usr/lib/qt5/qml/Sailfish/Silica/PullDownMenu.qml @@ -41,10 +41,12 @@ PulleyMenuBase { id: pullDownMenu property real topMargin: Theme.itemSizeSmall + property real _effectiveTopMargin: topMargin + + ((Screen.hasCutouts && _page && _page.isPortrait) + ? Screen.topCutout.height : 0) property real bottomMargin: _menuLabel ? 0 : Theme.paddingLarge - property real _contentEnd: contentColumn.height + bottomMargin property Item _menuLabel: { - var lastChild = contentColumn.visible && Util.childAt(contentColumn, width/2, contentColumn.height-1) + var lastChild = contentColumn.visible && Util.childAt(contentColumn, width / 2, contentColumn.height - 1) if (lastChild && lastChild.hasOwnProperty("__silica_menulabel")) { return lastChild } @@ -56,24 +58,41 @@ PulleyMenuBase { spacing: 0 y: flickable.originY - height + _contentEnd: contentColumn.height + bottomMargin _contentColumn: contentColumn _isPullDownMenu: true _inactiveHeight: 0 - _activeHeight: contentColumn.height + topMargin + bottomMargin - _inactivePosition: Math.round(flickable.originY - (_inactiveHeight + spacing)) + _activeHeight: contentColumn.height + _effectiveTopMargin + bottomMargin + _inactivePosition: Math.round(flickable.originY - _inactiveHeight - spacing) _finalPosition: _inactivePosition - _activeHeight _menuIndicatorPosition: height - _menuItemHeight + Theme.paddingSmall - spacing - _highlightIndicatorPosition: Math.min(height - Math.min(_dragDistance, _contentEnd) - spacing, - _menuIndicatorPosition - (_dragDistance/(_menuItemHeight+_bottomDragMargin)*(Theme.paddingSmall+_bottomDragMargin))) + _highlightIndicatorPosition: { + if (_dragDistance <= (_effectiveTopMargin + _menuItemHeight)) { + // gradually getting closer to (or inside) the lowest menu item + return _menuIndicatorPosition + - ((_dragDistance / (_menuItemActivationThreshold + _bottomDragMargin)) + * (Theme.paddingSmall + _bottomDragMargin)) + } else { + // position to topmost item when dragged beyond the items. only briefly shown during fade out. + // or if there are disabled items in the menu, this ensures the highlight stays at the activation point + return height + - Math.min(_dragDistance - _menuItemActivationThreshold + _menuItemHeight, _contentEnd) + - spacing + } + } property Component background: Rectangle { id: bg - anchors { fill: parent; bottomMargin: (pullDownMenu.spacing - _shadowHeight) * Math.min(1, _dragDistance/Theme.itemSizeSmall) } + + anchors { + fill: parent + bottomMargin: (pullDownMenu.spacing - _shadowHeight) * Math.min(1, _dragDistance / Theme.itemSizeSmall) + } opacity: pullDownMenu.active ? 1.0 : 0.0 gradient: Gradient { GradientStop { position: 0.0; color: Theme.rgba(pullDownMenu.backgroundColor, Theme.highlightBackgroundOpacity + 0.1) } GradientStop { - position: (pullDownMenu.height-pullDownMenu.spacing)/bg.height + position: (pullDownMenu.height - pullDownMenu.spacing) / bg.height color: Theme.rgba(pullDownMenu.backgroundColor, Theme.highlightBackgroundOpacity) } GradientStop { position: 1.0; color: Theme.rgba(pullDownMenu.backgroundColor, 0.0) } @@ -86,7 +105,9 @@ PulleyMenuBase { on_AtInitialPositionChanged: { if (!_atInitialPosition && !flickable.moving && _page && _page.orientationTransitionRunning) { // If this flickable has a context menu open, the menu visibility takes precedence over initial position reset - if (('__silica_contextmenu_instance' in flickable) && flickable.__silica_contextmenu_instance && flickable.__silica_contextmenu_instance._open) { + if (('__silica_contextmenu_instance' in flickable) + && flickable.__silica_contextmenu_instance + && flickable.__silica_contextmenu_instance._open) { return } @@ -106,6 +127,20 @@ PulleyMenuBase { } } + on_EffectiveTopMarginChanged: { + if (_atFinalPosition) { + resetOpenPositionTimer.start() // using timer to ensure the position properties have updated + } + } + + Timer { + id: resetOpenPositionTimer + interval: 0 + onTriggered: { + flickable.contentY = _finalPosition + } + } + Column { id: contentColumn @@ -115,14 +150,14 @@ PulleyMenuBase { onMenuContentYChanged: { if (menuContentY >= 0) { if (flickable.dragging && !_bounceBackRunning) { - _highlightMenuItem(contentColumn, menuContentY - y + _menuItemHeight) - } else if (quickSelect){ + _highlightMenuItem(contentColumn, menuContentY - y + _menuItemActivationThreshold) + } else if (quickSelect) { _quickSelectMenuItem(contentColumn, menuContentY - y + _menuItemHeight) } } } - y: pullDownMenu.topMargin + y: pullDownMenu._effectiveTopMargin width: parent.width visible: active } diff --git a/usr/lib/qt5/qml/Sailfish/Silica/PushUpMenu.qml b/usr/lib/qt5/qml/Sailfish/Silica/PushUpMenu.qml index 2bd974f2..942afc48 100644 --- a/usr/lib/qt5/qml/Sailfish/Silica/PushUpMenu.qml +++ b/usr/lib/qt5/qml/Sailfish/Silica/PushUpMenu.qml @@ -41,7 +41,6 @@ PulleyMenuBase { property real topMargin: _menuLabel ? 0 : Theme.paddingLarge property real bottomMargin: Theme.itemSizeSmall - property real _contentEnd: contentColumn.height + topMargin property Item _menuLabel: { var firstChild = contentColumn.visible && Util.childAt(contentColumn, width/2, 1) if (firstChild && firstChild.hasOwnProperty("__silica_menulabel")) { @@ -55,6 +54,7 @@ PulleyMenuBase { spacing: 0 y: flickable.originY + flickable.contentHeight + _contentDeficit + _contentEnd: contentColumn.height + topMargin _contentColumn: contentColumn _isPullDownMenu: false _inactiveHeight: 0 @@ -62,12 +62,18 @@ PulleyMenuBase { _inactivePosition: Math.round(y + _inactiveHeight + spacing - flickable.height) _finalPosition: _inactivePosition + _activeHeight _menuIndicatorPosition: -Theme.paddingSmall + spacing - _highlightIndicatorPosition: Math.max(Math.min(_dragDistance, _contentEnd) - _menuItemHeight + spacing, - _menuIndicatorPosition + (_dragDistance/(_menuItemHeight+_topDragMargin)*(Theme.paddingSmall+_topDragMargin))) + _highlightIndicatorPosition: Math.max(Math.min(_dragDistance, _contentEnd) + - _menuItemHeight + spacing, + _menuIndicatorPosition + (_dragDistance / (_menuItemHeight + _topDragMargin) + * (Theme.paddingSmall + _topDragMargin))) property Component background: Rectangle { id: bg - anchors { fill: parent; topMargin: (pushUpMenu.spacing - _shadowHeight) * Math.min(1, _dragDistance/Theme.itemSizeSmall) } + + anchors { + fill: parent + topMargin: (pushUpMenu.spacing - _shadowHeight) * Math.min(1, _dragDistance/Theme.itemSizeSmall) + } opacity: pushUpMenu.active ? 1.0 : 0.0 gradient: Gradient { GradientStop { position: 0.0; color: Theme.rgba(pushUpMenu.backgroundColor, 0.0) } @@ -92,7 +98,7 @@ PulleyMenuBase { if (menuContentY >= 0) { if (flickable.dragging && !_bounceBackRunning) { _highlightMenuItem(contentColumn, menuContentY - y - _menuItemHeight) - } else if (quickSelect){ + } else if (quickSelect) { _quickSelectMenuItem(contentColumn, menuContentY - y - _menuItemHeight) } } @@ -111,7 +117,9 @@ PulleyMenuBase { // Ensure that we are positioned at the bottom limit, even if the content does not fill the height property real _contentDeficit: Math.max(flickable.height - (flickable.contentHeight + _pdmHeight + spacing), 0) - property real _pdmHeight: flickable.pullDownMenu ? (flickable.pullDownMenu._inactiveHeight + flickable.pullDownMenu.spacing) : 0 + property real _pdmHeight: flickable.pullDownMenu + ? (flickable.pullDownMenu._inactiveHeight + flickable.pullDownMenu.spacing) + : 0 function _addToFlickable(flickableItem) { if (flickableItem.pushUpMenu !== undefined) { diff --git a/usr/lib/qt5/qml/Sailfish/Silica/RemorsePopup.qml b/usr/lib/qt5/qml/Sailfish/Silica/RemorsePopup.qml index 668f2958..5e5953c6 100644 --- a/usr/lib/qt5/qml/Sailfish/Silica/RemorsePopup.qml +++ b/usr/lib/qt5/qml/Sailfish/Silica/RemorsePopup.qml @@ -94,6 +94,21 @@ RemorseBase { z: 1 _wideMode: screen.sizeCategory > Screen.Medium _screenMargin: 0 + leftMargin: { + if (_page && _page.orientation == Orientation.Portrait && Screen.topCutout.height > Theme.paddingLarge) { + // assuming the popup pushed down enough not to need further corner avoidance + return Theme.horizontalPageMargin + } + + var biggestCorner = Math.max(Screen.topLeftCorner.radius, + Screen.topRightCorner.radius, + Screen.bottomLeftCorner.radius, + Screen.bottomRightCorner.radius) + + // popup is quite close to the edge so avoid the whole rounding area + return Math.max(biggestCorner, Theme.horizontalPageMargin) + } + rightMargin: leftMargin states: [ State { @@ -105,7 +120,8 @@ RemorseBase { PropertyChanges { target: remorsePopup visible: true - y: Theme.paddingMedium + y: (_page && _page.orientation == Orientation.Portrait ? Screen.topCutout.height : 0) + + Theme.paddingMedium _contentOpacity: 1 } }, diff --git a/usr/lib/qt5/qml/Sailfish/Silica/RemoveAnimation.qml b/usr/lib/qt5/qml/Sailfish/Silica/RemoveAnimation.qml index 49438a88..fff671de 100644 --- a/usr/lib/qt5/qml/Sailfish/Silica/RemoveAnimation.qml +++ b/usr/lib/qt5/qml/Sailfish/Silica/RemoveAnimation.qml @@ -42,7 +42,7 @@ SequentialAnimation { function _delayRemove(delay) { if (target.ListView.view) { target.ListView.delayRemove = delay - } else if (target.GridView.view){ + } else if (target.GridView.view) { target.GridView.delayRemove = delay } } diff --git a/usr/lib/qt5/qml/Sailfish/Silica/SilicaFlickable.qml b/usr/lib/qt5/qml/Sailfish/Silica/SilicaFlickable.qml index b1221a44..c9c67eb1 100644 --- a/usr/lib/qt5/qml/Sailfish/Silica/SilicaFlickable.qml +++ b/usr/lib/qt5/qml/Sailfish/Silica/SilicaFlickable.qml @@ -40,8 +40,6 @@ import "private/FastScrollAnimation.js" as FastScroll Flickable { id: flick - // API same as in SilicaWebView see also that. - // Property quickScrollEnabled deprecated. Use quickScroll instead. property alias quickScrollEnabled: quickScrollItem.quickScroll property alias quickScroll: quickScrollItem.quickScroll diff --git a/usr/lib/qt5/qml/Sailfish/Silica/SilicaGridView.qml b/usr/lib/qt5/qml/Sailfish/Silica/SilicaGridView.qml index 152ca031..9b9e0340 100644 --- a/usr/lib/qt5/qml/Sailfish/Silica/SilicaGridView.qml +++ b/usr/lib/qt5/qml/Sailfish/Silica/SilicaGridView.qml @@ -55,7 +55,9 @@ GridView { property Item __silica_contextmenu_instance property Item __silica_remorse_item: null - property real __silica_menu_height: Math.max(__silica_contextmenu_instance ? __silica_contextmenu_instance.height : 0, __silica_remorse_height) + property real __silica_menu_height: Math.max(__silica_contextmenu_instance + ? __silica_contextmenu_instance.height : 0, + __silica_remorse_height) property real __silica_remorse_height NumberAnimation { @@ -87,7 +89,8 @@ GridView { flickDeceleration: Theme.flickDeceleration maximumFlickVelocity: Theme.maximumFlickVelocity cacheBuffer: Theme.itemSizeMedium * 8 - boundsBehavior: (pullDownMenu && pullDownMenu._activationPermitted) || (pushUpMenu && pushUpMenu._activationPermitted) ? Flickable.DragOverBounds : Flickable.StopAtBounds + boundsBehavior: (pullDownMenu && pullDownMenu._activationPermitted) || (pushUpMenu && pushUpMenu._activationPermitted) + ? Flickable.DragOverBounds : Flickable.StopAtBounds BoundsBehavior { flickable: gridView } QuickScroll { diff --git a/usr/lib/qt5/qml/Sailfish/Silica/SilicaWebView.qml b/usr/lib/qt5/qml/Sailfish/Silica/SilicaWebView.qml deleted file mode 100644 index bbba8b82..00000000 --- a/usr/lib/qt5/qml/Sailfish/Silica/SilicaWebView.qml +++ /dev/null @@ -1,187 +0,0 @@ -/**************************************************************************************** -** -** Copyright (C) 2013 Jolla Ltd. -** All rights reserved. -** -** This file is part of Sailfish Silica UI component package. -** -** You may use this file under the terms of BSD license as follows: -** -** Redistribution and use in source and binary forms, with or without -** modification, are permitted provided that the following conditions are met: -** * Redistributions of source code must retain the above copyright -** notice, this list of conditions and the following disclaimer. -** * Redistributions in binary form must reproduce the above copyright -** notice, this list of conditions and the following disclaimer in the -** documentation and/or other materials provided with the distribution. -** * Neither the name of the Jolla Ltd nor the -** names of its contributors may be used to endorse or promote products -** derived from this software without specific prior written permission. -** -** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -** ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -** WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -** DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR -** ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -** (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -** LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND -** ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -** SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -** -****************************************************************************************/ - -import QtQuick 2.0 -import QtQuick.Window 2.0 -import QtWebKit 3.0 -import QtWebKit.experimental 1.0 -import Sailfish.Silica 1.0 -import Sailfish.Silica.private 1.0 -import "private" -import "private/FastScrollAnimation.js" as FastScroll -import "private/Util.js" as Utils - -WebView { - id: webView - - // Property quickScrollEnabled deprecated. Use quickScroll instead. - property alias quickScrollEnabled: quickScrollItem.quickScroll - property alias quickScroll: quickScrollItem.quickScroll - property alias quickScrollAnimating: quickScrollItem.quickScrollAnimating - property Item pullDownMenu - property Item pushUpMenu - readonly property bool pulleyMenuActive: pullDownMenu != null && pullDownMenu.active || pushUpMenu != null && pushUpMenu.active - property bool overridePageStackNavigation - property QtObject _scrollAnimation - property bool _pulleyDimmerActive: pullDownMenu && pullDownMenu._activeDimmer || pushUpMenu && pushUpMenu._activeDimmer - - // SilicaWebView extras - property Component header - property Item _headerItem - property Page _page - property bool _cookiesEnabled: true - - // Some components (currently libjollasignonui) may want to turn off - // focus animation completely - property bool _allowFocusAnimation: true - - // Part of experimental API - property Item _webPage: webView.experimental.page - - // For performance reasons we turn off WebView's automatic input field - // repositioning & scaling feature by setting experimental.enableInputFieldAnimation - // to false and manually trigger repositioning after the virtual keyboard - // animation is over. - VirtualKeyboardObserver { - id: vkbObserver - active: webView.visible - orientation: pageStack.currentPage.orientation - - onOpenedChanged: { - if (opened) { - if (webView.focus && webView._allowFocusAnimation) { - experimental.animateInputFieldVisible() - } - } - } - } - - function scrollToTop() { - FastScroll.scrollToTop(webView, quickScrollItem) - } - function scrollToBottom() { - FastScroll.scrollToBottom(webView, quickScrollItem) - } - - flickDeceleration: Theme.flickDeceleration - maximumFlickVelocity: Theme.maximumFlickVelocity - onHeaderChanged: webView.experimental.header = header - experimental.onHeaderItemChanged: { - _headerItem = webView.experimental.headerItem - if (_headerItem) { - _headerItem.parent = headerContent - } - } - - boundsBehavior: pageStack._leftFlickDifference == 0 && pageStack._rightFlickDifference == 0 - && ((pullDownMenu && pullDownMenu._activationPermitted) || (pushUpMenu && pushUpMenu._activationPermitted)) ? Flickable.DragOverBounds : Flickable.StopAtBounds - - // Experimental API usage - experimental.useDefaultContentItemSize: false - - // Column handles height of web content and width read from web page - // For still unknown reason pulley menu cannot be opened when contentHeight == height - // Due to Bug #7857, cleanup + 1px when bug is fixed - contentHeight: contentColumn.height + 1 - contentWidth: Math.floor(Math.max(webView.width, _webPage.width)) - - experimental.preferredMinimumContentsWidth: Screen.width - experimental.deviceWidth: Screen.width - experimental.deviceHeight: Screen.height - experimental.preferences.cookiesEnabled: _cookiesEnabled - experimental.enableInputFieldAnimation: false - experimental.enableResizeContent: !vkbObserver.animating - - TouchBlocker { - target: pageStack._leftFlickDifference != 0 || pageStack._rightFlickDifference != 0 ? webView : null - } - - // Binding contentWidth: Math.max(webView.width, _webPage.width) doesn't work. - // So, break intial bindings when geometry of web page changes. - Connections { - target: _webPage - onWidthChanged: contentWidth = Math.floor(Math.max(webView.width, _webPage.width)) - } - - Rectangle { - x: webView.contentX - y: _headerItem ? _headerItem.height : 0 - - width: webView.contentWidth - height: Math.max(webView.contentHeight, _page.height) - y - color: webView.experimental.transparentBackground ? "transparent" : "white" - } - - Column { - id : contentColumn - width: _webPage ? Math.floor(Math.max(webView.width, _webPage.width)) : webView.width - objectName: "contentColumn" - - Item { - id: headerContent - x: webView.contentX - width: webView.width - height: childrenRect.height - } - } - - BoundsBehavior { flickable: webView } - QuickScroll { - id: quickScrollItem - flickable: webView - } - - states: State { - name: "active" - when: !overridePageStackNavigation && _page != null && webView.visible - PropertyChanges { - target: pageStack - _noGrabbing: webView.moving || webView.experimental.pinching - } - PropertyChanges { - target: _page - backNavigation: webView.contentX <= Theme.paddingMedium && !webView.pulleyMenuActive && !webView.experimental.pinching - forwardNavigation: _page._belowTop && webView.contentX >= webView.contentWidth - webView.width - Theme.paddingMedium - && !webView.pulleyMenuActive && !webView.experimental.pinching - } - } - - Component.onCompleted: { - _webPage.parent = contentColumn - _page = Utils.findPage(webView) - if (!_page) { - console.log("No parent Page found. A SilicaWebView should be declared inside a Page, \ - as SilicaWebView overrides back and forward navigation bindings defined in Page.") - } - } -} diff --git a/usr/lib/qt5/qml/Sailfish/Silica/SlideshowView.qml b/usr/lib/qt5/qml/Sailfish/Silica/SlideshowView.qml index 66916510..ae368baa 100644 --- a/usr/lib/qt5/qml/Sailfish/Silica/SlideshowView.qml +++ b/usr/lib/qt5/qml/Sailfish/Silica/SlideshowView.qml @@ -76,14 +76,14 @@ PathView { path: Path { id: path startX: orientation === Qt.Horizontal ? -(view.itemWidth * view._multiplier - view.width/2) - : view.itemWidth / 2 + : view.itemWidth / 2 startY: orientation === Qt.Horizontal ? view.itemHeight / 2 : -(view.itemHeight * view._multiplier - view.height/2) PathLine { x: orientation === Qt.Horizontal ? (view.pathItemCount * view.itemWidth) + path.startX : view.itemWidth / 2 - y: orientation === Qt.Horizontal ? view.itemHeight / 2 + y: orientation === Qt.Horizontal ? view.itemHeight / 2 : (view.pathItemCount * view.itemHeight) + path.startY } } diff --git a/usr/lib/qt5/qml/Sailfish/Silica/TextEditorLabel.qml b/usr/lib/qt5/qml/Sailfish/Silica/TextEditorLabel.qml index 7b649d24..a06d382b 100644 --- a/usr/lib/qt5/qml/Sailfish/Silica/TextEditorLabel.qml +++ b/usr/lib/qt5/qml/Sailfish/Silica/TextEditorLabel.qml @@ -9,9 +9,9 @@ Label { text: editor ? editor.label : "" font.pixelSize: Theme.fontSizeSmall truncationMode: TruncationMode.Fade - color: editor.errorHighlight ? palette.errorColor - : highlighted ? palette.secondaryHighlightColor - : palette.secondaryColor + color: editor && editor.errorHighlight ? palette.errorColor + : highlighted ? palette.secondaryHighlightColor + : palette.secondaryColor horizontalAlignment: editor && editor.explicitHorizontalAlignment ? editor.horizontalAlignment : undefined opacity: editor && (editor._isEmpty && editor.hideLabelOnEmptyField) ? 0.0 : 1.0 diff --git a/usr/lib/qt5/qml/Sailfish/Silica/TimePicker.qml b/usr/lib/qt5/qml/Sailfish/Silica/TimePicker.qml index ad9ec2b9..9ca57ee6 100644 --- a/usr/lib/qt5/qml/Sailfish/Silica/TimePicker.qml +++ b/usr/lib/qt5/qml/Sailfish/Silica/TimePicker.qml @@ -100,11 +100,12 @@ SilicaItem { function _updateMinuteIndicator() { if (mouse.changingProperty == 0) { + var delta if (_hoursAndMinutes) { - var delta = (minute - outerIndicator.value) + delta = (minute - outerIndicator.value) outerIndicator.value += (delta % 60) } else { - var delta = (minute - innerIndicator.value) + delta = (minute - innerIndicator.value) innerIndicator.value += (delta % 60) } } @@ -118,19 +119,20 @@ SilicaItem { } function _formatTime(hour, minute, _second) { - var date = new Date() - date.setSeconds(_second) - date.setHours(hour) - date.setMinutes(minute) - var format if (_hoursAndMinutes) { + var date = new Date() + date.setSeconds(_second) + date.setHours(hour) + date.setMinutes(minute) + var format format = (hourMode == DateTime.DefaultHours ? Formatter.TimeValue : (hourMode == DateTime.TwentyFourHours ? Formatter.TimeValueTwentyFourHours : Formatter.TimeValueTwelveHours)) + return Format.formatDate(date, format) } else { - format = Formatter.DurationShort + // minutes and seconds mode + return Format.formatDuration(minute * 60 + _second) } - return Format.formatDate(date, format) } ShaderEffect { diff --git a/usr/lib/qt5/qml/Sailfish/Silica/ViewPlaceholder.qml b/usr/lib/qt5/qml/Sailfish/Silica/ViewPlaceholder.qml index 8fdbfaeb..ecf4ee0a 100644 --- a/usr/lib/qt5/qml/Sailfish/Silica/ViewPlaceholder.qml +++ b/usr/lib/qt5/qml/Sailfish/Silica/ViewPlaceholder.qml @@ -37,6 +37,7 @@ import "private/Util.js" as Util SilicaItem { id: placeholder + property Item flickable property alias text: mainLabel.text property alias textFormat: mainLabel.textFormat @@ -70,6 +71,7 @@ SilicaItem { InfoLabel { id: mainLabel } Text { id: hintLabel + x: leftMargin anchors.top: mainLabel.bottom width: parent.width - parent.leftMargin - parent.rightMargin @@ -87,6 +89,7 @@ SilicaItem { Component { // content we don't need until we're active id: activeContent + PulleyAnimationHint { flickable: placeholder.flickable width: parent.width - 2 * Theme.paddingLarge diff --git a/usr/lib/qt5/qml/Sailfish/Silica/private/BoundsBehavior.qml b/usr/lib/qt5/qml/Sailfish/Silica/private/BoundsBehavior.qml index e155fa69..6215ad60 100644 --- a/usr/lib/qt5/qml/Sailfish/Silica/private/BoundsBehavior.qml +++ b/usr/lib/qt5/qml/Sailfish/Silica/private/BoundsBehavior.qml @@ -44,7 +44,10 @@ BounceEffect { when: bounceEffect.active && bounceEffect.flickable.interactive target: bounceEffect.flickable && bounceEffect.flickable.contentItem property: "opacity" - value: Theme.opacityLow + (1.0 - Theme.opacityLow)*Math.pow((1.0 - Math.min(bounceEffect.difference, Theme.itemSizeExtraLarge*2)/(Theme.itemSizeExtraLarge*2)), 1.5) + value: Theme.opacityLow + + (1.0 - Theme.opacityLow) + * Math.pow((1.0 - Math.min(bounceEffect.difference, Theme.itemSizeExtraLarge*2) / (Theme.itemSizeExtraLarge*2)), + 1.5) } FadeAnimation { id: fadeInAnimation diff --git a/usr/lib/qt5/qml/Sailfish/Silica/private/ClockItem.qml b/usr/lib/qt5/qml/Sailfish/Silica/private/ClockItem.qml index 906364da..f33a7de9 100644 --- a/usr/lib/qt5/qml/Sailfish/Silica/private/ClockItem.qml +++ b/usr/lib/qt5/qml/Sailfish/Silica/private/ClockItem.qml @@ -4,6 +4,7 @@ import Nemo.Configuration 1.0 Row { id: clock + //: "translate as non-empty if am/pm indicator starts the 12h time pattern" //% "" property string startWithAp: qsTrId("components-la-time_start_with_ap") @@ -11,8 +12,8 @@ Row { property int hourMode: timeFormatConfig.value === "24" ? DateTime.TwentyFourHours : DateTime.TwelveHours - layoutDirection: (startWithAp !== "" && startWithAp !== "components-la-time_start_with_ap") ? Qt.RightToLeft - : Qt.LeftToRight + layoutDirection: (startWithAp !== "" && startWithAp !== "components-la-time_start_with_ap") + ? Qt.RightToLeft : Qt.LeftToRight Label { id: timeText diff --git a/usr/lib/qt5/qml/Sailfish/Silica/private/IconGridViewBase.qml b/usr/lib/qt5/qml/Sailfish/Silica/private/IconGridViewBase.qml index e3621697..8add8678 100644 --- a/usr/lib/qt5/qml/Sailfish/Silica/private/IconGridViewBase.qml +++ b/usr/lib/qt5/qml/Sailfish/Silica/private/IconGridViewBase.qml @@ -1,15 +1,28 @@ import QtQuick 2.4 import Sailfish.Silica 1.0 import "Util.js" as Util +import Nemo.Configuration 1.0 SilicaGridView { id: root property int pageHeight: height - property int horizontalMargin: largeScreen ? (fullHdPortraitWidth ? 3 : 6) * Theme.paddingLarge : Theme._homePageMargin + property int horizontalMargin: { + var margin = configs.launcher_horizontal_margin + if (margin) { + return margin + } else { + return Math.max((!isPortrait && Screen.topCutout.height > 0) + ? (Screen.topCutout.height + Theme.paddingSmall) : 0, + largeScreen ? (fullHdPortraitWidth ? 3 : 6) * Theme.paddingLarge + : (Theme.paddingLarge + Theme.paddingSmall)) + } + } property int launcherItemSpacing: Theme.paddingSmall property real minimumDelegateSize: Theme.iconSizeLauncher - property bool isPortrait: !_page || _page.isPortrait + property bool isPortrait: orientation === Orientation.Portrait + || orientation === Orientation.PortraitInverted + property int orientation: _page ? _page.orientation : Orientation.Portrait property Item _page: Util.findPage(root) // For wider than 16:9 full hd @@ -27,8 +40,26 @@ SilicaGridView { + launcherLabelMetrics.height + launcherItemSpacing property alias launcherLabelFontSize: launcherLabelMetrics.font.pixelSize - property int rows: Math.max(isPortrait ? 6 : 3, Math.floor(pageHeight / minimumCellHeight)) - property int columns: Math.max(isPortrait ? 4 : 6, Math.floor(parent.width / minimumCellWidth)) + property int rows: { + var rows = isPortrait ? configs.launcher_rows_portrait + : configs.launcher_rows_landscape + + if (rows > 0) { + return rows + } else { + return Math.max(isPortrait ? 6 : 3, Math.floor(pageHeight / minimumCellHeight)) + } + } + + property int columns: { + var columns = isPortrait ? configs.launcher_columns_portrait + : configs.launcher_columns_landscape + if (columns > 0) { + return columns + } else { + return Math.max(isPortrait ? 4 : 6, Math.floor(parent.width / minimumCellWidth)) + } + } property int initialCellWidth: (parent.width - 2*horizontalMargin) / columns readonly property bool largeScreen: Screen.sizeCategory >= Screen.Large @@ -43,4 +74,15 @@ SilicaGridView { id: launcherLabelMetrics font.pixelSize: Theme.fontSizeTiny } + + ConfigurationGroup { + id: configs + + path: "/desktop/sailfish/experimental" + property int launcher_horizontal_margin + property int launcher_rows_portrait + property int launcher_rows_landscape + property int launcher_columns_portrait + property int launcher_columns_landscape + } } diff --git a/usr/lib/qt5/qml/Sailfish/Silica/private/PulleyMenuBase.qml b/usr/lib/qt5/qml/Sailfish/Silica/private/PulleyMenuBase.qml index 6580e972..71205ac7 100644 --- a/usr/lib/qt5/qml/Sailfish/Silica/private/PulleyMenuBase.qml +++ b/usr/lib/qt5/qml/Sailfish/Silica/private/PulleyMenuBase.qml @@ -107,10 +107,12 @@ SilicaMouseArea { property real _finalPosition // The position where the menu is at the limit of its extent property bool _atInitialPosition: Math.abs(flickable.contentY - _inactivePosition) < 1.0 && !active property bool _atFinalPosition: Math.abs(flickable.contentY - _finalPosition) < 1.0 && active - property bool _pullDown: _inactivePosition > _finalPosition + property real _contentEnd property real _menuIndicatorPosition // The position of the highlight when the menu is closed property real _menuItemHeight: screen.sizeCategory <= Screen.Medium ? Theme.itemSizeExtraSmall : Theme.itemSizeSmall - + property real _menuItemActivationThreshold: _menuItemHeight + + ((_isPullDownMenu && Screen.hasCutouts && _page && _page.isPortrait) + ? Screen.topCutout.height : 0) property bool _activationInhibited property bool _activationPermitted: visible && enabled && _atInitialPosition && !_activationInhibited @@ -148,7 +150,9 @@ SilicaMouseArea { z: 10000 // we want the menu indicator and its dimmer to appear above content x: flickable.contentX + (flickable.width - width)/2 - width: flickable.width ? Math.min(flickable.width, screen.sizeCategory > Screen.Medium ? Screen.width*0.7 : Screen.width) : Screen.width + width: flickable.width ? Math.min(flickable.width, + screen.sizeCategory > Screen.Medium ? Screen.width*0.7 : Screen.width) + : Screen.width height: _activeHeight + spacing layer.enabled: active || (flickable.dragging && __silica_applicationwindow_instance._dimmingActive) @@ -323,8 +327,8 @@ SilicaMouseArea { if (child) { _quickSelected = true var xPos = width/2 - if ((_pullDown && parentItem.mapToItem(child, xPos, yPos).y <= _menuItemHeight) - || (!_pullDown && parentItem.mapToItem(child, xPos, yPos).y >= 0)) { + if ((_isPullDownMenu && parentItem.mapToItem(child, xPos, yPos).y <= _menuItemHeight) + || (!_isPullDownMenu && parentItem.mapToItem(child, xPos, yPos).y >= 0)) { if (flickable.dragging) { menuItem = child } @@ -347,7 +351,7 @@ SilicaMouseArea { return } - var xPos = width/2 + var xPos = width / 2 // Only try to highlight if we haven't dragged to the final position if (!flickable.dragging || !_atFinalPosition) { @@ -366,11 +370,7 @@ SilicaMouseArea { } if (!child) { menuItem = null - var wasHighlighted = !!highlightItem.highlightedItem highlightItem.clearHighlight() - if (logic.dragDistance <= _contentEnd && wasHighlighted) { - highlightItem.moveTo(_highlightIndicatorPosition) - } } } @@ -438,9 +438,14 @@ SilicaMouseArea { id: highlightItem y: { - if (!active) return _menuIndicatorPosition - if (highlightedItem || (!flickable.dragging && _atFinalPosition) - || logic.dragDistance > _contentEnd) return _highlightedItemPosition + if (!active) { + return _menuIndicatorPosition + } + + if (highlightedItem + || (!flickable.dragging && _atFinalPosition)) { + return _highlightedItemPosition + } return _highlightIndicatorPosition } @@ -456,10 +461,16 @@ SilicaMouseArea { } else if ((!active && !_hinting) || _bounceBackRunning) { return _inactiveOpacity } else if (!_hasMenuItems(_contentColumn)) { - return Theme.highlightBackgroundOpacity * (1.0 - logic.dragDistance/Theme.paddingMedium) + return Theme.highlightBackgroundOpacity * (1.0 - logic.dragDistance / Theme.paddingMedium) } else { - return Theme.highlightBackgroundOpacity * Math.max(1.5 - logic.dragDistance/_menuItemHeight, - logic.dragDistance <= _contentEnd && !flickAnimation.running ? 0.5 : 0.0) + // opacity on starts with 1.5 multiplier (could use something cleaner?), + // goes downwards with drag until lower part takes over, + // finally ensuring item hidden when dragged beyond the menu items + return Theme.highlightBackgroundOpacity + * Math.max(1.5 - logic.dragDistance / _menuItemHeight, + (logic.dragDistance <= (_contentEnd + _menuItemActivationThreshold) + && !flickAnimation.running) + ? 0.5 : 0.0) } } @@ -467,6 +478,7 @@ SilicaMouseArea { Timer { id: busyTimer + running: busy && !active && Qt.application.active interval: 500 repeat: true @@ -617,7 +629,7 @@ SilicaMouseArea { // Do not permit flicking inside the menu (unless it is a small flick that does not present // a danger of accidentally selecting the wrong item) if (active && !_quickSelected && (Math.abs(flickable.verticalVelocity) > Theme.dp(500))) { - var opening = _pullDown ? flickable.verticalVelocity < 0 : flickable.verticalVelocity > 0 + var opening = _isPullDownMenu ? flickable.verticalVelocity < 0 : flickable.verticalVelocity > 0 flickAnimation.to = opening ? _finalPosition : _inactivePosition flickAnimation.duration = 300 flickAnimation.restart() @@ -674,6 +686,7 @@ SilicaMouseArea { PulleyMenuLogic { id: logic + flickable: pulleyBase.flickable onFinalPositionReached: { if (active && _ngfEffect && !menuItem && !quickSelect && !delayedBounceTimer.running && !bounceBackAnimation.running) { @@ -706,8 +719,8 @@ SilicaMouseArea { } else if (flickable.height < flickable.contentHeight - _snapThreshold) { // If we are close to the menu location, snap to the end var dist = flickable.contentY - _inactivePosition - if (_pullDown && dist > 0 && dist < _snapThreshold - || !_pullDown && dist < 0 && dist > -_snapThreshold) { + if (_isPullDownMenu && dist > 0 && dist < _snapThreshold + || !_isPullDownMenu && dist < 0 && dist > -_snapThreshold) { snapAnimation.restart() } } @@ -776,7 +789,7 @@ SilicaMouseArea { Component.onCompleted: { // avoid hard dependency to ngf module - _ngfEffect = Qt.createQmlObject("import org.nemomobile.ngf 1.0; NonGraphicalFeedback { event: 'pulldown_lock' }", + _ngfEffect = Qt.createQmlObject("import Nemo.Ngf 1.0; NonGraphicalFeedback { event: 'pulldown_lock' }", highlightItem, 'NonGraphicalFeedback') } diff --git a/usr/lib/qt5/qml/Sailfish/Silica/private/RemorseBase.qml b/usr/lib/qt5/qml/Sailfish/Silica/private/RemorseBase.qml index ac01f956..79446d39 100644 --- a/usr/lib/qt5/qml/Sailfish/Silica/private/RemorseBase.qml +++ b/usr/lib/qt5/qml/Sailfish/Silica/private/RemorseBase.qml @@ -46,7 +46,7 @@ SwipeItem { //% "Undo" property string cancelText: fontMetrics.advanceWidth(_cancelTextFull) > labels.width ? qsTrId("components-la-undo") - : _cancelTextFull + : _cancelTextFull //% "Tap to undo" property string _cancelTextFull: qsTrId("components-la-tap-to-undo") diff --git a/usr/lib/qt5/qml/Sailfish/Silica/private/Scrollbar.qml b/usr/lib/qt5/qml/Sailfish/Silica/private/Scrollbar.qml index 10528fb2..989bcd31 100644 --- a/usr/lib/qt5/qml/Sailfish/Silica/private/Scrollbar.qml +++ b/usr/lib/qt5/qml/Sailfish/Silica/private/Scrollbar.qml @@ -57,7 +57,7 @@ VerticalScrollBase { _range: flickable.contentHeight + _topMenuSpacing + _bottomMenuSpacing - parent.height ColorBackground { - property real contrast: scrollbar.highlighted ? 1.65 : 1.35 + property real contrast: scrollbar.highlighted ? 1.65 : 1.35 anchors.fill: parent radius: Theme.paddingMedium diff --git a/usr/lib/qt5/qml/Sailfish/Silica/private/Slideable.qml b/usr/lib/qt5/qml/Sailfish/Silica/private/Slideable.qml index bdc361fb..23c69d14 100644 --- a/usr/lib/qt5/qml/Sailfish/Silica/private/Slideable.qml +++ b/usr/lib/qt5/qml/Sailfish/Silica/private/Slideable.qml @@ -146,7 +146,7 @@ Private.SlideableBase { createAdjacentItem(currentItem, Private.Slide.Backward) } - if (currentItem.Private.Slide.backward){ + if (currentItem.Private.Slide.backward) { _alternateItem = currentItem.Private.Slide.backward _anchorToBackwardSide(_alternateItem.anchors, currentItem) _alternateItem.visible = true diff --git a/usr/lib/qt5/qml/Sailfish/Silica/private/TabView.qml b/usr/lib/qt5/qml/Sailfish/Silica/private/TabView.qml index 32c955ef..535b0eeb 100644 --- a/usr/lib/qt5/qml/Sailfish/Silica/private/TabView.qml +++ b/usr/lib/qt5/qml/Sailfish/Silica/private/TabView.qml @@ -130,9 +130,10 @@ PagedView { BusyIndicator { running: !delayBusy.running && loading - x: (tabLoader.width - width) / 2 + // Avoid flicker when tab container gets repositioned + parent: tabLoader.parent + x: (tabLoader.width - width) / 2 + tabLoader.x y: root.height/3 - height/2 - tabBarLoader.height - size: BusyIndicatorSize.Large Timer { diff --git a/usr/lib/qt5/qml/Sailfish/Silica/private/TextBaseExtensionContainer.qml b/usr/lib/qt5/qml/Sailfish/Silica/private/TextBaseExtensionContainer.qml index 92ce36a5..65439c27 100644 --- a/usr/lib/qt5/qml/Sailfish/Silica/private/TextBaseExtensionContainer.qml +++ b/usr/lib/qt5/qml/Sailfish/Silica/private/TextBaseExtensionContainer.qml @@ -38,20 +38,20 @@ import Sailfish.Silica 1.0 Item { id: root - property bool active: !!item && item.width > 0 && item.height > 0 && item.opacity > 0.0 + property bool active: width > 0 property Item item property Item _oldItem: item onItemChanged: { - if (_oldItem) _oldItem.parent = null + if (_oldItem) { + _oldItem.parent = null + } if (item) { - item.parent = Qt.binding(function () { - return root.active ? root : null - }) + item.parent = root } _oldItem = item } - width: item ? item.width : 0 - height: item ? item.height : 0 + width: item && item.visible && item.opacity > 0 ? item.width : 0 + height: item && item.visible && item.opacity > 0 ? item.height : 0 } diff --git a/usr/lib/qt5/qml/Sailfish/Silica/private/VerticalScrollBase.qml b/usr/lib/qt5/qml/Sailfish/Silica/private/VerticalScrollBase.qml index 4a62a9be..23a444c2 100644 --- a/usr/lib/qt5/qml/Sailfish/Silica/private/VerticalScrollBase.qml +++ b/usr/lib/qt5/qml/Sailfish/Silica/private/VerticalScrollBase.qml @@ -63,9 +63,10 @@ SilicaItem { opacity: (timer.moving && _inBounds) || timer.running || highlighted ? 1.0 : 0.0 visible: flickable.contentHeight > flickable.height Behavior on opacity { FadeAnimation { duration: 400 } } - y: Math.max(margin, Math.min( - (parent.height / flickable.height) * (_headerSpacing + (flickable.contentY - flickable.originY + _topMenuSpacing) * _sizeRatio), - (parent.height - height - margin))) + y: Math.max(margin, + Math.min((parent.height / flickable.height) + * (_headerSpacing + (flickable.contentY - flickable.originY + _topMenuSpacing) * _sizeRatio), + (parent.height - height - margin))) Component.onCompleted: { if (!flickable) { diff --git a/usr/lib/qt5/qml/Sailfish/Silica/private/qmldir b/usr/lib/qt5/qml/Sailfish/Silica/private/qmldir index ed9a918c..8eebbdab 100644 --- a/usr/lib/qt5/qml/Sailfish/Silica/private/qmldir +++ b/usr/lib/qt5/qml/Sailfish/Silica/private/qmldir @@ -31,3 +31,4 @@ WindowGestureOverride 1.0 WindowGestureOverride.qml ZoomableFlickable 1.0 ZoomableFlickable.qml PageHeaderMouseArea 1.0 PageHeaderMouseArea.qml ButtonBorderColors 1.0 ButtonBorderColors.qml +YearMonthMenu 1.0 YearMonthMenu.qml \ No newline at end of file diff --git a/usr/lib/qt5/qml/Sailfish/Silica/qmldir b/usr/lib/qt5/qml/Sailfish/Silica/qmldir index 7d052ad1..8e72456e 100644 --- a/usr/lib/qt5/qml/Sailfish/Silica/qmldir +++ b/usr/lib/qt5/qml/Sailfish/Silica/qmldir @@ -34,7 +34,6 @@ InteractionHintLabel 1.0 InteractionHintLabel.qml SilicaListView 1.0 SilicaListView.qml SilicaGridView 1.0 SilicaGridView.qml SilicaFlickable 1.0 SilicaFlickable.qml -SilicaWebView 1.0 SilicaWebView.qml TextEditorLabel 1.0 TextEditorLabel.qml Label 1.0 Label.qml LinkedLabel 1.0 LinkedLabel.qml diff --git a/usr/lib/qt5/qml/Sailfish/Telephony/SimErrorState.qml b/usr/lib/qt5/qml/Sailfish/Telephony/SimErrorState.qml index 965e0929..b04b058b 100644 --- a/usr/lib/qt5/qml/Sailfish/Telephony/SimErrorState.qml +++ b/usr/lib/qt5/qml/Sailfish/Telephony/SimErrorState.qml @@ -1,7 +1,10 @@ import QtQml 2.2 import Sailfish.Telephony 1.0 -import MeeGo.QOfono 0.2 +import QOfono 0.2 +/*! + \inqmlmodule Sailfish.Telephony +*/ QtObject { id: root diff --git a/usr/lib/qt5/qml/Sailfish/Telephony/SimManager.qml b/usr/lib/qt5/qml/Sailfish/Telephony/SimManager.qml index da765453..c30880ba 100644 --- a/usr/lib/qt5/qml/Sailfish/Telephony/SimManager.qml +++ b/usr/lib/qt5/qml/Sailfish/Telephony/SimManager.qml @@ -1,9 +1,12 @@ import QtQuick 2.2 import Sailfish.Telephony 1.0 -import MeeGo.QOfono 0.2 +import QOfono 0.2 import org.nemomobile.ofono 1.0 -import org.nemomobile.dbus 2.0 +import Nemo.DBus 2.0 +/*! + \inqmlmodule Sailfish.Telephony +*/ Item { readonly property alias valid: modemManager.valid readonly property alias ready: modemManager.ready @@ -31,10 +34,12 @@ Item { property string simDescriptionSeparator: " | " property alias availableSimCount: simListModel.count // no. of SIMs that are not currently locked or otherwise unavailable - // The control type of the SimManager - // SimManagerType.Auto - default both voice and data SIM / modem - // SimManagerType.Voice - controlling voice SIM / modem (takes care of SMSes as well) - // SimManagerType.Data - controlling data SIM / modem + /*! + The control type of the SimManager + \value SimManagerType.Auto default both voice and data SIM / modem + \value SimManagerType.Voice controlling voice SIM / modem (takes care of SMSes as well) + \value SimManagerType.Data controlling data SIM / modem + */ property int controlType: SimManagerType.Auto property alias presentModemCount: modemManager.presentSimCount @@ -128,6 +133,9 @@ Item { return -1 } + /*! + \internal + */ function _updateSimData() { var names = [] var activeModems = [] diff --git a/usr/lib/qt5/qml/Sailfish/Telephony/SimPicker.qml b/usr/lib/qt5/qml/Sailfish/Telephony/SimPicker.qml index 07bfe315..999a6c6b 100644 --- a/usr/lib/qt5/qml/Sailfish/Telephony/SimPicker.qml +++ b/usr/lib/qt5/qml/Sailfish/Telephony/SimPicker.qml @@ -5,6 +5,9 @@ import Sailfish.Telephony 1.0 // TODO: replace with standard component +/*! + \inqmlmodule Sailfish.Telephony +*/ MouseArea { id: simPicker diff --git a/usr/lib/qt5/qml/Sailfish/Telephony/SimPickerMenuItem.qml b/usr/lib/qt5/qml/Sailfish/Telephony/SimPickerMenuItem.qml index 551aa4d6..4a7c2f0a 100644 --- a/usr/lib/qt5/qml/Sailfish/Telephony/SimPickerMenuItem.qml +++ b/usr/lib/qt5/qml/Sailfish/Telephony/SimPickerMenuItem.qml @@ -1,23 +1,27 @@ import QtQuick 2.1 import Sailfish.Silica 1.0 -// Replaces the content of a ContextMenu with the SimPicker -// Usage: -// -// ContextMenu { -// id: contextMenu -// SimPickerMenuItem { -// id: simSelector -// menu: contextMenu -// Behavior on opacity { FadeAnimation {} } -// onSimSelected: dial(remoteUid, sim) -// } -// MenuItem { -// text: "Call" -// onClicked: simSelector.active = true -// } -// } -// +/*! + \brief Replaces the content of a ContextMenu with the SimPicker + \inqmlmodule Sailfish.Telephony + + Usage: + \qml + ContextMenu { + id: contextMenu + SimPickerMenuItem { + id: simSelector + menu: contextMenu + Behavior on opacity { FadeAnimation {} } + onSimSelected: dial(remoteUid, sim) + } + MenuItem { + text: "Call" + onClicked: simSelector.active = true + } + } + \endqml +*/ SimPicker { id: simSelector diff --git a/usr/lib/qt5/qml/Sailfish/Telephony/SimSelector.qml b/usr/lib/qt5/qml/Sailfish/Telephony/SimSelector.qml index 5a13dfb9..7d11835c 100644 --- a/usr/lib/qt5/qml/Sailfish/Telephony/SimSelector.qml +++ b/usr/lib/qt5/qml/Sailfish/Telephony/SimSelector.qml @@ -1,14 +1,19 @@ import QtQuick 2.2 import Sailfish.Silica 1.0 import Sailfish.Telephony 1.0 -import org.nemomobile.dbus 2.0 +import Nemo.DBus 2.0 +/*! + \inqmlmodule Sailfish.Telephony +*/ SimSelectorBase { id: root property bool updateSelectedSim: true property bool restrictToActive - // A margin that is applied between sim indicators + /*! + A margin that is applied between sim indicators + */ property int innerMargin: Theme.paddingLarge * 2 signal simSelected(int sim, string modemPath) diff --git a/usr/lib/qt5/qml/Sailfish/Telephony/SimSelectorBase.qml b/usr/lib/qt5/qml/Sailfish/Telephony/SimSelectorBase.qml index 8b1d0e21..1cd9f6c9 100644 --- a/usr/lib/qt5/qml/Sailfish/Telephony/SimSelectorBase.qml +++ b/usr/lib/qt5/qml/Sailfish/Telephony/SimSelectorBase.qml @@ -6,6 +6,9 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 +/*! + \inqmlmodule Sailfish.Telephony +*/ Item { id: root diff --git a/usr/lib/qt5/qml/Sailfish/Telephony/qmldir b/usr/lib/qt5/qml/Sailfish/Telephony/qmldir index 58ce4167..dc82fa53 100644 --- a/usr/lib/qt5/qml/Sailfish/Telephony/qmldir +++ b/usr/lib/qt5/qml/Sailfish/Telephony/qmldir @@ -1,5 +1,6 @@ module Sailfish.Telephony plugin sailfishtelephonyplugin +typeinfo plugins.qmltypes SimManager 1.0 SimManager.qml SimErrorState 1.0 SimErrorState.qml SimPicker 1.0 SimPicker.qml diff --git a/usr/lib/qt5/qml/Sailfish/TextLinking/LinkHandler.qml b/usr/lib/qt5/qml/Sailfish/TextLinking/LinkHandler.qml index 3268e83d..8cea336e 100644 --- a/usr/lib/qt5/qml/Sailfish/TextLinking/LinkHandler.qml +++ b/usr/lib/qt5/qml/Sailfish/TextLinking/LinkHandler.qml @@ -34,16 +34,10 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 -import org.nemomobile.contacts 1.0 Item { id: root - Component { - id: personComponent - Person {} - } - function handleLink(link) { if (typeof(link.indexOf) == "undefined") { // Link can be url as well, try to convert to string @@ -68,22 +62,27 @@ Item { return } - if (scheme == "tel" || scheme == "sms" || scheme == "mailto") { - var person = personComponent.createObject(root) - if (scheme == "mailto") { - person.emailDetails = [ { - 'type': Person.EmailAddressType, - 'address': address, - 'index': -1 - } ] + if (scheme === "tel" || scheme === "sms" || scheme === "mailto") { + var personComponent = Qt.createComponent(Qt.resolvedUrl("Person.qml")) + if (personComponent.status === Component.Ready) { + var person = personComponent.createObject(root) + if (scheme === "mailto") { + person.emailDetails = [ { + 'type': person.emailAddressType, + 'address': address, + 'index': -1 + } ] + } else { + person.phoneDetails = [ { + 'type': person.phoneNumberType, + 'number': decodeURIComponent(address), + 'index': -1 + } ] + } + pageStack.animatorPush("Sailfish.Contacts.ContactCardPage", { contact: person }) } else { - person.phoneDetails = [ { - 'type': Person.PhoneNumberType, - 'number': decodeURIComponent(address), - 'index': -1 - } ] + Qt.openUrlExternally(link) } - pageStack.animatorPush("Sailfish.Contacts.ContactCardPage", { contact: person }) } else { Qt.openUrlExternally(link) } diff --git a/usr/lib/qt5/qml/Sailfish/TextLinking/Person.qml b/usr/lib/qt5/qml/Sailfish/TextLinking/Person.qml new file mode 100644 index 00000000..7329ae88 --- /dev/null +++ b/usr/lib/qt5/qml/Sailfish/TextLinking/Person.qml @@ -0,0 +1,40 @@ +/**************************************************************************************** +** +** Copyright (c) 2022 Jolla Ltd. +** All rights reserved. +** +** This file is part of Sailfish text linking component package. +** +** You may use this file under the terms of BSD license as follows: +** +** Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in the +** documentation and/or other materials provided with the distribution. +** * Neither the name of the Jolla Ltd nor the +** names of its contributors may be used to endorse or promote products +** derived from this software without specific prior written permission. +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +** ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +** WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +** DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR +** ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +** (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +** LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +** ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +** SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +** +****************************************************************************************/ + +import QtQuick 2.0 +import org.nemomobile.contacts 1.0 as Contacts + +Contacts.Person { + readonly property int emailAddressType: Contacts.Person.EmailAddressType + readonly property int phoneNumberType: Contacts.Person.PhoneNumberType +} diff --git a/usr/lib/qt5/qml/Sailfish/Timezone/CountryPicker.qml b/usr/lib/qt5/qml/Sailfish/Timezone/CountryPicker.qml index d6d0a6e6..3963e6de 100644 --- a/usr/lib/qt5/qml/Sailfish/Timezone/CountryPicker.qml +++ b/usr/lib/qt5/qml/Sailfish/Timezone/CountryPicker.qml @@ -5,6 +5,7 @@ import Sailfish.Timezone 1.0 Page { id: root + property bool showUndefinedCountry property alias model: view.model signal countryClicked(string countryName, string countryCode) @@ -60,6 +61,23 @@ Page { } } } + + BackgroundItem { + id: undefinedCountryItem + + visible: root.showUndefinedCountry && searchField.text == "" + height: Theme.itemSizeSmall + onClicked: root.countryClicked("", "") + + Label { + x: Theme.horizontalPageMargin + anchors.verticalCenter: parent.verticalCenter + //: list option for selecting unspecified country + //% "No country" + text: qsTrId("components_timezone-la-undefined_country") + color: undefinedCountryItem.down ? Theme.highlightColor : Theme.primaryColor + } + } } delegate: BackgroundItem { id: background diff --git a/usr/lib/qt5/qml/Sailfish/Timezone/qmldir b/usr/lib/qt5/qml/Sailfish/Timezone/qmldir index fb167bae..6b7f0efb 100644 --- a/usr/lib/qt5/qml/Sailfish/Timezone/qmldir +++ b/usr/lib/qt5/qml/Sailfish/Timezone/qmldir @@ -1,4 +1,5 @@ module Sailfish.Timezone plugin sailfishtimezoneplugin +typeinfo plugins.qmltypes TimezonePicker 1.0 TimezonePicker.qml CountryPicker 1.0 CountryPicker.qml diff --git a/usr/lib/qt5/qml/Sailfish/TransferEngine/ShareFilePreview.qml b/usr/lib/qt5/qml/Sailfish/TransferEngine/ShareFilePreview.qml index 88c4f5f5..5f69eda4 100644 --- a/usr/lib/qt5/qml/Sailfish/TransferEngine/ShareFilePreview.qml +++ b/usr/lib/qt5/qml/Sailfish/TransferEngine/ShareFilePreview.qml @@ -1,9 +1,37 @@ /**************************************************************************************** +** Copyright (c) 2021 - 2023 Jolla Ltd. +** Copyright (c) 2021 Open Mobile Platform LLC. ** -** Copyright (c) 2021 Open Mobile Platform LLC ** All rights reserved. ** -** License: Proprietary. +** This file is part of Sailfish Transfer Engine component package. +** +** You may use this file under the terms of BSD license as follows: +** +** Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are met: +** +** 1. Redistributions of source code must retain the above copyright notice, this +** list of conditions and the following disclaimer. +** +** 2. Redistributions in binary form must reproduce the above copyright notice, +** this list of conditions and the following disclaimer in the documentation +** and/or other materials provided with the distribution. +** +** 3. Neither the name of the copyright holder nor the names of its +** contributors may be used to endorse or promote products derived from +** this software without specific prior written permission. +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +** AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +** IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +** DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +** FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +** DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +** SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +** CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +** OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ** ****************************************************************************************/ import QtQuick 2.6 diff --git a/usr/lib/qt5/qml/Sailfish/TransferEngine/SharePostPreview.qml b/usr/lib/qt5/qml/Sailfish/TransferEngine/SharePostPreview.qml index 4f807c38..df53b778 100644 --- a/usr/lib/qt5/qml/Sailfish/TransferEngine/SharePostPreview.qml +++ b/usr/lib/qt5/qml/Sailfish/TransferEngine/SharePostPreview.qml @@ -1,10 +1,37 @@ /**************************************************************************************** -** -** Copyright (c) 2013 - 2021 Jolla Ltd. +** Copyright (c) 2013 - 2023 Jolla Ltd. ** Copyright (c) 2021 Open Mobile Platform LLC +** ** All rights reserved. ** -** License: Proprietary. +** This file is part of Sailfish Transfer Engine component package. +** +** You may use this file under the terms of BSD license as follows: +** +** Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are met: +** +** 1. Redistributions of source code must retain the above copyright notice, this +** list of conditions and the following disclaimer. +** +** 2. Redistributions in binary form must reproduce the above copyright notice, +** this list of conditions and the following disclaimer in the documentation +** and/or other materials provided with the distribution. +** +** 3. Neither the name of the copyright holder nor the names of its +** contributors may be used to endorse or promote products derived from +** this software without specific prior written permission. +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +** AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +** IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +** DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +** FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +** DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +** SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +** CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +** OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ** ****************************************************************************************/ diff --git a/usr/lib/qt5/qml/Sailfish/TransferEngine/TransfersPage.qml b/usr/lib/qt5/qml/Sailfish/TransferEngine/TransfersPage.qml index ffb0f085..6978b497 100644 --- a/usr/lib/qt5/qml/Sailfish/TransferEngine/TransfersPage.qml +++ b/usr/lib/qt5/qml/Sailfish/TransferEngine/TransfersPage.qml @@ -1,10 +1,37 @@ /**************************************************************************************** -** -** Copyright (c) 2013 - 2019 Jolla Ltd. +** Copyright (c) 2013 - 2023 Jolla Ltd. ** Copyright (c) 2020 Open Mobile Platform LLC +** ** All rights reserved. ** -** License: Proprietary. +** This file is part of Sailfish Transfer Engine component package. +** +** You may use this file under the terms of BSD license as follows: +** +** Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are met: +** +** 1. Redistributions of source code must retain the above copyright notice, this +** list of conditions and the following disclaimer. +** +** 2. Redistributions in binary form must reproduce the above copyright notice, +** this list of conditions and the following disclaimer in the documentation +** and/or other materials provided with the distribution. +** +** 3. Neither the name of the copyright holder nor the names of its +** contributors may be used to endorse or promote products derived from +** this software without specific prior written permission. +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +** AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +** IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +** DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +** FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +** DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +** SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +** CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +** OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ** ****************************************************************************************/ import QtQuick 2.0 @@ -35,12 +62,12 @@ Page { //% "Failed" s += qsTrId("transferui-la_transfer_failed") s += " \u2022 " - s += Format.formatDate(transferDate, Formatter.TimepointRelativeCurrentDay) + s += Format.formatDate(transferDate, Formatter.TimepointRelative) } else if (status === TransferModel.TransferCanceled) { //% "Stopped" s += qsTrId("transferui-la-transfer_stopped") } else { - s += Format.formatDate(transferDate, Formatter.TimepointRelativeCurrentDay) + s += Format.formatDate(transferDate, Formatter.TimepointRelative) } return s } @@ -221,7 +248,6 @@ Page { value: visible ? progress : 0 visible: status === TransferModel.TransferStarted indeterminate: progress < 0 || 1 < progress - clip: true highlighted: transferEntry.highlighted Behavior on height { NumberAnimation {} } diff --git a/usr/lib/qt5/qml/Sailfish/Tutorial/MainPage.qml b/usr/lib/qt5/qml/Sailfish/Tutorial/MainPage.qml index 2e4558bb..b71b4a95 100644 --- a/usr/lib/qt5/qml/Sailfish/Tutorial/MainPage.qml +++ b/usr/lib/qt5/qml/Sailfish/Tutorial/MainPage.qml @@ -176,7 +176,7 @@ TutorialPage { y: Theme.paddingMedium + Theme.paddingSmall width: parent.width - height: batteryIndicator.totalHeight + height: batteryIndicator.height BatteryStatusIndicator { id: batteryIndicator @@ -223,7 +223,8 @@ TutorialPage { id: launcherLayout // from Home LauncherGrid - property real topMargin: Screen.sizeCategory >= Screen.Large ? Theme.paddingLarge*4 : Theme._homePageMargin - Theme.paddingLarge + property real topMargin: Screen.sizeCategory >= Screen.Large ? Theme.paddingLarge*4 + : Theme.paddingSmall height: parent.height } diff --git a/usr/lib/qt5/qml/Sailfish/Tutorial/PulleyLesson.qml b/usr/lib/qt5/qml/Sailfish/Tutorial/PulleyLesson.qml index 4f61ec1a..7427a828 100644 --- a/usr/lib/qt5/qml/Sailfish/Tutorial/PulleyLesson.qml +++ b/usr/lib/qt5/qml/Sailfish/Tutorial/PulleyLesson.qml @@ -9,7 +9,7 @@ import QtTest 1.0 import QtGraphicalEffects 1.0 import Sailfish.Silica 1.0 import Sailfish.Silica.private 1.0 -import org.nemomobile.time 1.0 +import Nemo.Time 1.0 import "private" Lesson { diff --git a/usr/lib/qt5/qml/Sailfish/Tutorial/private/ClockItem.qml b/usr/lib/qt5/qml/Sailfish/Tutorial/private/ClockItem.qml index 667ae6ad..049ad027 100644 --- a/usr/lib/qt5/qml/Sailfish/Tutorial/private/ClockItem.qml +++ b/usr/lib/qt5/qml/Sailfish/Tutorial/private/ClockItem.qml @@ -7,7 +7,7 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 import Nemo.Configuration 1.0 -import org.nemomobile.time 1.0 +import Nemo.Time 1.0 SilicaItem { id: root diff --git a/usr/lib/qt5/qml/Sailfish/Tutorial/private/ConnectionStatusIndicator.qml b/usr/lib/qt5/qml/Sailfish/Tutorial/private/ConnectionStatusIndicator.qml index 3cd30f6f..4aaed3ab 100644 --- a/usr/lib/qt5/qml/Sailfish/Tutorial/private/ConnectionStatusIndicator.qml +++ b/usr/lib/qt5/qml/Sailfish/Tutorial/private/ConnectionStatusIndicator.qml @@ -6,7 +6,7 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 -import MeeGo.Connman 0.2 +import Connman 0.2 import org.nemomobile.lipstick 0.1 Item { @@ -22,11 +22,11 @@ Item { property string _wlanIconId: { // WLAN off if (!wlanNetworkTechnology.powered) - return ""; + return "" // WLAN tethering if (wlanNetworkTechnology.tethering) - return ""; + return "" // WLAN connected if (wlanNetworkTechnology.connected) { @@ -155,18 +155,14 @@ Item { property bool technologyPathsValid: wlanNetworkTechnology.path !== "" && mobileNetworkTechnology.path !== "" function updateTechnologies() { - if (available && technologiesEnabled) { + if (available) { wlanNetworkTechnology.path = networkManager.technologyPathForType("wifi") mobileNetworkTechnology.path = networkManager.technologyPathForType("cellular") } } onAvailableChanged: updateTechnologies() - onTechnologiesEnabledChanged: updateTechnologies() onTechnologiesChanged: updateTechnologies() - - servicesEnabled: !technologyPathsValid || connectionStatusIndicator.enabled - technologiesEnabled: !technologyPathsValid || connectionStatusIndicator.enabled } NetworkTechnology { @@ -176,8 +172,8 @@ Item { NetworkTechnology { id: mobileNetworkTechnology - property bool uploading: false - property bool downloading: false + property bool uploading + property bool downloading } Timer { diff --git a/usr/lib/qt5/qml/Sailfish/Utilities/ActionItem.qml b/usr/lib/qt5/qml/Sailfish/Utilities/ActionItem.qml new file mode 100644 index 00000000..60a32c8b --- /dev/null +++ b/usr/lib/qt5/qml/Sailfish/Utilities/ActionItem.qml @@ -0,0 +1,119 @@ +/** + * @file ActionItem.qml + * @brief Universal action item + * @copyright (C) 2014 Jolla Ltd. + * @par License: LGPL 2.1 http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 + +Item { + id: self + width: parent.width + height: dataArea.height + + property string actionName: "" + property string description: "" + property string remorseText: "" + property string title: "" + property string url: "" + property bool requiresReboot: false + property bool deviceLockRequired: true + + signal done(string name) + signal error(string name, string error) + + property var remorse: undefined + + Component.onCompleted: { + self.done.connect(actionList.done); + self.error.connect(actionList.error); + } + + function executeAction() { + var on_reply = function() { + mainPage.inProgress = false; + console.log("Done:", actionName); + self.done(actionName); + if (requiresReboot) + reboot(); + }; + var on_error = function(err) { + mainPage.inProgress = false; + // TODO show error message + console.log(actionName, " error:", err); + self.error(actionName, err); + }; + console.log("Start", actionName); + mainPage.inProgress = true; + action(on_reply, on_error); + } + + Column { + id: dataArea + width: parent.width + + SectionHeader { + text: self.title + } + Text { + anchors { + left: parent.left + right: parent.right + margins: Theme.paddingLarge + } + color: Theme.highlightColor + textFormat: Text.StyledText + linkColor: Theme.primaryColor + font.pixelSize: Theme.fontSizeSmall + wrapMode: Text.Wrap + text: description + onLinkActivated: Qt.openUrlExternally(link) + } + Item { width: parent.width; height: Theme.paddingMedium } + Label { + anchors { + left: parent.left + right: parent.right + margins: Theme.paddingLarge + } + wrapMode: Text.Wrap + visible: requiresReboot + color: Theme.highlightColor + font.pixelSize: Theme.fontSizeSmall + //% "This action requires reboot." + text: qsTrId("sailfish-tools-me-require-reboot") + } + Button { + id: btn + Component { + id: remorseComponent + RemorseItem { } + } + + function remorseAction(text, action, timeout) { + // null parent because a reference is held by RemorseItem until + // it either triggers or is cancelled. + if (!self.remorse) + remorse = remorseComponent.createObject(self)//null) + remorse.execute(dataArea, text, action, timeout) + } + + text: actionName + height: Theme.itemSizeSmall + anchors.horizontalCenter: parent.horizontalCenter + onClicked: { + var executeAfterRemorse = function() { + remorseAction(remorseText, executeAction, 5000); + }; + var fn = remorseText ? executeAfterRemorse : executeAction; + if (deviceLockRequired) { + requestSecurityCode(fn); + } else { + fn(); + } + } + } + } +} diff --git a/usr/lib/qt5/qml/Sailfish/Utilities/qmldir b/usr/lib/qt5/qml/Sailfish/Utilities/qmldir new file mode 100644 index 00000000..cffa5522 --- /dev/null +++ b/usr/lib/qt5/qml/Sailfish/Utilities/qmldir @@ -0,0 +1,3 @@ +module Sailfish.Utilities +plugin sailfishutilitiesplugin +ActionItem 1.0 ActionItem.qml diff --git a/usr/lib/qt5/qml/Sailfish/Vault/BackupRestoreStorageListModel.qml b/usr/lib/qt5/qml/Sailfish/Vault/BackupRestoreStorageListModel.qml index c0b189fa..fd2016cd 100644 --- a/usr/lib/qt5/qml/Sailfish/Vault/BackupRestoreStorageListModel.qml +++ b/usr/lib/qt5/qml/Sailfish/Vault/BackupRestoreStorageListModel.qml @@ -11,7 +11,7 @@ import QtQuick 2.6 import Sailfish.Silica 1.0 import Sailfish.Accounts 1.0 import Sailfish.Vault 1.0 -import MeeGo.Connman 0.2 +import Connman 0.2 import Nemo.DBus 2.0 import org.nemomobile.systemsettings 1.0 import com.jolla.settings.system 1.0 diff --git a/usr/lib/qt5/qml/Sailfish/Vault/BackupRestoreStoragePicker.qml b/usr/lib/qt5/qml/Sailfish/Vault/BackupRestoreStoragePicker.qml index 4d95bf69..535345b8 100644 --- a/usr/lib/qt5/qml/Sailfish/Vault/BackupRestoreStoragePicker.qml +++ b/usr/lib/qt5/qml/Sailfish/Vault/BackupRestoreStoragePicker.qml @@ -10,7 +10,7 @@ import QtQuick 2.6 import Sailfish.Silica 1.0 import Sailfish.Vault 1.0 -import MeeGo.Connman 0.2 +import Connman 0.2 import Nemo.Configuration 1.0 import com.jolla.settings.accounts 1.0 diff --git a/usr/lib/qt5/qml/Sailfish/Vault/BackupView.qml b/usr/lib/qt5/qml/Sailfish/Vault/BackupView.qml index 37e143a8..f76ccd47 100644 --- a/usr/lib/qt5/qml/Sailfish/Vault/BackupView.qml +++ b/usr/lib/qt5/qml/Sailfish/Vault/BackupView.qml @@ -9,7 +9,7 @@ import QtQuick 2.6 import Sailfish.Silica 1.0 import Sailfish.Vault 1.0 -import MeeGo.Connman 0.2 +import Connman 0.2 import Nemo.DBus 2.0 Column { diff --git a/usr/lib/qt5/qml/Sailfish/Weather/CurrentLocationModel.qml b/usr/lib/qt5/qml/Sailfish/Weather/CurrentLocationModel.qml new file mode 100644 index 00000000..25ffac53 --- /dev/null +++ b/usr/lib/qt5/qml/Sailfish/Weather/CurrentLocationModel.qml @@ -0,0 +1,108 @@ +import QtQuick 2.0 +import QtPositioning 5.2 +import QtQuick.XmlListModel 2.0 +import Nemo.KeepAlive 1.2 + +Item { + id: model + + property bool ready + property bool error + property string city + property string locationId + property bool metric: true + property bool positioningAllowed + property bool locationObtained + property bool active: true + property real searchRadius: 10 // find biggest city in specified kilometers + property var coordinate: positionSource.position.coordinate + + property string longitude: format(coordinate.longitude) + property string latitude: format(coordinate.latitude) + property bool waitForSecondUpdate + + function format(value) { + // optimize Foreca backend caching by + // rounding to closest even decimal + // (0.02 degree accuracy) e.g. 0.99 -> 1.00, 175.5637 -> 175.56 + if (value) { + var angle = value + var integer = Math.floor(value) + var decimal = 2*Math.round(50*(angle - integer)) + if (decimal == 100) { + integer = Math.floor(value+1) + decimal = 0 + } + return integer.toString() + "." + (decimal < 10 ? "0" : "") + decimal.toString() + } else { + return "0.0" + } + } + function updateLocation() { + active = true + // first update returns cached location, wait for real position fix + waitForSecondUpdate = true + } + function reloadModel() { + locationModel.reload() + } + + XmlListModel { + id: locationModel + + query: "/searchdata/location" + source: locationObtained ? "http://fnw-jll.foreca.com/findloc.php" + + "?lon=" + longitude + + "&lat=" + latitude + + "&format=xml/jolla-sep13fi" + + "&radius=" + searchRadius + : "" + onStatusChanged: { + if (status === XmlListModel.Ready && count > 0) { + var location = get(0) + locationId = location.locationId + city = location.city + metric = (location.locale !== "gb" && location.locale !== "us") + ready = true + } + if (status !== XmlListModel.Loading) { + if (backgroundJob.running) { + backgroundJob.finished() + } + } + error = (status === XmlListModel.Error) + } + + XmlRole { + name: "locationId" + query: "id/string()" + } + XmlRole { + name: "city" + query: "name/string()" + } + XmlRole { + name: "locale" + query: "land/string()" + } + } + PositionSource { + id: positionSource + active: model.positioningAllowed && model.active + onPositionChanged: { + locationObtained = true + if (!waitForSecondUpdate) { + model.active = false + } + waitForSecondUpdate = false + } + } + BackgroundJob { + id: backgroundJob + + triggeredOnEnable: true + enabled: true + frequency: BackgroundJob.ThirtyMinutes + onTriggered: model.updateLocation() + } +} diff --git a/usr/lib/qt5/qml/Sailfish/Weather/DailyForecastItem.qml b/usr/lib/qt5/qml/Sailfish/Weather/DailyForecastItem.qml new file mode 100644 index 00000000..fe86505a --- /dev/null +++ b/usr/lib/qt5/qml/Sailfish/Weather/DailyForecastItem.qml @@ -0,0 +1,42 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Sailfish.Weather 1.0 + +Column { + width: parent.width + anchors.centerIn: parent + property bool highlighted + Label { + property bool truncate: implicitWidth > parent.width - Theme.paddingSmall + + x: truncate ? Theme.paddingSmall : parent.width/2 - width/2 + // Difficult layout due to limited horizontal space + // Fade truncation overflows slightly to the adjacent delegate, + // but should be ok since there is horizontal padding + width: truncate ? parent.width : implicitWidth + truncationMode: truncate ? TruncationMode.Fade : TruncationMode.None + text: model.index === 0 + ? //% "Today" + qsTrId("weather-la-today") + : //% "ddd" + Qt.formatDateTime(timestamp, qsTrId("weather-la-date_pattern_shortweekdays")) + color: highlighted ? Theme.secondaryHighlightColor : Theme.secondaryColor + font.pixelSize: Theme.fontSizeSmall + } + Label { + text: TemperatureConverter.format(model.high) + anchors.horizontalCenter: parent.horizontalCenter + } + Image { + property string prefix: "image://theme/icon-" + (Screen.sizeCategory >= Screen.Large ? "l" : "m") + anchors.horizontalCenter: parent.horizontalCenter + source: model.weatherType.length > 0 ? prefix + "-weather-" + model.weatherType + + (highlighted ? "?" + Theme.highlightColor : "") + : "" + } + Label { + text: TemperatureConverter.format(model.low) + anchors.horizontalCenter: parent.horizontalCenter + color: highlighted ? Theme.secondaryHighlightColor : Theme.secondaryColor + } +} diff --git a/usr/lib/qt5/qml/Sailfish/Weather/ForecaToken.js b/usr/lib/qt5/qml/Sailfish/Weather/ForecaToken.js new file mode 100644 index 00000000..6a922a09 --- /dev/null +++ b/usr/lib/qt5/qml/Sailfish/Weather/ForecaToken.js @@ -0,0 +1,80 @@ +.import Sailfish.Weather 1.0 as Weather +.pragma library + +var user = "" +var password = "" +var token = "" +var tokenRequest +var pendingTokenRequests = [] +var lastUpdate = new Date() + +function fetchToken(model) { + if (model == undefined) { + console.warn("Token requested for undefined or null model") + return false + } + + if (token.length > 0 && !updateAllowed()) { + model.token = token + return true + } else { + if (!tokenRequest) { + + if (user.length === 0 || password.length === 0) { + var keyProvider = Qt.createQmlObject( + "import com.jolla.settings.accounts 1.0; StoredKeyProvider {}", + model, "StoreKeyProvider") + + user = keyProvider.storedKey("foreca", "", "user") + password = keyProvider.storedKey("foreca", "", "password") + keyProvider.destroy() + + if (user.length === 0 || password.length === 0) { + console.warn("Unable to get Foreca credentials needed to identify with the service") + return false + } + } + + tokenRequest = new XMLHttpRequest() + + var url = "https://pfa.foreca.com/authorize/token?user=" + user + "&password=" + password + + // Send the proper header information along with the tokenRequest + tokenRequest.onreadystatechange = function() { // Call a function when the state changes. + if (tokenRequest.readyState == XMLHttpRequest.DONE) { + if (tokenRequest.status == 200) { + var json = JSON.parse(tokenRequest.responseText) + token = json["access_token"] + } else { + token = "" + console.log("Failed to obtain Foreca token. HTTP error code: " + tokenRequest.status) + } + + for (var i = 0; i < pendingTokenRequests.length; i++) { + pendingTokenRequests[i].token = token + if (tokenRequest.status !== 200) { + pendingTokenRequests[i].status = (tokenRequest.status === 401) ? Weather.Weather.Unauthorized : Weather.Weather.Error + } + } + pendingTokenRequests = [] + tokenRequest = undefined + } + } + tokenRequest.open("GET", url) + tokenRequest.send() + } + pendingTokenRequests[pendingTokenRequests.length] = model + } + return false +} + +function updateAllowed(interval) { + // only update token if older than 45 minutes + interval = interval === undefined ? 45*60*1000 : interval + var now = new Date() + var updateAllowed = now.getDate() != lastUpdate.getDate() || (now - interval > lastUpdate) + if (updateAllowed) { + lastUpdate = now + } + return updateAllowed +} diff --git a/usr/lib/qt5/qml/Sailfish/Weather/HourlyForecastItem.qml b/usr/lib/qt5/qml/Sailfish/Weather/HourlyForecastItem.qml new file mode 100644 index 00000000..1cd632b1 --- /dev/null +++ b/usr/lib/qt5/qml/Sailfish/Weather/HourlyForecastItem.qml @@ -0,0 +1,85 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Sailfish.Weather 1.0 + +Column { + width: parent.width + property bool highlighted + property int hourMode: DateTime.TwentyFourHours + + Item { + property int padding: Theme.paddingSmall + width: temperatureLabel.width + height: temperatureGraph.height + temperatureLabel.height + padding + anchors.horizontalCenter: parent.horizontalCenter + Label { + id: temperatureLabel + text: TemperatureConverter.format(model.temperature) + y: (1 - model.relativeTemperature) * temperatureGraph.height - parent.padding + } + } + + Image { + property string prefix: "image://theme/icon-" + (Screen.sizeCategory >= Screen.Large ? "l" : "m") + anchors.horizontalCenter: parent.horizontalCenter + source: model.weatherType.length > 0 ? prefix + "-weather-" + model.weatherType + + (highlighted ? "?" + Theme.highlightColor : "") + : "" + } + + Row { + id: timeRow + anchors.horizontalCenter: parent.horizontalCenter + Label { + id: timeLabel + text: { + if (hourMode === DateTime.TwentyFourHours) { + return Format.formatDate(model.timestamp, Format.TimeValueTwentyFourHours) + } else { + var hours = model.timestamp.getHours() + if (hours === 0) { + hours = 12 + } else if (hours > 12) { + hours -= 12 + } + + //% "h" + //: Pattern for 12h time, should be either "h" or "hh", latter with optional 0 at the start (like "03") + var result = qsTrId("weather-la-12h_time_pattern_without_ap") + var zero = 0 + + if (result.indexOf("hh") !== -1) { + var hourString = "" + + if (hours < 10) { + hourString = zero.toLocaleString() + } + hourString += hours.toLocaleString() + + result = result.replace("hh", hourString) + } else { + result = result.replace("h", hours.toLocaleString()) + } + + return result + } + } + font.pixelSize: hourMode === DateTime.TwentyFourHours ? Theme.fontSizeSmall : Theme.fontSizeMedium + } + Label { + visible: hourMode === DateTime.TwelveHours + //: Short postfix shown behind hours in twelve hour mode, e.g. time is 8am + //: Align with jolla-clock-la-am + //% "AM" + text: model.timestamp.getHours() < 12 ? qsTrId("weather-la-hourmode_am") + //: Short postfix shown behind hours in twelve hour mode, e.g. 3pm time + //: Align with jolla-clock-la-pm + //% "PM" + : qsTrId("weather-clock-la-hourmode_pm") + color: highlighted ? Theme.secondaryHighlightColor : Theme.secondaryColor + font.pixelSize: Theme.fontSizeTiny + anchors.baseline: timeLabel.baseline + + } + } +} diff --git a/usr/lib/qt5/qml/Sailfish/Weather/LocationDetection.qml b/usr/lib/qt5/qml/Sailfish/Weather/LocationDetection.qml new file mode 100644 index 00000000..83b015c9 --- /dev/null +++ b/usr/lib/qt5/qml/Sailfish/Weather/LocationDetection.qml @@ -0,0 +1,47 @@ +pragma Singleton +import QtQuick 2.2 +import Sailfish.Weather 1.0 +import org.nemomobile.systemsettings 1.0 + +Item { + id: root + + property bool ready: model && model.ready + property bool error: model && model.error + property string city: ready ? model.city : true + property string locationId: ready ? model.locationId : "" + property bool positioningAllowed: locationSettings.locationEnabled + property bool metric: ready ? model.metric : true + property QtObject model + + onPositioningAllowedChanged: handleLocationSetting() + Component.onCompleted: handleLocationSetting() + + function updateLocation() { + if (positioningAllowed && model) { + model.updateLocation() + } + } + function reloadModel() { + if (positioningAllowed && model) { + model.reloadModel() + } + } + + function handleLocationSetting() { + if (positioningAllowed) { + if (!model) { + var modelComponent = Qt.createComponent("CurrentLocationModel.qml") + if (modelComponent.status === Component.Ready) { + model = modelComponent.createObject(root) + model.positioningAllowed = Qt.binding(function() { + return positioningAllowed + }) + } else { + console.log(modelComponent.errorString()) + } + } + } + } + LocationSettings { id: locationSettings } +} diff --git a/usr/lib/qt5/qml/Sailfish/Weather/LocationsModel.qml b/usr/lib/qt5/qml/Sailfish/Weather/LocationsModel.qml new file mode 100644 index 00000000..e8752491 --- /dev/null +++ b/usr/lib/qt5/qml/Sailfish/Weather/LocationsModel.qml @@ -0,0 +1,54 @@ +import QtQuick 2.0 +import Sailfish.Weather 1.0 + +ListModel { + id: root + + property string filter + property alias status: model.status + + onFilterChanged: if (filter.length === 0) clear() + + function reload() { + model.reload() + } + + readonly property WeatherRequest model: WeatherRequest { + id: model + + property string language: { + var locale = Qt.locale().name + if (locale === "zh_CN" || locale === "zh_TW") { + return locale + } else { + return locale.split("_")[0] + } + } + + source: filter.length > 0 ? "https://pfa.foreca.com/api/v1/location/search/" + filter.toLowerCase() + "&lang=" + language : "" + onRequestFinished: { + var locations = result["locations"] + if (result.length === 0 || locations === undefined) { + status = Weather.Error + } else { + while (root.count > locations.length) { + root.remove(locations.length) + } + for (var i = 0; i < locations.length; i++) { + if (i < root.count) { + root.set(i, locations[i]) + } else { + root.append(locations[i]) + } + } + } + } + + onStatusChanged: { + if (status === Weather.Error || status === Weather.Unauthorized) { + root.clear() + console.log("LocationsModel - location search failed with query string", filter) + } + } + } +} diff --git a/usr/lib/qt5/qml/Sailfish/Weather/PlaceholderItem.qml b/usr/lib/qt5/qml/Sailfish/Weather/PlaceholderItem.qml new file mode 100644 index 00000000..8a9bf52c --- /dev/null +++ b/usr/lib/qt5/qml/Sailfish/Weather/PlaceholderItem.qml @@ -0,0 +1,94 @@ +import QtQuick 2.2 +import Sailfish.Silica 1.0 + +Item { + id: root + + property bool error + property bool unauthorized + property bool empty + property bool enabled + property Flickable flickable + property Item _animationHint + property alias text: mainLabel.text + + signal reload + + function update() { + if (!_animationHint && enabled && flickable) { + _animationHint = animationHint.createObject(root) + } + } + Component.onCompleted: update() + onEnabledChanged: update() + onFlickableChanged: update() + + width: parent.width + height: mainLabel.height + Theme.paddingLarge + ((error || unauthorized) ? button.height : busyIndicator.height) + opacity: enabled ? 1.0 : 0.0 + Behavior on opacity { OpacityAnimator { easing.type: Easing.InOutQuad; duration: 400 } } + Label { + id: mainLabel + + wrapMode: Text.Wrap + horizontalAlignment: Text.AlignHCenter + + text: { + if (error) { + //% "Loading failed" + return qsTrId("weather-la-loading_failed") + } else if (unauthorized) { + //% "Invalid authentication credentials" + return qsTrId("weather-la-unauthorized") + } + + //% "Loading" + return qsTrId("weather-la-loading") + } + font { + pixelSize: Theme.fontSizeExtraLarge + family: Theme.fontFamilyHeading + } + anchors { + left: parent.left + right: parent.right + margins: Theme.paddingLarge + } + color: Theme.highlightColor + opacity: 0.6 + } + BusyIndicator { + id: busyIndicator + running: parent.opacity > 0 && !error && !unauthorized && !empty + size: BusyIndicatorSize.Large + anchors { + top: mainLabel.bottom + topMargin: Theme.paddingLarge + horizontalCenter: parent.horizontalCenter + } + } + Button { + id: button + //% "Try again" + text: qsTrId("weather-la-try_again") + opacity: enabled ? 1.0 : 0.0 + enabled: error + Behavior on opacity { FadeAnimation {} } + anchors { + top: mainLabel.bottom + topMargin: Theme.paddingLarge + horizontalCenter: parent.horizontalCenter + } + onClicked: reload() + } + Component { + id: animationHint + PulleyAnimationHint { + enabled: !error && !unauthorized + flickable: root.flickable + width: parent.width + height: width + anchors.centerIn: parent + } + } +} diff --git a/usr/lib/qt5/qml/Sailfish/Weather/ProviderDisclaimer.qml b/usr/lib/qt5/qml/Sailfish/Weather/ProviderDisclaimer.qml new file mode 100644 index 00000000..66d73466 --- /dev/null +++ b/usr/lib/qt5/qml/Sailfish/Weather/ProviderDisclaimer.qml @@ -0,0 +1,33 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 + +BackgroundItem { + id: root + + property var weather + property int topMargin: Theme.paddingLarge + property int bottomMargin: 2*Theme.paddingLarge + + onClicked: if (weather) Qt.openUrlExternally("http://foreca.mobi/spot.php?l=" + weather.locationId) + height: column.height + topMargin + bottomMargin + Column { + id: column + width: parent.width + spacing: Theme.paddingSmall + Label { + //% "Powered by" + text: qsTrId("weather-la-powered_by") + anchors.horizontalCenter: parent.horizontalCenter + font.pixelSize: Theme.fontSizeTiny + color: highlighted ? Theme.secondaryHighlightColor : Theme.secondaryColor + } + Image { + anchors.horizontalCenter: parent.horizontalCenter + source: "image://theme/graphic-foreca-large?" + (highlighted ? Theme.highlightColor : Theme.primaryColor) + } + anchors { + bottom: parent.bottom + bottomMargin: root.bottomMargin + } + } +} diff --git a/usr/lib/qt5/qml/Sailfish/Weather/TemperatureConverter.qml b/usr/lib/qt5/qml/Sailfish/Weather/TemperatureConverter.qml new file mode 100644 index 00000000..fa1a16f6 --- /dev/null +++ b/usr/lib/qt5/qml/Sailfish/Weather/TemperatureConverter.qml @@ -0,0 +1,25 @@ +pragma Singleton +import QtQuick 2.2 +import Nemo.Configuration 1.0 + +ConfigurationValue { + property bool celsius: { + switch (value) { + case "celsius": + return true + case "fahrenheit": + return false + default: + console.log("TemperatureConverter: Invalid temperature unit value", value) + return true + } + } + function formatWithoutUnit(temperature) { + return celsius ? temperature : Math.round(9/5*parseInt(temperature)+32).toString() + } + function format(temperature) { + return formatWithoutUnit(temperature) + "\u00B0" + } + key: "/sailfish/weather/temperature_unit" + defaultValue: "celsius" +} diff --git a/usr/lib/qt5/qml/Sailfish/Weather/TemperatureLabel.qml b/usr/lib/qt5/qml/Sailfish/Weather/TemperatureLabel.qml new file mode 100644 index 00000000..f603475a --- /dev/null +++ b/usr/lib/qt5/qml/Sailfish/Weather/TemperatureLabel.qml @@ -0,0 +1,45 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 + +Item { + property alias temperature: temperatureLabel.text + property alias feelsLikeTemperature: feelsLikeTemperatureLabel.text + property alias color: temperatureLabel.color + + height: temperatureLabel.height + width: temperatureLabel.width + degreeSymbol.width + Theme.paddingMedium + Label { + id: temperatureLabel + color: Theme.primaryColor + + // Glyphs larger than 100 or so look poorly in the default rendering mode + renderType: font.pixelSize > 100 ? Text.NativeRendering : Text.QtRendering + font { + pixelSize: 120*Screen.width/540 + family: Theme.fontFamilyHeading + } + } + Label { + id: degreeSymbol + text: "\u00B0" + color: parent.color + anchors { + left: temperatureLabel.right + leftMargin: Theme.paddingMedium + } + font { + pixelSize: 3*Theme.fontSizeLarge + family: Theme.fontFamilyHeading + } + } + Label { + id: feelsLikeTemperatureLabel + opacity: 0.6 + color: parent.color + font.pixelSize: Theme.fontSizeLarge + anchors { + baseline: temperatureLabel.baseline + right: degreeSymbol.right + } + } +} diff --git a/usr/lib/qt5/qml/Sailfish/Weather/WeatherBanner.qml b/usr/lib/qt5/qml/Sailfish/Weather/WeatherBanner.qml new file mode 100644 index 00000000..1e5b31fd --- /dev/null +++ b/usr/lib/qt5/qml/Sailfish/Weather/WeatherBanner.qml @@ -0,0 +1,401 @@ +import QtQuick 2.6 +import Sailfish.Silica 1.0 +import Sailfish.Silica.private 1.0 +import Sailfish.Weather 1.0 +import Nemo.Configuration 1.0 + +ListItem { + id: weatherBanner + + property alias weather: savedWeathersModel.currentWeather + property alias autoRefresh: savedWeathersModel.autoRefresh + property alias active: weatherModel.active + property bool hourly: forecastMode.value === "hourly" + property bool expanded: true + readonly property QtObject forecastModel: hourly ? (hourlyForecastLoader.item ? hourlyForecastLoader.item.model : null) + : (dailyForecastLoader.item ? dailyForecastLoader.item.model : null) + readonly property bool loading: forecastModel && forecastModel.status === Weather.Loading + readonly property bool _error: forecastModel && forecastModel.status === Weather.Error + readonly property bool _unauthorized: forecastModel && forecastModel.status === Weather.Unauthorized + readonly property int _forecastCount: forecastModel ? forecastModel.count : 0 + + _backgroundColor: "transparent" + onActiveChanged: if (!active) save() + onHourlyChanged: forecastMode.value = hourly ? "hourly" : "daily" + + function reload(userRequested) { + weatherModel.reload(userRequested) + forecastModel.reload(userRequested) + } + + function save() { + savedWeathersModel.save() + } + + onClicked: { + if (!expanded) { + expanded = true + } else if (!_error && !_unauthorized) { + hourly = !hourly + } + + if (!_unauthorized) { + weatherModel.attemptReload(true) + forecastModel.attemptReload(true) + } + } + + visible: enabled + contentHeight: enabled ? column.height : 0 + enabled: weather && weather.populated + + menu: Component { + ContextMenu { + MenuLabel { + //% "Updated %1" + text: forecastModel ? qsTrId("weather-la-updated_time").arg( + Format.formatDate(forecastModel.timestamp, Formatter.Timepoint)) + : "" + visible: !_error && !_unauthorized && !loading + } + + MenuItem { + //% "Open app" + text: qsTrId("weather-la-open_app") + onClicked: WeatherLauncher.launch() + } + MenuItem { + visible: !_unauthorized + //% "Reload" + text: qsTrId("weather-la-reload") + onClicked: reload(true) + } + } + } + + Column { + id: column + width: parent.width + Row { + id: row + + property int margin: (column.width - image.width - Theme.paddingMedium - temperatureLabel.width + - Theme.paddingSmall - cityLabel.width)/2 + + x: margin + width: parent.width - x + height: Theme.itemSizeSmall + + Image { + id: image + width: height + height: parent.height + anchors.verticalCenter: parent.verticalCenter + source: weather && weather.weatherType.length > 0 ? "image://theme/icon-l-weather-" + weather.weatherType + + (highlighted ? ("?" + Theme.highlightColor) : "") + : "" + } + + Item { + width: Theme.paddingMedium + height: 1 + } + + Label { + id: temperatureLabel + text: weather ? TemperatureConverter.format(weather.temperature) : "" + font.pixelSize: Theme.fontSizeExtraLarge + anchors.verticalCenter: parent.verticalCenter + } + + Item { + width: Theme.paddingSmall + height: 1 + } + + Label { + id: cityLabel + text: weather ? weather.city : "" + color: highlighted ? Theme.secondaryHighlightColor : Theme.secondaryColor + font { + pixelSize: Theme.fontSizeSmall + family: Theme.fontFamilyHeading + } + anchors.baseline: temperatureLabel.baseline + truncationMode: TruncationMode.Fade + width: Math.min(implicitWidth, + column.width - image.width - Theme.paddingMedium - Theme.paddingSmall + - temperatureLabel.width - expandButton.width - Theme.horizontalPageMargin) + } + + Item { + height: 1 + width: parent.margin - expandButton.width - Theme.horizontalPageMargin + Theme.paddingLarge + } + + IconButton { + id: expandButton + + height: Math.max(parent.height, Theme.itemSizeSmall) + width: icon.width + 2*Theme.paddingLarge + onClicked: expanded = !expanded + icon { + transformOrigin: Item.Center + source: "image://theme/icon-s-arrow" + rotation: expanded ? 180 : 0 + } + Behavior on icon.rotation { RotationAnimator { duration: 200 }} + } + } + Column { + width: parent.width + opacity: expanded ? 1.0 : 0.0 + + height: expanded ? implicitHeight : 0 + Behavior on opacity { FadeAnimator {} } + Behavior on height { + NumberAnimation { + duration: 200 + easing.type: Easing.InOutQuad + } + } + + Item { + width: parent.width + height: Math.max(hourlyForecastLoader.height, dailyForecastLoader.height) + Loader { + id: dailyForecastLoader + width: parent.width + active: !weatherBanner.hourly && expanded + anchors.verticalCenter: parent.verticalCenter + onActiveChanged: if (active) active = active // remove binding + + sourceComponent: WeatherForecastList { + id: dailyForecastList + + columnCount: model.visibleCount + active: !weatherBanner.hourly + model: WeatherForecastModel { + active: weatherBanner.active && !weatherBanner.hourly + weather: weatherBanner.weather + timestamp: weatherModel.timestamp + } + + delegate: Item { + width: dailyForecastList.itemWidth + height: dailyForecastList.height + DailyForecastItem { + highlighted: weatherBanner.highlighted + onHeightChanged: if (model.index == 0) dailyForecastList.itemHeight = height + } + } + } + } + + Loader { + id: hourlyForecastLoader + width: parent.width + active: weatherBanner.hourly && expanded + anchors.verticalCenter: parent.verticalCenter + onActiveChanged: if (active) active = active // remove binding + + sourceComponent: WeatherForecastList { + id: hourlyForecastList + + property int hourMode: timeFormatConfig.value === "24" ? DateTime.TwentyFourHours + : DateTime.TwelveHours + active: weatherBanner.hourly + columnCount: model.visibleCount + + FontMetrics { + id: fontMetrics + font.pixelSize: Theme.fontSizeMedium // align with temperature label in HourlyForecastItem + } + + Item { + y: fontMetrics.height + + visible: hourlyForecastList.model.count > 0 + width: hourlyForecastList.width - hourlyForecastList.itemWidth/2 + height: temperatureGraph.height + anchors.horizontalCenter: parent.horizontalCenter + clip: true + + LineGraph { + id: temperatureGraph + + function update() { + var array = [] + var model = hourlyForecastList.model + + array[0] = 2 * model.get(0).relativeTemperature - model.get(1).relativeTemperature + for (var i = 0; i < model.visibleCount; i++) { + array[i + 1] = model.get(i).relativeTemperature + } + array[model.visibleCount + 1] = model.get(model.visibleCount).relativeTemperature + values = array + } + + width: hourlyForecastList.width + hourlyForecastList.itemWidth + anchors.horizontalCenter: parent.horizontalCenter + height: Theme.itemSizeMedium/2 + lineWidth: Theme.paddingSmall/3 + color: Theme.highlightColor + } + } + + model: WeatherForecastModel { + hourly: true + active: weatherBanner.active && weatherBanner.hourly + weather: weatherBanner.weather + timestamp: weatherModel.timestamp + onStatusChanged: if (status === Weather.Ready) temperatureGraph.update() + } + + delegate: Item { + width: hourlyForecastList.itemWidth + height: hourlyForecastList.height + HourlyForecastItem { + hourMode: hourlyForecastList.hourMode + highlighted: weatherBanner.highlighted + onHeightChanged: if (model.index == 0) hourlyForecastList.itemHeight = height + } + } + } + } + + BusyIndicator { + size: Screen.sizeCategory >= Screen.Large ? BusyIndicatorSize.Large : BusyIndicatorSize.Medium + anchors.centerIn: parent + running: weatherBanner.loading && forecastModel.count === 0 + } + + Column { + width: parent.width + spacing: Theme.paddingSmall + anchors.verticalCenter: parent.verticalCenter + + opacity: (_error || _unauthorized) && _forecastCount === 0 ? 1 : 0 + Behavior on opacity { FadeAnimator {} } + + Label { + anchors.horizontalCenter: parent.horizontalCenter + text: { + if (_error) { + //% "No network" + return qsTrId("weather-la-no_network") + } + + //% "Invalid authentication credentials" + return qsTrId("weather-la-unauthorized") + } + font.pixelSize: _error ? Theme.fontSizeLarge : Theme.fontSizeMedium + } + Label { + visible: _error + anchors.horizontalCenter: parent.horizontalCenter + color: highlighted ? Theme.secondaryHighlightColor : Theme.secondaryColor + + //% "Tap to retry" + text: qsTrId("weather-la-tap_to_retry") + } + } + } + + MouseArea { + id: footer + + property bool down: pressed && containsMouse + + onClicked: Qt.openUrlExternally("http://foreca.mobi/spot.php?l=" + savedWeathersModel.currentWeather.locationId) + + width: footerRow.width + height: footerRow.height + Theme.paddingSmall + anchors { right: parent.right; rightMargin: Theme.horizontalPageMargin } + enabled: savedWeathersModel.currentWeather && savedWeathersModel.currentWeather.populated && !_error && expanded + + Row { + id: footerRow + + BusyIndicator { + size: BusyIndicatorSize.Small + anchors.verticalCenter: parent.verticalCenter + running: minimumTimeout.running || (weatherBanner.loading && forecastModel.count > 0) + onRunningChanged: minimumTimeout.restart() + Timer { + id: minimumTimeout + interval: 400 + } + } + Item { + height: 1 + width: Theme.paddingMedium + } + + Image { + anchors.verticalCenter: parent.verticalCenter + source: "image://theme/graphic-foreca-small?" + + (highlighted || footer.down ? Theme.highlightColor : Theme.primaryColor) + } + Label { + //: Indicates when the shown forecast information was updated + //: Displayed right after small Foreca logo, i.e. "FORECA, updated 12:59, 1.3.2020" + //% ", updated %1" + text: forecastModel ? qsTrId("weather-la-comma_updated_time") + .arg(Format.formatDate(forecastModel.timestamp, Format.Timepoint)) + : "" + anchors.verticalCenter: parent.verticalCenter + font.pixelSize: Theme.fontSizeExtraSmall + highlighted: weatherBanner.highlighted || footer.down + + visible: _error && _forecastCount > 0 + } + } + } + Label { + x: Theme.horizontalPageMargin + width: parent.width - 2*x + horizontalAlignment: Text.AlignRight + font.pixelSize: Theme.fontSizeExtraSmall + wrapMode: Text.Wrap + + //% "No network, tap to retry" + text: qsTrId("weather-la-no_network_tap_to_retry") + + color: highlighted ? Theme.secondaryHighlightColor : Theme.secondaryColor + opacity: enabled ? 1 : 0 + height: enabled ? implicitHeight : 0 + enabled: _error && _forecastCount > 0 + Behavior on opacity { FadeAnimator {} } + Behavior on height { + NumberAnimation { + duration: 200 + easing.type: Easing.InOutQuad + } + } + } + } + } + + SavedWeathersModel { + id: savedWeathersModel + autoRefresh: true + } + + WeatherModel { + id: weatherModel + weather: savedWeathersModel.currentWeather + savedWeathers: savedWeathersModel + } + + ConfigurationValue { + id: forecastMode + key: "/sailfish/weather/forecast_mode" + defaultValue: "hourly" + } + + ConfigurationValue { + id: timeFormatConfig + key: "/sailfish/i18n/lc_timeformat24h" + } +} diff --git a/usr/lib/qt5/qml/Sailfish/Weather/WeatherConnectionHelper.qml b/usr/lib/qt5/qml/Sailfish/Weather/WeatherConnectionHelper.qml new file mode 100644 index 00000000..6e5edfee --- /dev/null +++ b/usr/lib/qt5/qml/Sailfish/Weather/WeatherConnectionHelper.qml @@ -0,0 +1,4 @@ +pragma Singleton +import Nemo.Connectivity 1.0 + +ConnectionHelper {} diff --git a/usr/lib/qt5/qml/Sailfish/Weather/WeatherDetailItem.qml b/usr/lib/qt5/qml/Sailfish/Weather/WeatherDetailItem.qml new file mode 100644 index 00000000..262d8cd5 --- /dev/null +++ b/usr/lib/qt5/qml/Sailfish/Weather/WeatherDetailItem.qml @@ -0,0 +1,9 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 + +DetailItem { + readonly property bool onLeft: Positioner.index % 2 == 0 + width: parent.width / parent.columns + leftMargin: onLeft || isPortrait ? Theme.horizontalPageMargin : Theme.paddingMedium + rightMargin: !onLeft || isPortrait ? Theme.horizontalPageMargin : Theme.paddingMedium +} \ No newline at end of file diff --git a/usr/lib/qt5/qml/Sailfish/Weather/WeatherDetailsHeader.qml b/usr/lib/qt5/qml/Sailfish/Weather/WeatherDetailsHeader.qml new file mode 100644 index 00000000..e8b27aea --- /dev/null +++ b/usr/lib/qt5/qml/Sailfish/Weather/WeatherDetailsHeader.qml @@ -0,0 +1,208 @@ +import QtQuick 2.2 +import Sailfish.Silica 1.0 +import Sailfish.Weather 1.0 + +Item { + id: root + + property var model + property var weather + property bool today + property bool current + property int status + + width: parent.width + height: childrenRect.height + + Column { + width: parent.width + PageHeader { + id: pageHeader + title: weather ? weather.city : "" + description: { + if (status === Weather.Error) { + //% "Loading failed" + return qsTrId("weather-la-weather_loading_failed") + } else if (status === Weather.Unauthorized) { + //% "Invalid authentication credentials" + return qsTrId("weather-la-weather_unauthorized") + } else if (status === Weather.Loading) { + //% "Loading" + return qsTrId("weather-la-weather_loading") + } else if (today) { + //% "Weather today" + return qsTrId("weather-la-weather_today") + } else { + //% "Weather forecast" + return qsTrId("weather-la-weather_forecast") + } + } + } + + + Item { + width: parent.width + height: windDirectionIcon.height + windDirectionIcon.y + + Label { + id: accumulatedPrecipitationLabel + color: Theme.highlightColor + font.pixelSize: Theme.fontSizeHuge + text: model ? parseFloat(model.accumulatedPrecipitation).toLocaleString(Qt.locale(), 'f', 1) : "" + anchors { + verticalCenter: windDirectionIcon.verticalCenter + left: Screen.sizeCategory >= Screen.Large ? undefined : parent.left + horizontalCenter: Screen.sizeCategory >= Screen.Large ? windDirectionIcon.horizontalCenter : undefined + leftMargin: Theme.horizontalPageMargin + horizontalCenterOffset: -Screen.width/4 + } + } + Label { + id: precipitationMetricLabel + + anchors { + left: accumulatedPrecipitationLabel.right + baseline: accumulatedPrecipitationLabel.baseline + } + color: Theme.highlightColor + //: Millimeters, short form + //% "mm" + text: qsTrId("weather-la-mm") + font.pixelSize: Theme.fontSizeExtraSmall + } + Label { + anchors { + left: accumulatedPrecipitationLabel.left + top: accumulatedPrecipitationLabel.baseline + topMargin: Theme.paddingSmall + } + width: parent.width/3 - Theme.paddingLarge + wrapMode: Text.WordWrap + color: Theme.secondaryHighlightColor + font.pixelSize: Theme.fontSizeExtraSmall + //% "Precipitation" + text: qsTrId("weather-la-precipitation") + } + Image { + id: windDirectionIcon + y: Screen.sizeCategory >= Screen.Large ? 0 : -Theme.paddingLarge + source: "image://theme/graphic-weather-wind-direction?" + Theme.highlightColor + anchors.horizontalCenter: parent.horizontalCenter + // possible rotation values are 0 and multiplies of 45 degrees + // once valid value is obtained enable animation on further changes + rotation: model ? model.windDirection + 180 : -1 // place opposite to indicate where wind is going to + onRotationChanged: if (rotation !== -1) rotationBehavior.enabled = true + Behavior on rotation { + id: rotationBehavior + enabled: false + RotationAnimator { + duration: 200 + easing.type: Easing.InOutQuad + direction: RotationAnimator.Shortest + } + } + } + Label { + id: windSpeedLabel + color: Theme.highlightColor + font.pixelSize: Theme.fontSizeHuge + anchors.centerIn: windDirectionIcon + text: model ? model.maximumWindSpeed : "" + } + Label { + anchors { + horizontalCenter: windSpeedLabel.horizontalCenter + top: windSpeedLabel.baseline + topMargin: Theme.paddingSmall + } + + // TODO: localize + //: Meters per second, short form + //% "m/s" + text: qsTrId("weather-la-m_per_s") + color: Theme.secondaryHighlightColor + font.pixelSize: Theme.fontSizeExtraSmall + } + Label { + id: temperatureHighLabel + color: Theme.highlightColor + font.pixelSize: Theme.fontSizeHuge + text: model ? TemperatureConverter.format(model.high) : "" + anchors { + verticalCenter: windDirectionIcon.verticalCenter + right: Screen.sizeCategory >= Screen.Large ? undefined : parent.right + horizontalCenter: Screen.sizeCategory >= Screen.Large ? windDirectionIcon.horizontalCenter : undefined + rightMargin: Theme.horizontalPageMargin + horizontalCenterOffset: Screen.width/4 + } + } + Label { + color: Theme.secondaryHighlightColor + font.pixelSize: Theme.fontSizeExtraSmall + anchors { + top: temperatureHighLabel.baseline + topMargin: Theme.paddingSmall + right: temperatureHighLabel.right + } + //: Shows daily low temperature as label, e.g. "Low -3°". Degree symbol comes from outside. + //% "Low %1" + text: model ? qsTrId("weather-la-daily_low_temperature").arg(TemperatureConverter.format(model.low)) : "" + } + } + Item { width: 1; height: Theme.paddingMedium } + Label { + color: Theme.highlightColor + anchors.horizontalCenter: parent.horizontalCenter + width: Screen.sizeCategory >= Screen.Large && isPortrait ? Screen.width/2 + : parent.width - 2*Theme.horizontalPageMargin + wrapMode: Text.Wrap + font.pixelSize: Theme.fontSizeLarge + horizontalAlignment: Text.AlignHCenter + text: model ? model.description : "" + height: lineCount === 1 && isPortrait ? 2*implicitHeight : implicitHeight + } + Item { width: 1; height: Theme.paddingMedium } + Grid { + width: parent.width + columns: isPortrait ? 1 : 2 + WeatherDetailItem { + //% "Weather station" + label: qsTrId("weather-la-weather_station") + value: weather.station || "" + visible: value + } + WeatherDetailItem { + //% "Date" + label: qsTrId("weather-la-weather_date") + value: { + if (model) { + var dateString = Format.formatDate(model.timestamp, Format.DateLong) + return dateString.charAt(0).toUpperCase() + dateString.substr(1) + } + return "" + } + } + WeatherDetailItem { + //% "Cloudiness" + label: qsTrId("weather-la-cloudiness") + value: model ? model.cloudiness + Qt.locale().percent : "" + } + WeatherDetailItem { + //% "Precipitation rate" + label: qsTrId("weather-la-precipitationrate") + value: model ? model.precipitationRate : "" + } + WeatherDetailItem { + //% "Precipitation type" + label: qsTrId("weather-la-precipitationtype") + value: model ? model.precipitationType : "" + } + } + + ProviderDisclaimer { + weather: root.weather + topMargin: Theme.paddingMedium + bottomMargin: Theme.paddingLarge + } + } +} diff --git a/usr/lib/qt5/qml/Sailfish/Weather/WeatherForecastList.qml b/usr/lib/qt5/qml/Sailfish/Weather/WeatherForecastList.qml new file mode 100644 index 00000000..9ec9198a --- /dev/null +++ b/usr/lib/qt5/qml/Sailfish/Weather/WeatherForecastList.qml @@ -0,0 +1,21 @@ +import QtQuick 2.6 +import Sailfish.Silica 1.0 +import Sailfish.Weather 1.0 + +SilicaListView { + property bool active: true + property int columnCount: 6 + + opacity: active ? 1.0 : 0.0 + Behavior on opacity { FadeAnimator {}} + width: parent.width + property int itemWidth: width/columnCount + property int itemHeight + implicitHeight: 2*(Screen.sizeCategory >= Screen.Large ? Theme.itemSizeExtraLarge : Theme.itemSizeLarge) + height: Math.max(itemHeight, implicitHeight) + + clip: true // limit to 6 forecasts + currentIndex: -1 + interactive: false + orientation: ListView.Horizontal +} diff --git a/usr/lib/qt5/qml/Sailfish/Weather/WeatherForecastModel.qml b/usr/lib/qt5/qml/Sailfish/Weather/WeatherForecastModel.qml new file mode 100644 index 00000000..c0095dd3 --- /dev/null +++ b/usr/lib/qt5/qml/Sailfish/Weather/WeatherForecastModel.qml @@ -0,0 +1,111 @@ +import QtQuick 2.0 +import Sailfish.Weather 1.0 +import "WeatherModel.js" as WeatherModel + +ListModel { + id: root + + property bool hourly + property var weather + property alias active: model.active + property date timestamp + property alias status: model.status + property int visibleCount: 6 + property int minimumHourlyRange: 4 + readonly property bool loading: forecastModel.status == Weather.Loading + readonly property int locationId: weather ? weather.locationId : -1 + + onLocationIdChanged: { + model.status = Weather.Null + clear() + } + + function attemptReload(userRequested) { + model.attemptReload(userRequested) + } + + function reload(userRequested) { + model.reload(userRequested) + } + + readonly property WeatherRequest model: WeatherRequest { + id: model + + source: root.locationId > 0 ? + "https://pfa.foreca.com/api/v1/forecast/" + + (hourly ? "hourly/" : "daily/") + root.locationId : "" + + // update allowed every half hour for hourly weather, every 3 hours for daily weather + property int maxUpdateInterval: hourly ? 30*60*1000 : 180*60*1000 + function updateAllowed() { + return status !== Weather.Unauthorized && (status === Weather.Error || status === Weather.Null || WeatherModel.updateAllowed(maxUpdateInterval)) + } + + onRequestFinished: { + var forecast = result["forecast"] + if (result.length === 0 || forecast.length === "") { + error = true + } else { + var weatherData = [] + for (var i = 0; i < forecast.length; i++) { + var data = forecast[i] + var weather = WeatherModel.getWeatherData(data) + if (hourly) { + if (i % 3 !== 0) continue + weather.timestamp = new Date(data.time) + weather.temperature = data.temperature + } else { + var dateArray = data.date.split("-") + weather.timestamp = new Date(parseInt(dateArray[0]), + parseInt(dateArray[1] - 1), + parseInt(dateArray[2])) + weather.accumulatedPrecipitation = data.precipAccum + weather.maximumWindSpeed = data.maxWindSpeed + weather.windDirection = data.windDir + weather.high = data.maxTemp + weather.low = data.minTemp + } + weatherData[weatherData.length] = weather + } + + if (hourly) { + var minimumTemperature = weatherData[0].temperature + var maximumTemperature = weatherData[0].temperature + for (i = 1; i < visibleCount + 1; i++) { + var temperature = weatherData[i].temperature + minimumTemperature = Math.min(minimumTemperature, temperature) + maximumTemperature = Math.max(maximumTemperature, temperature) + } + var range = maximumTemperature - minimumTemperature + if (range < minimumHourlyRange) { + minimumTemperature -= Math.floor((minimumHourlyRange - range ) / 2) + range = minimumHourlyRange + } + + var array = [] + for (i = 0; i < visibleCount + 1; i++) { + weatherData[i].relativeTemperature = (weatherData[i].temperature - minimumTemperature) / range + } + } + + while (root.count > weatherData.length) { + root.remove(weatherData.length) + } + + for (i = 0; i < weatherData.length; i++) { + if (i < root.count) { + root.set(i, weatherData[i]) + } else { + root.append(weatherData[i]) + } + } + } + } + + onStatusChanged: { + if (status === Weather.Error) { + console.log("WeatherForecastModel - could not obtain forecast weather data", weather ? weather.city : "", weather ? weather.locationId : "") + } + } + } +} diff --git a/usr/lib/qt5/qml/Sailfish/Weather/WeatherHeader.qml b/usr/lib/qt5/qml/Sailfish/Weather/WeatherHeader.qml new file mode 100644 index 00000000..46d588f1 --- /dev/null +++ b/usr/lib/qt5/qml/Sailfish/Weather/WeatherHeader.qml @@ -0,0 +1,97 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Sailfish.Weather 1.0 + +MouseArea { + id: root + + property var weather + property bool highlighted: pressed && containsMouse + property date timestamp: weather ? weather.timestamp : new Date() + + enabled: weather && weather.populated + width: parent.width + height: childrenRect.height + + Behavior on height { NumberAnimation { duration: 200; easing.type: Easing.InOutQuad } } + WeatherImage { + id: weatherImage + x: Theme.horizontalPageMargin + y: 2*Theme.horizontalPageMargin + highlighted: root.highlighted + height: sourceSize.height > 0 ? sourceSize.height : 256*Theme.pixelRatio + weatherType: weather && weather.weatherType.length > 0 ? weather.weatherType : "" + } + PageHeader { + id: pageHeader + property int offset: _titleItem.y + _titleItem.height + anchors { + left: weatherImage.right + // weather graphics have some inline padding and rounded edges to give space for header + leftMargin: -Theme.itemSizeMedium + right: parent.right + } + title: weather ? (weather.city + ", " + weather.country + + (weather.adminArea ? (", " + weather.adminArea) : "")) : "" + } + Column { + id: column + anchors { + top: pageHeader.top + topMargin: pageHeader.offset + left: weatherImage.right + right: parent.right + rightMargin: Theme.horizontalPageMargin + } + + spacing: -Theme.paddingMedium + Item { + width: parent.width + height: secondaryLabel.height + timestampLabel.height + Label { + id: secondaryLabel + font.pixelSize: Theme.fontSizeSmall + color: Theme.secondaryHighlightColor + //% "Current location" + text: qsTrId("weather-la-current_location") + horizontalAlignment: Text.AlignRight + wrapMode: Text.Wrap + width: parent.width + } + Label { + id: timestampLabel + width: parent.width + wrapMode: Text.Wrap + anchors.top: secondaryLabel.bottom + font.pixelSize: Theme.fontSizeSmall + horizontalAlignment: Text.AlignRight + color: Theme.secondaryHighlightColor + text: Format.formatDate(timestamp, Format.TimeValue) + } + } + TemperatureLabel { + anchors.right: parent.right + temperature: weather ? TemperatureConverter.formatWithoutUnit(weather.temperature) : "" + feelsLikeTemperature: weather ? TemperatureConverter.formatWithoutUnit(weather.feelsLikeTemperature) : "" + color: highlighted ? Theme.highlightColor : Theme.primaryColor + } + } + Label { + anchors { + top: column.bottom + topMargin: -Theme.paddingMedium + left: parent.left + right: parent.right + leftMargin: Theme.horizontalPageMargin + rightMargin: Theme.horizontalPageMargin + } + color: highlighted ? Theme.secondaryHighlightColor : Theme.secondaryColor + font { + pixelSize: Theme.fontSizeExtraLarge + family: Theme.fontFamilyHeading + } + wrapMode: Text.Wrap + horizontalAlignment: Text.AlignRight + text: weather ? weather.description : "" + } +} diff --git a/usr/lib/qt5/qml/Sailfish/Weather/WeatherImage.qml b/usr/lib/qt5/qml/Sailfish/Weather/WeatherImage.qml new file mode 100644 index 00000000..44be8ba4 --- /dev/null +++ b/usr/lib/qt5/qml/Sailfish/Weather/WeatherImage.qml @@ -0,0 +1,10 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 + +Image { + property var weatherType + property bool highlighted + source: weatherType.length > 0 ? "image://theme/graphic-weather-" + weatherType + + (highlighted ? "?" + Theme.highlightColor : "") + : "" +} diff --git a/usr/lib/qt5/qml/Sailfish/Weather/WeatherIndicator.qml b/usr/lib/qt5/qml/Sailfish/Weather/WeatherIndicator.qml new file mode 100644 index 00000000..129291af --- /dev/null +++ b/usr/lib/qt5/qml/Sailfish/Weather/WeatherIndicator.qml @@ -0,0 +1,50 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Sailfish.Weather 1.0 + +Row { + id: root + + property alias weather: savedWeathersModel.currentWeather + property alias autoRefresh: savedWeathersModel.autoRefresh + property alias active: weatherModel.active + property alias temperatureFont: temperatureLabel.font + + anchors.horizontalCenter: parent.horizontalCenter + height: image.height + spacing: Theme.paddingMedium + visible: !!weather + + Image { + id: image + anchors.verticalCenter: parent.verticalCenter + source: weather && weather.weatherType.length > 0 + ? "image://theme/icon-m-weather-" + weather.weatherType + : "" + // JB#43864 don't yet have weather graphics in small-plus size, so set size manually + sourceSize.width: Theme.iconSizeSmallPlus + sourceSize.height: Theme.iconSizeSmallPlus + } + + Label { + id: temperatureLabel + anchors.verticalCenter: image.verticalCenter + text: weather ? TemperatureConverter.format(weather.temperature) : "" + color: Theme.primaryColor + font { + pixelSize: Theme.fontSizeExtraLarge + family: Theme.fontFamilyHeading + } + } + + SavedWeathersModel { + id: savedWeathersModel + autoRefresh: true + } + + WeatherModel { + id: weatherModel + weather: savedWeathersModel.currentWeather + savedWeathers: savedWeathersModel + } +} diff --git a/usr/lib/qt5/qml/Sailfish/Weather/WeatherModel.js b/usr/lib/qt5/qml/Sailfish/Weather/WeatherModel.js new file mode 100644 index 00000000..9f005409 --- /dev/null +++ b/usr/lib/qt5/qml/Sailfish/Weather/WeatherModel.js @@ -0,0 +1,157 @@ +var lastUpdate = new Date() + +function updateAllowed(interval) { + // only update automatically if more than minutes has + // passed since the last update (default 30mins: 30*60*1000) + // or the date has changed + interval = interval === undefined ? 30*60*1000 : interval + var now = new Date() + var updateAllowed = now.getDate() != lastUpdate.getDate() || (now - interval > lastUpdate) + if (updateAllowed) { + lastUpdate = now + } + return updateAllowed +} + +function getWeatherData(weather) { + var precipitationRateCode = weather.symbol.charAt(2) + var precipitationRate = "" + switch (precipitationRateCode) { + case '0': + //% "No precipitation" + precipitationRate = qsTrId("weather-la-precipitation_none") + break + case '1': + //% "Slight precipitation" + precipitationRate = qsTrId("weather-la-precipitation_slight") + break + case '2': + //% "Showers" + precipitationRate = qsTrId("weather-la-precipitation_showers") + break + case '3': + //% "Precipitation" + precipitationRate = qsTrId("weather-la-precipitation_normal") + break + case '4': + //% "Thunder" + precipitationRate = qsTrId("weather-la-precipitation_thunder") + break + default: + console.log("WeatherModel warning: invalid precipitation rate code", precipitationRateCode) + break + } + + var precipitationType = "" + if (precipitationRateCode === '0') { // no rain + //% "None" + precipitationType = qsTrId("weather-la-precipitationtype_none") + } else { + var precipitationTypeCode = weather.symbol.charAt(3) + switch (precipitationTypeCode) { + case '0': + //% "Rain" + precipitationType = qsTrId("weather-la-precipitationtype_rain") + break + case '1': + //% "Sleet" + precipitationType = qsTrId("weather-la-precipitationtype_sleet") + break + case '2': + //% "Snow" + precipitationType = qsTrId("weather-la-precipitationtype_snow") + break + default: + console.log("WeatherModel warning: invalid precipitation type code", precipitationTypeCode) + break + } + } + + var data = { + "description": description(weather.symbol), + "weatherType": weatherType(weather.symbol), + "cloudiness": (100*parseInt(weather.symbol.charAt(1))/4), + "precipitationRate": precipitationRate, + "precipitationType": precipitationType + } + return data +} + +function weatherType(code) { + // just direct mapping, but ensure we receive valid data + if (code.length === 4) { + return code + } else { + console.warn("Invalid weather code") + return "" + } +} + +function description(code) { + var localizations = { + //% "Clear" + "000": qsTrId("weather-la-description_clear"), + //% "Mostly clear" + "100": qsTrId("weather-la-description_mostly_clear"), + //% "Partly cloudy" + "200": qsTrId("weather-la-description_partly_cloudy"), + //% "Cloudy" + "300": qsTrId("weather-la-description_cloudy"), + //% "Overcast" + "400": qsTrId("weather-la-description_overcast"), + //% "Thin high clouds" + "500": qsTrId("weather-la-description-thin_high_clouds"), + //% "Fog" + "600": qsTrId("weather-la-description-fog"), + //% "Partly cloudy and light rain" + "210": qsTrId("weather-la-description_partly_cloudy_and_light_rain"), + //% "Cloudy and light rain" + "310": qsTrId("weather-la-description_cloudy_and_light_rain"), + //% "Overcast and light rain" + "410": qsTrId("weather-la-description_overcast_and_light_rain"), + //% "Partly cloudy and showers" + "220": qsTrId("weather-la-description_partly_cloudy_and_showers"), + //% "Cloudy and showers" + "320": qsTrId("weather-la-description_cloudy_and_showers"), + //% "Overcast and showers" + "420": qsTrId("weather-la-description_overcast_and_showers"), + //% "Overcast and rain" + "430": qsTrId("weather-la-description_overcast_and_rain"), + //% "Partly cloudy, possible thunderstorms with rain" + "240": qsTrId("weather-la-description_partly_cloudy_possible_thunderstorms_with_rain"), + //% "Cloudy, thunderstorms with rain" + "340": qsTrId("weather-la-description_cloudy_thunderstorms_with_rain"), + //% "Overcast, thunderstorms with rain" + "440": qsTrId("weather-la-description_overcast_thunderstorms_with_rain"), + //% "Partly cloudy and light wet snow" + "211": qsTrId("weather-la-description_partly_cloudy_and_light_wet_snow"), + //% "Cloudy and light wet snow" + "311": qsTrId("weather-la-description_cloudy_and_light_wet_snow"), + //% "Overcast and light wet snow" + "411": qsTrId("weather-la-description_overcast_and_light_wet_snow"), + //% "Partly cloudy and wet snow showers" + "221": qsTrId("weather-la-description_partly_cloudy_and_wet_snow_showers"), + //% "Cloudy and wet snow showers" + "321": qsTrId("weather-la-description_cloudy_and_wet_snow_showers"), + //% "Overcast and wet snow showers" + "421": qsTrId("weather-la-description_overcast_and_wet_snow_showers"), + //% "Overcast and wet snow" + "431": qsTrId("weather-la-description_overcast_and_wet_snow"), + //% "Partly cloudy and light snow" + "212": qsTrId("weather-la-description_partly_cloudy_and_light_snow"), + //% "Cloudy and light snow" + "312": qsTrId("weather-la-description_cloudy_and_light_snow"), + //% "Overcast and light snow" + "412": qsTrId("weather-la-description_overcast_and_light_snow"), + //% "Partly cloudy and snow showers" + "222": qsTrId("weather-la-description_partly_cloudy_and_snow_showers"), + //% "Cloudy and snow showers" + "322": qsTrId("weather-la-description_cloudy_and_snow_showers"), + //% "Overcast and snow showers" + "422": qsTrId("weather-la-description_overcast_and_snow_showers"), + //% "Overcast and snow" + "432": qsTrId("weather-la-description_overcast_and_snow") + } + + return localizations[code.substr(1,3)] +} diff --git a/usr/lib/qt5/qml/Sailfish/Weather/WeatherModel.qml b/usr/lib/qt5/qml/Sailfish/Weather/WeatherModel.qml new file mode 100644 index 00000000..3deaff59 --- /dev/null +++ b/usr/lib/qt5/qml/Sailfish/Weather/WeatherModel.qml @@ -0,0 +1,81 @@ +import QtQuick 2.0 +import Sailfish.Weather 1.0 +import "WeatherModel.js" as WeatherModel + +WeatherRequest { + property var weather + property var savedWeathers + property date timestamp: new Date() + readonly property int locationId: !!weather ? weather.locationId : -1 + + readonly property WeatherRequest latestObservation: WeatherRequest { + property var weatherJson + // Store our own copy of locationId, since parent.locationId may change mid-fetch + property int requestedLocationId: -1 + + active: false + source: requestedLocationId > 0 + ? "https://pfa.foreca.com/api/v1/observation/latest/" + requestedLocationId + : "" + onRequestFinished: { + if (!weatherJson) return + + active = false; + var observations = result["observations"] + if (observations.length > 0) { + weatherJson["station"] = observations[0].station + } + savedWeathersModel.update(requestedLocationId, weatherJson) + } + + onStatusChanged: { + if (status === Weather.Error || status == Weather.Unauthorized) { + if (savedWeathers) { + savedWeathers.setErrorStatus(requestedLocationId, status) + } + + console.log("WeatherModel - could not obtain weather station data", weather ? weather.city : "", weather ? weather.locationId : "") + } + } + } + + source: locationId > 0 ? "https://pfa.foreca.com/api/v1/current/" + locationId : "" + + function updateAllowed() { + return status === Weather.Null || status === Weather.Error || WeatherModel.updateAllowed() + } + + onRequestFinished: { + var current = result["current"] + if (result.length === 0 || current.temperature === "") { + status = Weather.Error + } else { + var weather = WeatherModel.getWeatherData(current) + weather.timestamp = new Date(current.time) + this.timestamp = weather.timestamp + + weather.temperature = current.temperature + weather.feelsLikeTemperature = current.feelsLikeTemp + var json = { + "temperature": weather.temperature, + "feelsLikeTemperature": weather.feelsLikeTemperature, + "weatherType": weather.weatherType, + "description": weather.description, + "timestamp": weather.timestamp + } + latestObservation.weatherJson = json + latestObservation.requestedLocationId = locationId + latestObservation.active = true + } + } + + onStatusChanged: { + if (status === Weather.Error || status == Weather.Unauthorized) { + if (savedWeathers) { + savedWeathers.setErrorStatus(locationId, status) + } + + console.log("WeatherModel - could not obtain weather data", weather ? weather.city : "", weather ? weather.locationId : "") + } + } +} diff --git a/usr/lib/qt5/qml/Sailfish/Weather/WeatherPage.qml b/usr/lib/qt5/qml/Sailfish/Weather/WeatherPage.qml new file mode 100644 index 00000000..d3d5937e --- /dev/null +++ b/usr/lib/qt5/qml/Sailfish/Weather/WeatherPage.qml @@ -0,0 +1,118 @@ +import QtQuick 2.2 +import Sailfish.Silica 1.0 +import Sailfish.Weather 1.0 + +Page { + id: root + + property var weather + property var weatherModel + property int currentIndex + property bool current + + SilicaFlickable { + anchors { + top: parent.top + bottom: weatherForecastList.top + } + clip: true + width: parent.width + contentHeight: weatherHeader.height + VerticalScrollDecorator {} + + PullDownMenu { + visible: forecastModel.count > 0 + busy: forecastModel.status === Weather.Loading + + MenuItem { + //% "More information" + text: qsTrId("weather-me-more_information") + onClicked: Qt.openUrlExternally("http://foreca.mobi/spot.php?l=" + root.weather.locationId) + } + MenuItem { + //% "Update" + text: qsTrId("weather-me-update") + onClicked: forecastModel.reload(true) + } + } + WeatherDetailsHeader { + id: weatherHeader + + current: root.current + today: root.currentIndex === 0 + opacity: forecastModel.count > 0 ? 1.0 : 0.0 + weather: root.weather + status: forecastModel.status + model: forecastModel.count > 0 ? forecastModel.get(currentIndex) : null + Behavior on opacity { OpacityAnimator { easing.type: Easing.InOutQuad; duration: 400 } } + } + PlaceholderItem { + y: Theme.itemSizeSmall + Theme.itemSizeLarge*2 + error: forecastModel.status === Weather.Error + unauthorized: forecastModel.status === Weather.Unauthorized + enabled: forecastModel.count === 0 + onReload: forecastModel.reload(true) + } + } + + PanelBackground { + width: parent.width + height: weatherForecastList.height + anchors.bottom: parent.bottom + opacity: forecastModel.count > 0 ? 1.0 : 0.0 + Behavior on opacity { OpacityAnimator { easing.type: Easing.InOutQuad; duration: 400 } } + } + SilicaListView { + id: weatherForecastList + + readonly property int availableWidth: Screen.sizeCategory >= Screen.Large ? Screen.width : root.width + readonly property int itemWidth: availableWidth/forecastModel.visibleCount + + width: forecastModel.visibleCount * itemWidth + opacity: forecastModel.count > 0 ? 1.0 : 0.0 + Behavior on opacity { OpacityAnimator { easing.type: Easing.InOutQuad; duration: 400 } } + + model: WeatherForecastModel { + id: forecastModel + weather: root.weather + timestamp: weatherModel.timestamp + active: root.status === PageStatus.Active && Qt.application.active + } + + clip: true + orientation: ListView.Horizontal + height: 2*(Screen.sizeCategory >= Screen.Large ? Theme.itemSizeExtraLarge : Theme.itemSizeLarge) + anchors { + bottom: parent.bottom + horizontalCenter: parent.horizontalCenter + } + delegate: MouseArea { + readonly property bool down: pressed && containsMouse + + onClicked: root.currentIndex = model.index + + width: weatherForecastList.itemWidth + height: weatherForecastList.height + + Rectangle { + visible: down || root.currentIndex == model.index + anchors.fill: parent + gradient: Gradient { + GradientStop { + position: 0.0 + color: "transparent" + } + GradientStop { + position: 1.0 + color: Theme.rgba(Theme.highlightBackgroundColor, + Theme.colorScheme === Theme.LightOnDark ? 0.3 : 0.5) + } + } + } + + DailyForecastItem { + highlighted: down + } + } + } +} diff --git a/usr/lib/qt5/qml/Sailfish/Weather/WeatherRequest.qml b/usr/lib/qt5/qml/Sailfish/Weather/WeatherRequest.qml new file mode 100644 index 00000000..01c7d932 --- /dev/null +++ b/usr/lib/qt5/qml/Sailfish/Weather/WeatherRequest.qml @@ -0,0 +1,93 @@ +import QtQuick 2.0 +import Sailfish.Weather 1.0 +import "ForecaToken.js" as Token + +QtObject { + id: root + property bool active: true + property string source + readonly property bool online: WeatherConnectionHelper.online + property int status: Weather.Null + property string token + property var request + + signal requestFinished(var result) + + onTokenChanged: sendRequest() + onActiveChanged: if (active) attemptReload() + onOnlineChanged: if (online) attemptReload() + onSourceChanged: if (source.length > 0) attemptReload() + Component.onCompleted: Token.fetchToken(this) + + // Note: this is overridden in WeatherModel and WeatherForecastModel + function updateAllowed() { + return active + } + + function attemptReload(userRequested) { + if (updateAllowed()) { + reload(userRequested) + } else if (userRequested) { + console.log("Weather update not allowed (not active)") + } + } + + // userRequested: true to open a connection dialog in case + // there's no currently available connection; + // false for the request to fail silently + function reload(userRequested) { + if (online && source.length > 0) { + status = Weather.Loading + if (Token.fetchToken(root)) { + sendRequest() + } + } else if (source.length === 0) { + status = Weather.Null + } else { + status = Weather.Error + if (userRequested) { + WeatherConnectionHelper.attemptToConnectNetwork() + } else { + WeatherConnectionHelper.requestNetwork() + } + } + } + + function sendRequest() { + if (source.length > 0 && token.length > 0 && !request) { + status = Weather.Loading + request = new XMLHttpRequest() + timeout.restart() + + // Send the proper header information along with the request + request.onreadystatechange = function() { // Call a function when the state changes. + if (request.readyState == XMLHttpRequest.DONE) { + timeout.stop() + if (request.status === 200) { + var data = JSON.parse(request.responseText) + requestFinished(data) + status = Weather.Ready + } else { + console.warn("Failed to obtain weather data. HTTP error code: " + request.status) + status = Weather.Error + } + request = undefined + } + } + request.open("GET", source + "&token=" + token) + request.send() + } + } + property Timer timeout: Timer { + id: timeout + interval: 8000 + onTriggered: { + if (request) { + request.abort() + request = undefined + console.warn("Failed to obtain weather data. The request timed out after 8 seconds") + status = Weather.Error + } + } + } +} diff --git a/usr/lib/qt5/qml/Sailfish/Weather/qmldir b/usr/lib/qt5/qml/Sailfish/Weather/qmldir new file mode 100644 index 00000000..778183d7 --- /dev/null +++ b/usr/lib/qt5/qml/Sailfish/Weather/qmldir @@ -0,0 +1,24 @@ +module Sailfish.Weather +plugin sailfishweatherplugin +singleton LocationDetection 1.0 LocationDetection.qml +singleton WeatherConnectionHelper 1.0 WeatherConnectionHelper.qml +singleton TemperatureConverter 1.0 TemperatureConverter.qml +CurrentLocationModel 1.0 CurrentLocationModel.qml +LocationsModel 1.0 LocationsModel.qml +WeatherPage 1.0 WeatherPage.qml +WeatherModel 1.0 WeatherModel.qml +WeatherRequest 1.0 WeatherRequest.qml +WeatherForecastList 1.0 WeatherForecastList.qml +WeatherForecastModel 1.0 WeatherForecastModel.qml +HourlyForecastItem 1.0 HourlyForecastItem.qml +DailyForecastItem 1.0 DailyForecastItem.qml +WeatherDetailItem 1.0 WeatherDetailItem.qml +TemperatureLabel 1.0 TemperatureLabel.qml +WeatherImage 1.0 WeatherImage.qml +WeatherHeader 1.0 WeatherHeader.qml +WeatherDetailsHeader 1.0 WeatherDetailsHeader.qml +PlaceholderItem 1.0 PlaceholderItem.qml +ProviderDisclaimer 1.0 ProviderDisclaimer.qml +WeatherBanner 1.0 WeatherBanner.qml +WeatherIndicator 1.0 WeatherIndicator.qml +WeatherForecastList 1.0 WeatherForecastList.qml diff --git a/usr/lib/qt5/qml/Sailfish/WebEngine/qmldir b/usr/lib/qt5/qml/Sailfish/WebEngine/qmldir index 92ed8258..7883514b 100644 --- a/usr/lib/qt5/qml/Sailfish/WebEngine/qmldir +++ b/usr/lib/qt5/qml/Sailfish/WebEngine/qmldir @@ -1,2 +1,3 @@ module Sailfish.WebEngine plugin sailfishwebengineplugin +typeinfo plugins.qmltypes diff --git a/usr/lib/qt5/qml/Sailfish/WebView/Controls/TextSelectionController.qml b/usr/lib/qt5/qml/Sailfish/WebView/Controls/TextSelectionController.qml index 5c8f2845..71bd83cc 100644 --- a/usr/lib/qt5/qml/Sailfish/WebView/Controls/TextSelectionController.qml +++ b/usr/lib/qt5/qml/Sailfish/WebView/Controls/TextSelectionController.qml @@ -11,7 +11,7 @@ import QtQuick 2.1 import Sailfish.Silica 1.0 -import MeeGo.QOfono 0.2 +import QOfono 0.2 MouseArea { id: root diff --git a/usr/lib/qt5/qml/Sailfish/WebView/Controls/qmldir b/usr/lib/qt5/qml/Sailfish/WebView/Controls/qmldir index dfd16d21..adaa8483 100644 --- a/usr/lib/qt5/qml/Sailfish/WebView/Controls/qmldir +++ b/usr/lib/qt5/qml/Sailfish/WebView/Controls/qmldir @@ -1,5 +1,6 @@ module Sailfish.WebView.Controls plugin sailfishwebviewcontrolsplugin +typeinfo plugins.qmltypes depends Sailfish.WebEngine 1.0 TextSelectionController 1.0 TextSelectionController.qml TextSelectionHandle 1.0 TextSelectionHandle.qml diff --git a/usr/lib/qt5/qml/Sailfish/WebView/Pickers/qmldir b/usr/lib/qt5/qml/Sailfish/WebView/Pickers/qmldir index 607787ba..42ef05f0 100644 --- a/usr/lib/qt5/qml/Sailfish/WebView/Pickers/qmldir +++ b/usr/lib/qt5/qml/Sailfish/WebView/Pickers/qmldir @@ -1,5 +1,6 @@ module Sailfish.WebView.Pickers plugin sailfishwebviewpickersplugin +typeinfo plugins.qmltypes MultiSelectDialog 1.0 MultiSelectDialog.qml SingleSelectPage 1.0 SingleSelectPage.qml PickerCreator 1.0 PickerCreator.qml diff --git a/usr/lib/qt5/qml/Sailfish/WebView/Popups/ContextMenu.qml b/usr/lib/qt5/qml/Sailfish/WebView/Popups/ContextMenu.qml index c8885494..99382636 100644 --- a/usr/lib/qt5/qml/Sailfish/WebView/Popups/ContextMenu.qml +++ b/usr/lib/qt5/qml/Sailfish/WebView/Popups/ContextMenu.qml @@ -9,8 +9,9 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at http://mozilla.org/MPL/2.0/. */ -import QtQuick 2.0 +import QtQuick 2.6 import Sailfish.Silica 1.0 +import Sailfish.Silica.private 1.0 import Sailfish.WebView.Popups 1.0 ContextMenuInterface { @@ -26,8 +27,8 @@ ContextMenuInterface { // Image has source. That can be used to distinguish from other links. readonly property bool isImage: !isJavascriptFunction && imageSrc.length > 0 - // All except image elements are considered as links. - readonly property bool isLink: !isJavascriptFunction && linkHref.length > 0 && imageSrc.length === 0 + // All elements are considered as links. + readonly property bool isLink: !isJavascriptFunction && linkHref.length > 0 // Separate hyper text links from other content types. readonly property bool isHyperTextLink: linkProtocol === "http" || linkProtocol === "https" || linkProtocol === "file" @@ -37,7 +38,7 @@ ContextMenuInterface { readonly property bool isTel: linkProtocol === "tel" readonly property bool isSMS: linkProtocol === "sms" readonly property bool isGeo: linkProtocol === "geo" - readonly property bool isNavigable: isLink && !knownPlatformProtocol && !isImage + readonly property bool isNavigable: isLink && !knownPlatformProtocol width: parent.width height: parent.height @@ -51,6 +52,7 @@ ContextMenuInterface { function _hide() { opacity = 0.0 + expander.open = false } Rectangle { @@ -60,201 +62,237 @@ ContextMenuInterface { GradientStop { position: 1.0; color: Theme.rgba(Theme.highlightDimmerColor, .91) } } - Column { - width: parent.width - spacing: Theme.paddingMedium - anchors.top: parent.top - anchors.topMargin: Theme.paddingLarge*2 - - Label { - id: title - anchors.horizontalCenter: parent.horizontalCenter - visible: root.linkTitle.length > 0 - text: root.linkTitle - width: root.width - Theme.horizontalPageMargin*2 - elide: Text.ElideRight - wrapMode: Text.Wrap - maximumLineCount: 2 - color: Theme.highlightColor - font.pixelSize: Theme.fontSizeExtraLarge - horizontalAlignment: Text.AlignHCenter - opacity: Theme.opacityHigh - } + SilicaFlickable { + anchors.fill: parent + // Extra height: content topMargin + 1 x paddingLarge to end. + contentHeight: content.height + Theme.paddingLarge * 3 - Label { - anchors.horizontalCenter: parent.horizontalCenter - color: Theme.highlightColor - text: root.imageSrc.length > 0 ? root.imageSrc : root.url - width: root.width - Theme.horizontalPageMargin*2 - wrapMode: Text.Wrap - elide: Text.ElideRight - maximumLineCount: landscape ? 1 : 4 - font.pixelSize: title.visible ? Theme.fontSizeMedium : Theme.fontSizeExtraLarge - horizontalAlignment: Text.AlignHCenter - opacity: Theme.opacityHigh - } - } + VerticalScrollDecorator {} + + Column { + id: content + + width: parent.width + spacing: Theme.paddingMedium + anchors.top: parent.top + anchors.topMargin: Theme.paddingLarge*2 + + Expander { + id: expander + horizontalMargin: Theme.horizontalPageMargin - Column { - id: menu - - property Item highlightedItem - - anchors.bottom: parent.bottom - anchors.bottomMargin: landscape ? Theme.paddingLarge : Theme.itemSizeSmall - width: parent.width - - MenuItem { - visible: !isHyperTextLink && !isImage - text: { - if (isMailto) { - //% "Write email" - return qsTrId("sailfish_components_webview_popups-me-write-email") - } else if (isTel) { - //: Call, context of calling via voice call - //% "Call" - return qsTrId("sailfish_components_webview_popups-me-call") - } else if (isSMS) { - //: Send message (sms) - //% "Send message" - return qsTrId("sailfish_components_webview_popups-me-send-message") - } else { - //: Open link in current tab - //% "Open link" - return qsTrId("sailfish_components_webview_popups-me-open_link") + width: parent.width + collapsedHeight: title.y + Math.min(title.height, 4 * fontMetrics.height) + expandedHeight: title.y + title.height + Theme.paddingLarge + Theme.paddingMedium + + FontMetrics { + id: fontMetrics + font: title.font } - } - onClicked: { - root._hide() - Qt.openUrlExternally(root.linkHref) - } - } + Label { + id: title + anchors.horizontalCenter: parent.horizontalCenter + visible: root.linkTitle || root.url + text: root.linkTitle || root.url + width: root.width - Theme.horizontalPageMargin*2 + wrapMode: Text.Wrap + color: Theme.highlightColor + font.pixelSize: Theme.fontSizeLarge + horizontalAlignment: Text.AlignHCenter + opacity: Theme.opacityHigh - MenuItem { - visible: root.isNavigable && !!tabModel - //: Open link in a new tab from browser context menu - //% "Open link in a new tab" - text: qsTrId("sailfish_components_webview_popups-me-open_link_in_new_tab") - onClicked: { - root._hide() - tabModel.newTab(root.linkHref, root.linkTitle) - } - } + MouseArea { + enabled: !expander.expandable + anchors { + fill: parent + topMargin: -content.anchors.topMargin + leftMargin: -Theme.horizontalPageMargin + rightMargin: -Theme.horizontalPageMargin + bottomMargin: -Theme.paddingMedium + } - MenuItem { - visible: root.isLink - //: Share link from browser context menu - //% "Share" - text: qsTrId("sailfish_components_webview_popups-me-share_link") - onClicked: { - root._hide() - webShareAction.shareLink(root.linkHref, root.linkTitle) - } - WebShareAction { - id: webShareAction + onClicked: root._hide() + } + } } - } - MenuItem { - visible: !root.isImage && root.url - //: Copy link to clipboard from context menu - //% "Copy link" - text: qsTrId("sailfish_components_webview_popups-me-copy_link_to_clipboard") - onClicked: { - root._hide() - Clipboard.text = root.url - } - } + Label { + id: imageTitle + anchors.horizontalCenter: parent.horizontalCenter + visible: text !== title.text + color: Theme.highlightColor + text: root.imageSrc.length > 0 ? root.imageSrc : root.url + width: root.width - Theme.horizontalPageMargin*2 + wrapMode: Text.Wrap + elide: Text.ElideRight + maximumLineCount: landscape ? 1 : 4 + font.pixelSize: title.visible ? Theme.fontSizeMedium : Theme.fontSizeExtraLarge + horizontalAlignment: Text.AlignHCenter + opacity: Theme.opacityHigh - MenuItem { - visible: !root.isImage && root.linkTitle && (root.linkTitle != root.url) - //: Copy text to clipboard from context menu - //% "Copy text" - text: qsTrId("sailfish_components_webview_popups-me-copy_text_to_clipboard") - onClicked: { - root._hide() - Clipboard.text = root.linkTitle + MouseArea { + anchors { + fill: parent + leftMargin: -Theme.horizontalPageMargin + rightMargin: -Theme.horizontalPageMargin + bottomMargin: -Theme.paddingMedium + } + + onClicked: root._hide() + } } - } - DownloadMenuItem { - visible: root.downloadsEnabled && root.isNavigable - //% "Save link" - text: qsTrId("sailfish_components_webview_popups-me-save_link") - targetDirectory: StandardPaths.download - linkUrl: root.linkHref - contentType: root.contentType - viewId: root.viewId - onClicked: root._hide() - } + // Padding between titles and menu. + Item { + anchors { + left: parent.left + right: parent.right + leftMargin: -Theme.horizontalPageMargin + rightMargin: -Theme.horizontalPageMargin + } + + height: Math.max(Theme.itemSizeSmall, root.height - Theme.paddingLarge*2 - expander.height - imageTitle.height - menu.height - (landscape ? Theme.paddingLarge : Theme.itemSizeSmall)) - MenuItem { - visible: root.isImage && !!tabModel - //% "Open image in a new tab" - text: qsTrId("sailfish_components_webview_popups-me-open_image_in_new_tab") - onClicked: { - root._hide() - tabModel.newTab(root.imageSrc, "") + MouseArea { + anchors.fill: parent + onClicked: root._hide() + } } - } - DownloadMenuItem { - visible: root.isImage - //: This menu item saves image to Gallery application - //% "Save image" - text: qsTrId("sailfish_components_webview_popups-me-save_image") - targetDirectory: StandardPaths.download - linkUrl: root.imageSrc - contentType: root.contentType - viewId: root.viewId - onClicked: root._hide() - } + Column { + id: menu - function highlightItem(yPos) { - var xPos = width/2 - var child = childAt(xPos, yPos) - if (!child) { - setHighlightedItem(null) - return - } - var parentItem - while (child) { - if (child && child.hasOwnProperty("__silica_menuitem") && child.enabled) { - setHighlightedItem(child) - break + width: parent.width + + ContextMenuItem { + visible: !isHyperTextLink && !isImage + text: { + if (isMailto) { + //% "Write email" + return qsTrId("sailfish_components_webview_popups-me-write-email") + } else if (isTel) { + //: Call, context of calling via voice call + //% "Call" + return qsTrId("sailfish_components_webview_popups-me-call") + } else if (isSMS) { + //: Send message (sms) + //% "Send message" + return qsTrId("sailfish_components_webview_popups-me-send-message") + } else { + //: Open link in current tab + //% "Open link" + return qsTrId("sailfish_components_webview_popups-me-open_link") + } + } + onClicked: { + root._hide() + Qt.openUrlExternally(root.linkHref) + } } - parentItem = child - yPos = parentItem.mapToItem(child, xPos, yPos).y - child = parentItem.childAt(xPos, yPos) - } - } - function setHighlightedItem(item) { - if (item === highlightedItem) { - return - } - if (highlightedItem) { - highlightedItem.down = false - } - highlightedItem = item - if (highlightedItem) { - highlightedItem.down = true - } - } - } - MouseArea { - anchors.fill: parent - onPressed: menu.highlightItem(mouse.y - menu.y) - onPositionChanged: menu.highlightItem(mouse.y - menu.y) - onCanceled: menu.setHighlightedItem(null) - onReleased: { - if (menu.highlightedItem !== null) { - menu.highlightedItem.down = false - menu.highlightedItem.clicked() - } else { - onClicked: root._hide() + ContextMenuItem { + visible: root.isNavigable && !!tabModel + //: Open link in a new tab from browser context menu + //% "Open link in a new tab" + text: qsTrId("sailfish_components_webview_popups-me-open_link_in_new_tab") + onClicked: { + root._hide() + tabModel.newTab(root.linkHref, root.linkTitle) + } + } + + ContextMenuItem { + visible: root.isLink + //: Share link from browser context menu + //% "Share" + text: qsTrId("sailfish_components_webview_popups-me-share_link") + onClicked: { + root._hide() + webShareAction.shareLink(root.linkHref, root.linkTitle) + } + WebShareAction { + id: webShareAction + } + } + + ContextMenuItem { + visible: root.url + //: Copy link to clipboard from context menu + //% "Copy link" + text: qsTrId("sailfish_components_webview_popups-me-copy_link_to_clipboard") + onClicked: { + root._hide() + Clipboard.text = root.url + } + } + + ContextMenuItem { + visible: !root.isImage && root.linkTitle && (root.linkTitle != root.url) + //: Copy text to clipboard from context menu + //% "Copy text" + text: qsTrId("sailfish_components_webview_popups-me-copy_text_to_clipboard") + onClicked: { + root._hide() + Clipboard.text = root.linkTitle + } + } + + DownloadMenuItem { + visible: root.downloadsEnabled && root.isNavigable + //% "Save link" + text: qsTrId("sailfish_components_webview_popups-me-save_link") + targetDirectory: StandardPaths.download + linkUrl: root.linkHref + contentType: root.contentType + viewId: root.viewId + onClicked: root._hide() + } + + ContextMenuItem { + visible: root.isImage && !!tabModel + //% "Open image in a new tab" + text: qsTrId("sailfish_components_webview_popups-me-open_image_in_new_tab") + onClicked: { + root._hide() + tabModel.newTab(root.imageSrc, "") + } + } + + DownloadMenuItem { + visible: root.isImage + //: This menu item saves image to Gallery application + //% "Save image" + text: qsTrId("sailfish_components_webview_popups-me-save_image") + targetDirectory: StandardPaths.download + linkUrl: root.imageSrc + contentType: root.contentType + viewId: root.viewId + onClicked: root._hide() + } + + ContextMenuItem { + visible: root.isImage + //: Share image from context menu + //% "Share image" + text: qsTrId("sailfish_components_webview_popups-me-share_image") + onClicked: { + root._hide() + webShareAction.shareLink(root.imageSrc, root.url, qsTrId("sailfish_components_webview_popups-me-share_image")) + } + } + + ContextMenuItem { + visible: root.isImage + //: Copy image link clipboard from context menu + //% "Copy image link" + text: qsTrId("sailfish_components_webview_popups-me-copy_image_link_to_clipboard") + onClicked: { + root._hide() + Clipboard.text = root.imageSrc + } + } } } } diff --git a/usr/lib/qt5/qml/Sailfish/WebView/Popups/ContextMenuItem.qml b/usr/lib/qt5/qml/Sailfish/WebView/Popups/ContextMenuItem.qml new file mode 100644 index 00000000..0e15197e --- /dev/null +++ b/usr/lib/qt5/qml/Sailfish/WebView/Popups/ContextMenuItem.qml @@ -0,0 +1,24 @@ +/**************************************************************************** +** +** Copyright (c) 2023 Jolla Ltd. +** +****************************************************************************/ + +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Sailfish.WebView.Popups 1.0 + +ListItem { + property alias text: menuItem.text + + width: parent.width + height: screen.sizeCategory <= Screen.Medium ? Theme.itemSizeExtraSmall : Theme.itemSizeSmall + + MenuItem { + id: menuItem + } +} diff --git a/usr/lib/qt5/qml/Sailfish/WebView/Popups/DownloadMenuItem.qml b/usr/lib/qt5/qml/Sailfish/WebView/Popups/DownloadMenuItem.qml index e289b9a5..23d70b2f 100644 --- a/usr/lib/qt5/qml/Sailfish/WebView/Popups/DownloadMenuItem.qml +++ b/usr/lib/qt5/qml/Sailfish/WebView/Popups/DownloadMenuItem.qml @@ -15,7 +15,7 @@ import Sailfish.Silica 1.0 import Sailfish.WebEngine 1.0 import Sailfish.Pickers 1.0 -MenuItem { +ContextMenuItem { id: root property string targetDirectory diff --git a/usr/lib/qt5/qml/Sailfish/WebView/Popups/WebShareAction.qml b/usr/lib/qt5/qml/Sailfish/WebView/Popups/WebShareAction.qml index 857cd887..86ca4f89 100644 --- a/usr/lib/qt5/qml/Sailfish/WebView/Popups/WebShareAction.qml +++ b/usr/lib/qt5/qml/Sailfish/WebView/Popups/WebShareAction.qml @@ -14,12 +14,12 @@ import Sailfish.Share 1.0 ShareAction { id: shareAction - function shareLink(linkHref, linkTitle) + function shareLink(linkHref, linkTitle, title) { _share( //: Header for link sharing //% "Share link" - qsTrId("sailfish_browser-he-share_link"), + title ? title : qsTrId("sailfish_browser-he-share_link"), { "type": "text/x-url", "linkTitle": linkTitle, diff --git a/usr/lib/qt5/qml/Sailfish/WebView/Popups/qmldir b/usr/lib/qt5/qml/Sailfish/WebView/Popups/qmldir index df2cf37d..08dc0740 100644 --- a/usr/lib/qt5/qml/Sailfish/WebView/Popups/qmldir +++ b/usr/lib/qt5/qml/Sailfish/WebView/Popups/qmldir @@ -1,5 +1,6 @@ module Sailfish.WebView.Popups plugin sailfishwebviewpopupsplugin +typeinfo plugins.qmltypes singleton LocationSettings 1.0 LocationSettings.qml PopupProvider 1.0 PopupProvider.qml UserPromptInterface 1.0 UserPromptInterface.qml diff --git a/usr/lib/qt5/qml/Sailfish/WebView/WebView.qml b/usr/lib/qt5/qml/Sailfish/WebView/WebView.qml index bf6b9975..e2236f71 100644 --- a/usr/lib/qt5/qml/Sailfish/WebView/WebView.qml +++ b/usr/lib/qt5/qml/Sailfish/WebView/WebView.qml @@ -65,9 +65,7 @@ RawWebView { || _appActive && (webViewPage.status === PageStatus.Deactivating) _acceptTouchEvents: !textSelectionActive - viewportHeight: webViewPage - ? ((webViewPage.orientation & Orientation.PortraitMask) ? height : width) - : undefined + viewportHeight: webViewPage ? height : undefined orientation: { switch (_pageOrientation) { diff --git a/usr/lib/qt5/qml/Sailfish/WebView/qmldir b/usr/lib/qt5/qml/Sailfish/WebView/qmldir index 265a3bae..cc8dcd25 100644 --- a/usr/lib/qt5/qml/Sailfish/WebView/qmldir +++ b/usr/lib/qt5/qml/Sailfish/WebView/qmldir @@ -1,5 +1,6 @@ module Sailfish.WebView plugin sailfishwebviewplugin +typeinfo plugins.qmltypes WebView 1.0 WebView.qml WebViewFlickable 1.0 WebViewFlickable.qml WebViewPage 1.0 WebViewPage.qml diff --git a/usr/lib/qt5/qml/Sailfish/WindowManager/qmldir b/usr/lib/qt5/qml/Sailfish/WindowManager/qmldir new file mode 100644 index 00000000..82d1d715 --- /dev/null +++ b/usr/lib/qt5/qml/Sailfish/WindowManager/qmldir @@ -0,0 +1,3 @@ +module Sailfish.WindowManager +plugin SailfishWindowManagerPlugin +classname SailfishWindowManagerPlugin diff --git a/usr/lib/qt5/qml/com/jolla/camera/capture/CameraModeHint.qml b/usr/lib/qt5/qml/com/jolla/camera/capture/CameraModeHint.qml index 623302ac..9da71bc4 100644 --- a/usr/lib/qt5/qml/com/jolla/camera/capture/CameraModeHint.qml +++ b/usr/lib/qt5/qml/com/jolla/camera/capture/CameraModeHint.qml @@ -19,9 +19,8 @@ Loader { anchors.fill: parent InteractionHintLabel { - //: Push up or down to change between photo and video mode - //% "Push up or down to change between photo and video mode" - text: qsTrId("camera-la-camera_mode_hint") + //% "Swipe down to access camera settings" + text: qsTrId("camera-la-camera_settings_hint") anchors.bottom: parent.bottom opacity: touchInteractionHint.running ? 1.0 : 0.0 Behavior on opacity { FadeAnimation { duration: 800 } } diff --git a/usr/lib/qt5/qml/com/jolla/camera/capture/CaptureOverlay.qml b/usr/lib/qt5/qml/com/jolla/camera/capture/CaptureOverlay.qml index ee36968a..613ac267 100644 --- a/usr/lib/qt5/qml/com/jolla/camera/capture/CaptureOverlay.qml +++ b/usr/lib/qt5/qml/com/jolla/camera/capture/CaptureOverlay.qml @@ -4,9 +4,9 @@ import QtMultimedia 5.4 import QtPositioning 5.1 import Sailfish.Silica 1.0 import com.jolla.camera 1.0 -import org.nemomobile.time 1.0 -import org.nemomobile.configuration 1.0 -import org.nemomobile.notifications 1.0 +import Nemo.Time 1.0 +import Nemo.Configuration 1.0 +import Nemo.Notifications 1.0 import org.nemomobile.systemsettings 1.0 import QtSensors 5.0 @@ -32,9 +32,27 @@ SettingsOverlay { width: captureView.width height: captureView.height - property int _pictureRotation: Screen.primaryOrientation == Qt.PortraitOrientation ? 0 : 90 + property int _pictureRotation: { + if (orientationSensor.connectedToBackend) { + return Screen.primaryOrientation == Qt.PortraitOrientation ? 0 : 90 + } else { + switch (Screen.orientation) { + case Qt.PortraitOrientation: + return 0 + case Qt.LandscapeOrientation: + return 90 + case Qt.InvertedPortraitOrientation: + return 180 + case Qt.InvertedLandscapeOrientation: + return 270 + default: + return 0 + } + } + } OrientationSensor { + id: orientationSensor active: captureView.effectiveActive onReadingChanged: { diff --git a/usr/lib/qt5/qml/com/jolla/camera/capture/CaptureView.qml b/usr/lib/qt5/qml/com/jolla/camera/capture/CaptureView.qml index 1d32ab54..3b7d7976 100644 --- a/usr/lib/qt5/qml/com/jolla/camera/capture/CaptureView.qml +++ b/usr/lib/qt5/qml/com/jolla/camera/capture/CaptureView.qml @@ -9,10 +9,10 @@ import QtMultimedia 5.4 import Sailfish.Silica 1.0 import Sailfish.Policy 1.0 import com.jolla.camera 1.0 -import org.nemomobile.policy 1.0 -import org.nemomobile.ngf 1.0 -import org.nemomobile.dbus 2.0 -import org.nemomobile.notifications 1.0 +import Nemo.Policy 1.0 +import Nemo.Ngf 1.0 +import Nemo.DBus 2.0 +import Nemo.Notifications 1.0 import org.nemomobile.systemsettings 1.0 import "../settings" diff --git a/usr/lib/qt5/qml/com/jolla/camera/settings.qml b/usr/lib/qt5/qml/com/jolla/camera/settings.qml index 43c8006a..c16323fe 100644 --- a/usr/lib/qt5/qml/com/jolla/camera/settings.qml +++ b/usr/lib/qt5/qml/com/jolla/camera/settings.qml @@ -1,6 +1,6 @@ import QtQuick 2.0 import QtMultimedia 5.6 -import org.nemomobile.configuration 1.0 +import Nemo.Configuration 1.0 import com.jolla.camera 1.0 SettingsBase { diff --git a/usr/lib/qt5/qml/com/jolla/camera/settings/CameraDeviceToggle.qml b/usr/lib/qt5/qml/com/jolla/camera/settings/CameraDeviceToggle.qml index fb85d884..bfc5793e 100644 --- a/usr/lib/qt5/qml/com/jolla/camera/settings/CameraDeviceToggle.qml +++ b/usr/lib/qt5/qml/com/jolla/camera/settings/CameraDeviceToggle.qml @@ -17,7 +17,7 @@ Grid { rows: orientation === Qt.Vertical ? repeater.count : 1 spacing: Theme.paddingMedium - readonly property bool _supportNotEnabled: model.length > 1 && labels.length === 0 + readonly property bool _supportNotEnabled: !!model && model.length > 1 && labels.length === 0 on_SupportNotEnabledChanged: if (_supportNotEnabled) console.warn("Device supports multiple back cameras, please define dconf /apps/jolla-camera/backCameraLabels") Repeater { @@ -26,6 +26,7 @@ Grid { highlighted: mouseArea.pressed && mouseArea.containsMouse || modelData.deviceId === Settings.deviceId width: Theme.itemSizeExtraSmall height: Theme.itemSizeExtraSmall + visible: cameraLabel.text != '' MouseArea { id: mouseArea @@ -45,6 +46,7 @@ Grid { } Label { + id: cameraLabel // TODO: Don't hardcode these values text: root.labels.length > model.index ? root.labels[model.index] : "" color: parent.highlighted ? root.highlightColor : Theme.lightPrimaryColor diff --git a/usr/lib/qt5/qml/com/jolla/camera/settings/SettingsOverlay.qml b/usr/lib/qt5/qml/com/jolla/camera/settings/SettingsOverlay.qml index 3175bedb..2035afa2 100644 --- a/usr/lib/qt5/qml/com/jolla/camera/settings/SettingsOverlay.qml +++ b/usr/lib/qt5/qml/com/jolla/camera/settings/SettingsOverlay.qml @@ -2,6 +2,7 @@ import QtQuick 2.4 import QtMultimedia 5.6 import Sailfish.Silica 1.0 import com.jolla.camera 1.0 +import Nemo.Configuration 1.0 PinchArea { id: overlay @@ -133,7 +134,10 @@ PinchArea { } CameraDeviceToggle { - onSelected: Settings.deviceId = deviceId + onSelected: { + Settings.deviceId = deviceId + camera.digitalZoom = 1.0 + } parent: { switch(_overlayPosition.backCameraToggle) { @@ -397,7 +401,9 @@ PinchArea { enabled: overlay._exposed visible: overlay._exposed - columns: Math.min(count, Math.floor((parent.width + spacing - 2 * Theme.horizontalPageMargin)/(overlay._menuWidth + spacing))) + columns: Math.min(count, + Math.floor((parent.width + spacing - 2 * Theme.horizontalPageMargin) + / (overlay._menuWidth + spacing))) spacing: overlay._menuItemHorizontalSpacing Item { @@ -458,13 +464,15 @@ PinchArea { SettingsMenu { id: exposureModeMenu - active: model.length > 1 || CameraConfigs.supportedIsoSensitivities.length == 0 + active: model.length > 1 + || (!experimentalModes.value && CameraConfigs.supportedIsoSensitivities.length == 0) width: overlay._menuWidth title: Settings.exposureModeText header: upperHeader - // Disabled in 4.4.0 - model: CameraConfigs.supportedIsoSensitivities.length == 0 - ? [Camera.ExposureManual] : [] + model: experimentalModes.value + ? CameraConfigs.supportedExposureModes + : CameraConfigs.supportedIsoSensitivities.length == 0 + ? [Camera.ExposureManual] : [] delegate: SettingsMenuItem { settings: Settings.mode property: "exposureMode" @@ -527,7 +535,9 @@ PinchArea { Row { id: topRow - property real _topRowMargin: overlay.topButtonRowHeight/2 - overlay._menuWidth/2 + property real _topRowMargin: Math.max(overlay.topButtonRowHeight/2 - overlay._menuWidth/2, + (Screen.hasCutouts && overlay.isPortrait) + ? (Screen.topCutout.height + Theme.paddingSmall) : 0) anchors.horizontalCenter: parent.horizontalCenter spacing: grid.spacing @@ -561,14 +571,15 @@ PinchArea { Item { width: overlay._menuWidth height: width - // Disabled in 4.4.0 - visible: CameraConfigs.supportedIsoSensitivities.length == 0 + visible: experimentalModes.value ? CameraConfigs.supportedExposureModes.length > 1 + : CameraConfigs.supportedIsoSensitivities.length == 0 y: topRow.dragY(exposureModeMenu.currentItem ? exposureModeMenu.currentItem.y : 0) Icon { anchors.centerIn: parent color: Theme.lightPrimaryColor - source: Settings.exposureModeIcon(Camera.ExposureManual /*Settings.mode.exposureMode*/) + source: Settings.exposureModeIcon(experimentalModes.value ? Settings.mode.exposureMode + : Camera.ExposureManual) } } @@ -770,4 +781,11 @@ PinchArea { qsTrId("camera-la-landscape-capture-key-location") } } + + ConfigurationValue { + id: experimentalModes + + key: "/apps/jolla-camera/enable_experimental_modes" + defaultValue: false + } } diff --git a/usr/lib/qt5/qml/com/jolla/email/AccountCreation.qml b/usr/lib/qt5/qml/com/jolla/email/AccountCreation.qml new file mode 100644 index 00000000..b67f4d11 --- /dev/null +++ b/usr/lib/qt5/qml/com/jolla/email/AccountCreation.qml @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2019 Jolla Ltd. + * + * License: Proprietary + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import com.jolla.settings.accounts 1.0 + +AccountCreationManager { + id: genericAccountCreator + + property bool creationDone + + signal creationCompleted + + Component.onCompleted: { + if (genericAccountCreator.hasOwnProperty("serviceFilter")) { + serviceFilter = ["e-mail"] + } + startAccountCreation() + } + + Connections { + target: endDestination + onStatusChanged: { + if (endDestination.status == PageStatus.Active && !creationDone) { + // don't emit immediately as new pages cannot be pushed at this point + delayCompletedSignal.start() + } + } + } + + Timer { + id: delayCompletedSignal + interval: 1 + onTriggered: { + creationDone = true + creationCompleted() + } + } +} diff --git a/usr/lib/qt5/qml/com/jolla/email/AttachmentDownloadPage.qml b/usr/lib/qt5/qml/com/jolla/email/AttachmentDownloadPage.qml new file mode 100644 index 00000000..dc84179d --- /dev/null +++ b/usr/lib/qt5/qml/com/jolla/email/AttachmentDownloadPage.qml @@ -0,0 +1,122 @@ +/* + * Copyright (c) 2012 – 2019 Jolla Ltd. + * + * License: Proprietary + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Nemo.Email 0.1 + +Page { + id: root + + property EmailMessage email + property Item composerItem + property int undownloadedAttachmentsCount + property bool downloadInProgress + + onStatusChanged: { + if (status === PageStatus.Deactivating && downloadInProgress) { + email.cancelMessageDownload() + busyLabel.running = false + } else if (status === PageStatus.Deactivating && !composerItem.discardUndownloadedAttachments) { + composerItem.removeUndownloadedAttachments() + composerItem.discardUndownloadedAttachments = true + } + } + + Column { + x: Theme.horizontalPageMargin + anchors.top: parent.top + anchors.topMargin: Theme.itemSizeLarge // Page header size + width: parent.width - x*2 + spacing: Theme.paddingLarge + opacity: busyLabel.running ? 0.0 : 1.0 + Behavior on opacity { FadeAnimator {} } + + Label { + width: parent.width + wrapMode: Text.Wrap + font.pixelSize: Theme.fontSizeExtraLarge + color: Theme.highlightColor + //: When singular "Download attachment?" when plural "Download attachments?" + //% "Download attachment?" + text: qsTrId("jolla-email-la-download-attachments-header", undownloadedAttachmentsCount) + } + + Label { + id: informationLabel + width: parent.width + wrapMode: Text.Wrap + font.pixelSize: Theme.fontSizeSmall + color: Theme.rgba(Theme.highlightColor, 0.9) + //: When singular "The attachment you are forwarding has not been downloaded yet", + //: plural "Some of the attachments you are forwarding have not been downloaded yet" + //% "The attachment you are forwarding has not been downloaded yet." + text: qsTrId("jolla-email-la-forward-attachments-info", undownloadedAttachmentsCount) + } + } + + ButtonLayout { + anchors { + bottom: parent.bottom + bottomMargin: Theme.itemSizeMedium + } + preferredWidth: Theme.buttonWidthMedium + + Button { + id: downloadAttachButton + visible: !busyLabel.running + //: Download attachments button + //% "Download" + text: qsTrId("jolla-email-la-download_attachments_forward") + icon.source: "image://theme/icon-splus-cloud-download" + onClicked: { + busyLabel.running = true + downloadInProgress = true + email.downloadMessage() + } + } + SecondaryButton { + visible: !busyLabel.running + //: Discard not downloaded attachments button + //% "Discard" + text: qsTrId("jolla-email-la-discard_not_downloaded_attachments") + icon.source: "image://theme/icon-splus-cancel" + onClicked: { + composerItem.removeUndownloadedAttachments() + composerItem.discardUndownloadedAttachments = true + pageStack.pop() + } + } + } + + BusyLabel { + id: busyLabel + //: When singular "Downloading attachment", when plural "Downloading attachments" + //% "Downloading attachment..." + text: qsTrId("jolla-email-la-downloading-attachments", undownloadedAttachmentsCount) + } + + Connections { + target: email + onMessageDownloaded: { + downloadInProgress = false + composerItem.setOriginalMessageAttachments() + pageStack.pop() + } + + onMessageDownloadFailed: { + downloadInProgress = false + //: When singular "The attachment could not be downloaded, please check your internet connection.", + //: when plural "Some attachments could not be downloaded, please check your internet connection." + //% "The attachment could not be downloaded, please check your internet connection." + informationLabel.text = qsTrId("jolla-email-la-attachments-download-failed-info", undownloadedAttachmentsCount) + //: Try again button + //% "Try again" + downloadAttachButton.text = qsTrId("jolla-email-la-download-attachments_try_again") + busyLabel.running = false + } + } +} diff --git a/usr/lib/qt5/qml/com/jolla/email/AttachmentsPage.qml b/usr/lib/qt5/qml/com/jolla/email/AttachmentsPage.qml new file mode 100644 index 00000000..4a631b5d --- /dev/null +++ b/usr/lib/qt5/qml/com/jolla/email/AttachmentsPage.qml @@ -0,0 +1,138 @@ +/* + * Copyright (c) 2013 – 2019 Jolla Ltd. + * + * License: Proprietary + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Sailfish.Pickers 1.0 +import Nemo.Thumbnailer 1.0 + +Page { + property alias attachmentFiles: listView.model + property Component contentPicker + + signal addAttachments + + allowedOrientations: Orientation.All + + SilicaListView { + id: listView + + anchors.fill: parent + + PullDownMenu { + MenuItem { + //% "Add new attachment" + text: qsTrId("jolla-email-me-add_new_attachment") + onClicked: addAttachments() + } + MenuItem { + visible: attachmentFiles.count > 0 + // Defined in email composer page + text: qsTrId("jolla-email-me-remove_all_attachments", attachmentFiles.count) + onDelayedClick: attachmentFiles.clear() + } + } + + header: PageHeader { + title: attachmentFiles.count == 0 + //% "No Attachments" + ? qsTrId("jolla-email-he-no_attachments") + //: Singular: 1 attachment (or one as text), plural: X Attachments (X the number) + //% "%n Attachments" + : qsTrId("jolla-email-he-attachments_page", attachmentFiles.count) + } + + delegate: ListItem { + contentHeight: Theme.itemSizeMedium + menu: menuComponent + + ListView.onRemove: animateRemoval() + + Rectangle { + id: iconContainer + width: height + height: parent.height + + gradient: Gradient { + GradientStop { position: 0.0; color: Theme.rgba(Theme.primaryColor, 0.1) } + GradientStop { position: 1.0; color: "transparent" } + } + + Thumbnail { + id: thumbnail + visible: url != "" && status != Thumbnail.Null && status != Thumbnail.Error + height: defaultIcon.height + width: height + anchors.centerIn: parent + sourceSize.width: width + sourceSize.height: height + source: url + mimeType: mimeType + } + + Image { + id: defaultIcon + visible: !thumbnail.visible + anchors.centerIn: parent + source: Theme.iconForMimeType(mimeType) + (highlighted ? "?" + Theme.highlightColor : "") + } + } + + Label { + id: attachmentTitleLabel + anchors { + left: iconContainer.right + leftMargin: Theme.paddingLarge + right: parent.right + rightMargin: Theme.horizontalPageMargin + verticalCenter: iconContainer.verticalCenter + verticalCenterOffset: -attachmentSizeLabel.height/2 + } + text: title + truncationMode: TruncationMode.Fade + color: highlighted ? Theme.highlightColor : Theme.primaryColor + } + + Label { + id: attachmentSizeLabel + anchors { + left: iconContainer.right + leftMargin: Theme.paddingLarge + right: parent.right + rightMargin: Theme.horizontalPageMargin + top: attachmentTitleLabel.bottom + } + font.pixelSize: Theme.fontSizeExtraSmall + text: Format.formatFileSize(fileSize) + truncationMode: TruncationMode.Fade + color: highlighted ? Theme.secondaryHighlightColor : Theme.secondaryColor + } + + Component { + id: menuComponent + + ContextMenu { + MenuItem { + // Defined in email composer page + text: qsTrId("jolla-email-me-remove_all_attachments", 1) + onClicked: { + attachmentFiles.remove(model.index) + } + } + } + } + } + + ViewPlaceholder { + enabled: attachmentFiles.count == 0 + + //% "Pull down to add attachments" + text: qsTrId("email-la_no_attachments_viewplace_text") + } + + VerticalScrollDecorator {} + } +} diff --git a/usr/lib/qt5/qml/com/jolla/email/BatchedMessageDeletion.qml b/usr/lib/qt5/qml/com/jolla/email/BatchedMessageDeletion.qml new file mode 100644 index 00000000..b3f34f8d --- /dev/null +++ b/usr/lib/qt5/qml/com/jolla/email/BatchedMessageDeletion.qml @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2019 Open Mobile Platform LLC. + * + * License: Proprietary + */ + +pragma Singleton +import QtQml 2.2 + +QtObject { + property var _pendingDeletions: ({}) + + function addMessage(messageId) { + if (messageId) { + _pendingDeletions[messageId] = true + } + } + + function removeMessage(messageId) { + if (messageId) { + delete _pendingDeletions[messageId] + } + } + + function messageReadyForDeletion(messageId) { + if (messageId) { + _pendingDeletions[messageId] = false + } + } + + function run(emailAgent) { + var messageIds = [] + for (var messageId in _pendingDeletions) { + messageIds.push(messageId) + if (_pendingDeletions[messageId] === true) { + // Don't go ahead with the batched deletion until all messages are marked as ready + // for deletion. + return + } + } + + _pendingDeletions = {} + if (messageIds.length > 0) { + emailAgent.deleteMessagesFromVariantList(messageIds) + } + } +} diff --git a/usr/lib/qt5/qml/com/jolla/email/CompressibleItem.qml b/usr/lib/qt5/qml/com/jolla/email/CompressibleItem.qml new file mode 100644 index 00000000..905d6d60 --- /dev/null +++ b/usr/lib/qt5/qml/com/jolla/email/CompressibleItem.qml @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2013 – 2019 Jolla Ltd. + * + * License: Proprietary + */ + +import QtQuick 2.0 + +FocusScope { + property bool compressible: true + property real expandedHeight: children[0].implicitHeight + property real compressionHeight + readonly property bool compressed: height < 1 + + height: expandedHeight - compressionHeight + width: parent.width + opacity: compressed ? 0 : Math.pow((height / expandedHeight), 3) +} diff --git a/usr/lib/qt5/qml/com/jolla/email/Compressor.qml b/usr/lib/qt5/qml/com/jolla/email/Compressor.qml new file mode 100644 index 00000000..d88c364b --- /dev/null +++ b/usr/lib/qt5/qml/com/jolla/email/Compressor.qml @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2013 – 2019 Jolla Ltd. + * + * License: Proprietary + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 + +Item { + property Item expanderItem + property Item expansibleItem: children[0] + + width: parent.width + height: _heightRange.min + _expansionHeight + + property real minimumHeight: _heightRange.min + property real maximumHeight: _heightRange.max + + property var _heightRange: _getHeightRange() + property real _compressionRange: (_heightRange.max - _heightRange.min) + property real _expansionHeight: Math.round(_compressionRange * expanderItem.expansion) + property real compressionHeight: _compressionRange - _expansionHeight + + // Don't allow a child's compressibility to become false while it is uncompressed + property var _compressionPrevented + + function _testCompressible() { + var childNonCompressible = [] + for (var i = 0; i < expansibleItem.children.length; ++i) { + var child = expansibleItem.children[i] + childNonCompressible.push(child.visible && !child.compressible) + } + _compressionPrevented = childNonCompressible + } + + Connections { + target: expanderItem + onChangingChanged: { + // Compression is starting - reset the compression prevention flags + if (expanderItem.changing && (expanderItem.expansion == 1.0)) { + _testCompressible() + } + } + } + + onCompressionHeightChanged: { + var compression = compressionHeight + for (var i = 0; i < expansibleItem.children.length; ++i) { + var child = expansibleItem.children[i] + if (child.visible || !child.compressible) { + if (child.compressible) { + // Check that this child wasn't previously prevented from compressing + if (_compressionPrevented === undefined || _compressionPrevented[i] === false) { + child.compressionHeight = Math.min(compression, child.expandedHeight) + compression = compression - child.compressionHeight + } + } else { + child.compressionHeight = 0 + } + } + } + } + + function _getHeightRange() { + var min = 0 + var max = 0 + + for (var i = 0; i < expansibleItem.children.length; ++i) { + var child = expansibleItem.children[i] + if (child.visible) { + if (child.expandedHeight) { + max += child.expandedHeight + } else { + max += child.height + } + + if ((child.compressible === undefined) || (child.compressible === false) || + (_compressionPrevented !== undefined && _compressionPrevented[i] === true)) { + if (child.expandedHeight) { + min += child.expandedHeight + } else { + min += child.height + } + } + } + } + + return { 'min': Math.floor(min), 'max': Math.floor(max) } + } +} diff --git a/usr/lib/qt5/qml/com/jolla/email/EmailComposer.qml b/usr/lib/qt5/qml/com/jolla/email/EmailComposer.qml new file mode 100644 index 00000000..4aa0f313 --- /dev/null +++ b/usr/lib/qt5/qml/com/jolla/email/EmailComposer.qml @@ -0,0 +1,1067 @@ +/* + * Copyright (c) 2013 – 2019 Jolla Ltd. + * Copyright (c) 2019 – 2020 Open Mobile Platform LLC. + * + * License: Proprietary + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Sailfish.Pickers 1.0 +import Nemo.Email 0.1 +import org.nemomobile.contacts 1.0 +import com.jolla.email.settings.translations 1.0 +import Nemo.Configuration 1.0 + +Item { + id: messageComposer + + anchors.fill: parent + + property alias attachmentsModel: attachmentFiles + property alias emailSubject: message.subject + property alias emailTo: message.to + property alias emailCc: message.cc + property alias emailBcc: message.bcc + property alias emailBody: message.body + property alias messageId: message.messageId + property alias maximumAttachmentsSize: attachmentSizeMaxConfig.value + + property alias _toSummary: to.summary + property alias _bodyText: body.text + + property string action + property alias originalMessageId: originalMessage.messageId + property int accountId + + // Destroy online search model upon account change + onAccountIdChanged: { + if (onlineSearchModel) + onlineSearchModel.destroy() + + createOnlineSearchModel() + } + + property string signature + property bool validSignatureSet + property bool hasRecipients: !to.empty || !cc.empty || !bcc.empty + property Page popDestination + property int undownloadedAttachmentsCount + property bool _isPortrait: !pageStack.currentPage || pageStack.currentPage.isPortrait + property bool draft // opened from draft + property bool autoSaveDraft + property bool popOnDraftSaved + property bool discardUndownloadedAttachments + property alias toFieldHasFocus: to.activeFocus + property int totalAttachmentSize + readonly property bool maxAttachmentSizeExceeded: totalAttachmentSize > attachmentSizeMaxConfig.value + + // avoid flashing menu when popping page + property bool _effectiveAutoSaveDraft: autoSaveDraft + //: Discard draft message + //% "Discard draft" + readonly property string _strDiscardDraft: qsTrId("jolla-components_email-me-discard_draft") + //: Save draft message + //% "Save draft" + readonly property string _strSaveDraft: qsTrId("jolla-components_email-me-save_draft") + //: Send message + //% "Send" + readonly property string _strSend: qsTrId("jolla-components_email-me-send") + + signal requestDraftRemoval(int messageId) + + function _ensureRecipientsComplete() { + to.updateSummary() + cc.updateSummary() + bcc.updateSummary() + } + + function createOnlineSearchModel() { + if (accountListModel.customFieldFromAccountId("globalAddressList", accountId) == "true") { + var onlineSearchModelComponent = Qt.createComponent("OnlineSearchModel.qml") + if (onlineSearchModelComponent.status == Component.Ready) { + onlineSearchModel = onlineSearchModelComponent.createObject(messageComposer, { "accountId": accountId }) + } + } + } + + onSignatureChanged: { + // Sometimes ConfigurationValue return undefined in the first read + if (!validSignatureSet && signature) { + loadQuotedBody() + validSignatureSet = true + } + } + + // FIXME: this is not safe or good. should get info from pagestack when item gets popped. + Component.onDestruction: { + if (_effectiveAutoSaveDraft && messageComposer.undownloadedAttachmentsCount === 0 + && messageContentModified()) { + saveDraft() + } + } + + EmailMessage { + id: message + onSendEnqueued: { + messageComposer.enabled = true + if (success) { + _effectiveAutoSaveDraft = false + if (popDestination) { + // pop any page/dialog on top of composer if it exists + pageStack.pop(popDestination) + } else { + pageStack.pop() + } + } + } + } + + EmailMessage { + id: originalMessage + + onQuotedBodyChanged: { + loadQuotedBody() + } + } + + ListModel { + id: attachmentFiles + } + + AttachmentListModel { + id: attachmentListModel + } + + PeopleModel { + id: contactSearchModel + } + + property QtObject onlineSearchModel + + Component { + id: accountCreatorComponent + AccountCreation { + endDestination: pageStack.find(function(page) { + return true + }) + } + } + + function _messagePriority(index) { + return (index === 1 ? EmailMessage.HighPriority : (index === 2 ? EmailMessage.LowPriority + : EmailMessage.NormalPriority)) + } + + function setOriginalMessageAttachments() { + undownloadedAttachmentsCount = 0 + if (draft) { + attachmentListModel.messageId = message.messageId + } else { + attachmentListModel.messageId = originalMessage.messageId + } + + //Save any existent attachment that is not from the original message + var i + if (attachmentFiles.count) { + for (i = attachmentFiles.count -1; i >= 0; --i) { + if (attachmentFiles.get(i).FromOriginalMessage === "true") { + attachmentFiles.remove(i) + } + } + } + + for (i = 0; i < attachmentListModel.count; ++i) { + // first check whether attachment is downloaded or not. + if (attachmentListModel.isDownloaded(i)) { + // if attachment downloaded we should try to save it on a disk + if (!emailAgent.downloadAttachment(originalMessage.messageId, attachmentListModel.location(i))) { + console.warn("Failed to save attachment " + attachmentListModel.location(i) + " on a disk:") + } + } + attachmentFiles.append({"url": attachmentListModel.url(i), + "title": attachmentListModel.displayName(i), + "mimeType": attachmentListModel.mimeType(i), + "fileSize": attachmentListModel.size(i), + "FromOriginalMessage": "true"}) + + if (attachmentListModel.url(i) === "") { + ++undownloadedAttachmentsCount + } + } + } + + function _originalMessageAttachmentsDownloaded() { + for (var i = 0; i < attachmentFiles.count; ++i) { + if (attachmentFiles.get(i).url === "") { + return false + } + } + return true + } + + function removeUndownloadedAttachments() { + //Remove any existent attachment that is not downloaded + if (attachmentFiles.count) { + for (var i = attachmentFiles.count -1; i >= 0; --i) { + if (attachmentFiles.get(i).url === "") { + attachmentFiles.remove(i) + } + } + } + undownloadedAttachmentsCount = 0 + } + + function saveNewContacts() { + to.saveNewContacts() + cc.saveNewContacts() + bcc.saveNewContacts() + } + + function buildMessage() { + message.to = to.recipientsToString() + message.cc = cc.recipientsToString() + message.bcc = bcc.recipientsToString() + message.from = from.value + message.signingPlugin = cryptoSignatureSwitch.checked + ? accountListModel.cryptoSignatureType(accountId) : "" + message.signingKeys = cryptoSignatureSwitch.checked + ? accountListModel.cryptoSignatureIds(accountId) : [] + message.priority = _messagePriority(importance.currentIndex) + message.subject = subject.text + message.body = body.text + body.quote + message.requestReadReceipt = requestReadReceiptSwitch.checked + + if (attachmentFiles.count > 0) { + var att = [] + for (var i = 0; i < attachmentFiles.count; ++i) { + att.push(attachmentFiles.get(i).url) + } + message.attachments = att + } + } + + function sendMessage() { + // In case something goes wrong don't save invalid references + if (!_originalMessageAttachmentsDownloaded()) { + removeUndownloadedAttachments() + } + saveNewContacts() + buildMessage() + messageComposer.enabled = false + message.send() + } + + function saveDraft() { + // In case something goes wrong don't save invalid references + if (!_originalMessageAttachmentsDownloaded()) { + removeUndownloadedAttachments() + } + saveNewContacts() + buildMessage() + message.saveDraft() + if (popOnDraftSaved) { + if (popDestination) { + pageStack.pop(popDestination) + } else { + pageStack.pop() + } + } + } + + function _discardDraft() { + _effectiveAutoSaveDraft = false + + // pop any page/dialog on top of composer if it exists + if (popDestination) { + pageStack.pop(popDestination) + } else { + pageStack.pop() + } + if (draft) { + // handling or ignoring depends on caller + requestDraftRemoval(message.messageId) + } + } + + function isSelectedAttachment(acceptedItem) { + for (var i = 0; i < attachmentFiles.count; ++i) { + var attachedItem = attachmentFiles.get(i) + if (acceptedItem.filePath === attachedItem.filePath) { + return true + } + } + return false + } + + function modifyAttachments() { + var obj = pageStack.animatorPush(contentPicker) + obj.pageCompleted.connect(function(picker) { + picker.selectedContentChanged.connect(function() { + for (var i = 0; i < picker.selectedContent.count; ++i) { + var acceptedItem = picker.selectedContent.get(i) + if (!isSelectedAttachment(acceptedItem)) { + attachmentFiles.insert(0, acceptedItem) + } + } + }) + }) + } + + function showAttachments() { + var properties = { attachmentFiles: attachmentFiles } + var obj = pageStack.animatorPush(Qt.resolvedUrl('AttachmentsPage.qml'), properties) + obj.pageCompleted.connect(function(page) { + page.addAttachments.connect(modifyAttachments) + }) + } + + function messageContentModified() { + if (hasRecipients || subject.text != '' || body.text != '' + && body.text != signature || attachmentFiles.count) { + return true + } else { + return false + } + } + + function forwardContentAvailable() { + if (!_originalMessageAttachmentsDownloaded()) { + pageStack.animatorPush(Qt.resolvedUrl('AttachmentDownloadPage.qml'), + { email: originalMessage, + composerItem: messageComposer, + undownloadedAttachmentsCount: undownloadedAttachmentsCount }) + } + } + + function forwardPrecursor() { + var precursor = '\n\n' + //: Indicator of original message content + //% "--- Original message ---" + precursor += qsTrId("jolla-components_email-la-original_message") + return precursor + } + + function replyPrecursor() { + var precursor = '\n\n' + var timestamp = Format.formatDate(originalMessage.date, Formatter.DateFull) + + //: Indicator of reply message origin (%1:timestamp %2:mailSender) + //% "On %1, %2 wrote:" + precursor += qsTrId("jolla-components_email-la-reply_message_origin").arg(timestamp).arg(originalMessage.fromDisplayName) + return precursor + } + + function loadQuotedBody() { + if (action && action != 'forward') { + body.text = replyPrecursor() + body.quote = originalMessage.quotedBody + signature + // Append max 10000 chars from the quote + body.appendQuote(10000) + } + } + + SilicaFlickable { + property bool waitToAppend + anchors.fill: parent + contentWidth: parent.width + contentHeight: accountListModel.numberOfAccounts ? contentItem.y + contentItem.height : viewPlaceHolder.height + + NoAccountsPlaceholder { + id: viewPlaceHolder + enabled: !accountListModel.numberOfAccounts + } + + onAtYEndChanged: { + if (atYEnd && body.quote.length) { + if (quickScrollAnimating) { + waitToAppend = true + } else { + // Append next max 2500 chars from the quote + body.appendQuote(2500) + } + } + } + + onQuickScrollAnimatingChanged: { + if (!quickScrollAnimating && waitToAppend) { + waitToAppend = false + // Append next max 2500 chars from the quote + body.appendQuote(2500) + } + } + + RemorsePopup { + id: discardDraftRemorse + } + + PullDownMenu { + onActiveChanged: { + if (active) { + _ensureRecipientsComplete() + } + } + + MenuItem { + visible: accountListModel.numberOfAccounts + // explicit save action only when not doing it automatically + text: autoSaveDraft ? _strDiscardDraft : _strSaveDraft + enabled: messageContentModified() + //% "Discarding draft" + onClicked: autoSaveDraft ? (draft ? _discardDraft() + : discardDraftRemorse.execute(qsTrId("email-me-discarding_draft"), _discardDraft)) + : saveDraft() + } + MenuItem { + visible: accountListModel.numberOfAccounts + text: _strSend + enabled: hasRecipients && (subject.text != '' || body.text != '') && !maxAttachmentSizeExceeded + onClicked: sendMessage() + } + MenuItem { + visible: !accountListModel.numberOfAccounts + //: Add account menu item + //% "Add account" + text: qsTrId("jolla-email-me-add_account") + onClicked: { + var accountCreator = accountCreatorComponent.createObject(messageComposer) + accountCreator.creationCompleted.connect(function() { accountCreator.destroy() }) + } + } + } + + PushUpMenu { + visible: accountListModel.numberOfAccounts && flickable.contentHeight > 1.5*(isLandscape ? Screen.width : Screen.height) + + onActiveChanged: { + if (active) { + _ensureRecipientsComplete() + } + } + + MenuItem { + text: _strSend + enabled: hasRecipients && (subject.text != '' || body.text != '') + onClicked: sendMessage() + } + MenuItem { + text: autoSaveDraft ? _strDiscardDraft : _strSaveDraft + enabled: messageContentModified() + //% "Discarding draft" + onClicked: autoSaveDraft ? (draft ? _discardDraft() + : discardDraftRemorse.execute(qsTrId("email-me-discarding_draft"), _discardDraft)) + : saveDraft() + } + } + + + Column { + id: contentItem + visible: accountListModel.numberOfAccounts + y: isLandscape ? Theme.paddingMedium : 0 + width: parent.width - x + opacity: messageComposer.enabled ? 1. : Theme.opacityLow + + PageHeader { + id: pageHeader + //: New mail page title + //% "New mail" + title: qsTrId("jolla-email-he-new_mail") + } + + Compressor { + id: metadata + expanderItem: expanderControl + + width: parent.width + + Column { + property string accountDisplayName: accountListModel.displayNameFromAccountId(accountId) + property string accountEmailAddress: accountListModel.emailAddressFromAccountId(accountId) + + width: parent.width + + EmailRecipientField { + id: to + + compressible: false + contactSearchModel: contactSearchModel + onlineSearchModel: messageComposer.onlineSearchModel + onlineSearchDisplayName: parent.accountDisplayName ? parent.accountDisplayName : parent.accountEmailAddress + showLabel: _isPortrait + + //: 'To' recipient label + //% "To" + placeholderText: qsTrId("jolla-components_email-la-to") + + onLastFieldExited: { + if (!cc.compressed) { + cc.forceActiveFocus() + } else if (!bcc.compressed) { + bcc.forceActiveFocus() + } else { + subject.forceActiveFocus() + } + } + } + EmailRecipientField { + id: cc + + contactSearchModel: contactSearchModel + onlineSearchModel: messageComposer.onlineSearchModel + onlineSearchDisplayName: parent.accountDisplayName ? parent.accountDisplayName : parent.accountEmailAddress + showLabel: _isPortrait + + //: 'CC' recipient label + //% "Cc" + placeholderText: qsTrId("jolla-components_email-la-cc") + + onLastFieldExited: { + if (!bcc.compressed) { + bcc.forceActiveFocus() + } else { + subject.forceActiveFocus() + } + } + } + EmailRecipientField { + id: bcc + + contactSearchModel: contactSearchModel + onlineSearchModel: messageComposer.onlineSearchModel + onlineSearchDisplayName: parent.accountDisplayName ? parent.accountDisplayName : parent.accountEmailAddress + showLabel: _isPortrait + + //: 'BCC' recipient label + //% "Bcc" + placeholderText: qsTrId("jolla-components_email-la-bcc") + + onLastFieldExited: { + subject.forceActiveFocus() + } + } + MetaDataComboBox { + id: from + // Don't allow to change from account of a existent draft + visible: !draft && accountListModel.numberOfAccounts > 1 + + menu: ContextMenu { + width: parent ? parent.width : 0 + + Repeater { + id: fromRepeater + model: accountListModel + MenuItem { + text: emailAddress + } + } + } + + //: From label + //% "From:" + label: qsTrId("jolla-components_email-la-from") + + onCurrentIndexChanged: { + if (accountId !== accountListModel.accountId(currentIndex)) { + accountId = accountListModel.accountId(currentIndex) + var newSignature = '\n\n-- \n' + accountListModel.signature(accountId) + if (newSignature !== signature) { + // only part of the signature is in the screen, flush the rest + if (body.quote.length && body.quote.length <= signature.length) { + body.appendQuote(body.quote.length) + } + + var appendSignature = accountListModel.appendSignature(accountId) + var textAfterSignature = "" + var tempText = "" + if (body.quote.length) { + tempText = body.quote + } else { + tempText = body.text + } + + var signatureIndex = tempText.lastIndexOf(signature) + if (signatureIndex != -1) { + textAfterSignature = tempText.substring(signatureIndex + signature.length, tempText.length) + tempText = tempText.substring(0, signatureIndex) + } + + if (appendSignature) { + signature = newSignature + } else { + signature = "" + } + + if (body.quote.length) { + body.quote = tempText + signature + textAfterSignature + } else { + body.text = tempText + signature + textAfterSignature + } + } + } + } + } + MetaDataComboBox { + id: importance + compressible: currentIndex === 0 + + menu: ContextMenu { + width: parent ? parent.width : 0 + + MenuItem { + //: Normal priority + //% "Normal" + text: qsTrId("jolla-email-la-priority_Normal") + } + MenuItem { + //: High priority + //% "High" + text: qsTrId("jolla-email-la-priority_high") + } + MenuItem { + //: Low priority + //% "Low" + text: qsTrId("jolla-email-la-priority_low") + } + } + + //: Importance label + //% "Importance:" + label: qsTrId("jolla-components_email-la-importance") + } + CompressibleItem { + id: cryptoSignatureSwitch + property alias checked: signatureSwitch.checked + visible: accountListModel.cryptoSignatureType(accountId).length > 0 + compressible: !signatureSwitch.error + SignatureSwitch { + id: signatureSwitch + visible: !cryptoSignatureSwitch.compressed + width: parent.width + checked: accountListModel.useCryptoSignatureByDefault(accountId) + protocol: message.cryptoProtocolForKey + (accountListModel.cryptoSignatureType(accountId) + ,accountListModel.cryptoSignatureIds(accountId)) + error: message.signatureStatus == EmailMessage.SignedInvalid + } + } + CompressibleItem { + id: requestReadReceiptItem + compressible: true + TextSwitch { + id: requestReadReceiptSwitch + checked: false + visible: !requestReadReceiptItem.compressed + //: Enables read receipt request + //% "Request read receipt" + text: qsTrId("jolla-email-la-request_read_receipt") + } + } + CompressibleItem { + id: attachmentsItem + + compressible: attachmentFiles.count === 0 + + Item { + width: parent.width + // Padding small between this and subject field + implicitHeight: attachmentBg.height + (attachmentSizeLabel.visible && attachmentSizeLabel.text.length + ? attachmentSizeLabel.height : 0) + Theme.paddingSmall + + ListItem { + id: attachmentBg + onClicked: attachmentFiles.count === 0 ? modifyAttachments() : showAttachments() + enabled: !attachmentsItem.compressed + // If there is nothing to remove, don't show menu. + menu: attachmentFiles.count > 0 ? contextMenuComponent : null + + // TODO: Should be changed to Label, default color should be primaryColor + // as this is something that can be pressed. Need to change _updateAttachmentText as well. + TextField { + id: attachments + + anchors { + left: parent.left + right: addButton.left + rightMargin: Theme.paddingMedium + verticalCenter: parent.verticalCenter + verticalCenterOffset: Theme.paddingSmall + } + labelVisible: false + color: attachmentBg.highlighted ? Theme.highlightColor : Theme.primaryColor + placeholderColor: color + + readOnly: true + // Disable mouse handling so that List + enabled: false + opacity: 1.0 // shouldn't look disabled + + //: Attachments selector + //% "Add attachment" + placeholderText: qsTrId("jolla-components_email-ph-attachments") + } + + Image { + id: addButton + + source: "image://theme/icon-m-add" + (attachmentBg.highlighted ? "?" + Theme.highlightColor : "") + opacity: Theme.opacityHigh + + anchors { + verticalCenter: parent.verticalCenter + right: parent.right + rightMargin: Theme.horizontalPageMargin - Theme.paddingMedium + } + } + } + + Label { + id: attachmentSizeLabel + + anchors { + top: attachmentBg.bottom + left: attachmentBg.left + leftMargin: Theme.horizontalPageMargin + right: attachmentBg.right + rightMargin: Theme.horizontalPageMargin + } + + color: maxAttachmentSizeExceeded ? "#ff4d4d" : Theme.highlightColor + wrapMode: Text.Wrap + width: attachmentBg.width + font.pixelSize: Theme.fontSizeExtraSmall + + text: { + if (totalAttachmentSize > 0) { + if (maxAttachmentSizeExceeded) { + //% "Email cannot be sent! Total file size exceeds %1." + return qsTrId("jolla-components_email-la-attachments_size_exceed_max").arg(Format.formatFileSize(attachmentSizeMaxConfig.value)) + } else if (totalAttachmentSize > attachmentSizeWarningConfig.value) { + //% "Total file size exceeds %1. Consider removing some attachments." + return qsTrId("jolla-components_email-la-attachments_size_exceed_warning").arg(Format.formatFileSize(attachmentSizeWarningConfig.value)) + } + } + return "" + } + } + + // This should be attachments.text: _updateAttachmentText() instead + // but currectly _updateAttachmentText() break the binding. + Connections { + target: attachmentFiles + onCountChanged: _updateAttachmentText() + } + + Component { + id: contextMenuComponent + + ContextMenu { + MenuItem { + visible: attachmentFiles.count > 0 + //: When plural "Remove all attachments" and singular "Remove attachment". + //% "Remove attachment" + text: qsTrId("jolla-email-me-remove_all_attachments", attachmentFiles.count) + onClicked: { + attachmentFiles.clear() + attachments.text = "" + } + } + } + } + } + } + MetaDataTextField { + id: subject + compressible: false + + //: Subject label + //% "Subject" + placeholderText: qsTrId("jolla-components_email-la-subject") + onEnterKeyClicked: { + body.forceActiveFocus() + } + } + } + } + + MouseArea { + width: parent.width + height: expanderControl.height + Expander { + id: expanderControl + + minimumHeight: metadata.minimumHeight + maximumHeight: metadata.maximumHeight + + anchors { + right: parent.right + rightMargin: Theme.horizontalPageMargin - Theme.paddingMedium + } + } + onClicked: body.forceActiveFocus() + } + + // What should this control be? For now, just a text field + TextArea { + id: body + + property string quote + + width: parent.width + background: null // expanding text areas with nothing below them don't need bottom border background + height: Math.max(messageComposer.height - (contentItem.y + (isLandscape ? 0 : pageHeader.height) + + metadata.height + expanderControl.height), + implicitHeight) + color: Theme.primaryColor + font { pixelSize: Theme.fontSizeMedium; family: Theme.fontFamily } + + //% "Write message..." + placeholderText: (action.slice(0, 5) !== 'reply') ? qsTrId("jolla-components_email-ph-body") + //: Reply text placeholder + //% "Write reply..." + : qsTrId("jolla-components_email-ph-reply") + + function appendQuote(maxLength) { + var lineBreak = -1 + if (quote.length > maxLength) { + lineBreak = quote.lastIndexOf('\n', maxLength) + } + var cutIndex = (lineBreak < maxLength - 200) ? maxLength : lineBreak + text = text + quote.substring(0, cutIndex) + quote = quote.substring(cutIndex) + } + } + } + VerticalScrollDecorator {} + } + + PageBusyIndicator { + running: !messageComposer.enabled + } + + Component { + id: contentPicker + + MultiContentPickerDialog { + //% "Attach files" + title: qsTrId("jolla-components_email-he-attach-files") + } + } + + EmailAccountListModel { + id: accountListModel + onlyTransmitAccounts: true + } + + function _updateAttachmentText() { + var names = [] + var attachmentTextUpdated = false + totalAttachmentSize = 0 + for (var i = 0; i < attachmentFiles.count; ++i) { + var attachmentObj = attachmentFiles.get(i) + names.push(attachmentObj.title) + totalAttachmentSize += attachmentObj.fileSize + attachments.text = names.join(Format.listSeparator) + if (!attachmentTextUpdated && attachments.implicitWidth > attachments.width) { + while (names.length > 1 && attachments.implicitWidth > attachments.width) { + names.pop() + //: Number of additional attachments that are not currently shown + //% "%n other(s)" + var more = qsTrId("jolla-components_email-la-attachments_summary", attachmentFiles.count - names.length) + attachments.text = names.join(Format.listSeparator) + Format.listSeparator + more + } + attachmentTextUpdated = true + } + } + + var attachmentSizeText = totalAttachmentSize == 0 ? "" : " (" + Format.formatFileSize(totalAttachmentSize) + ")" + attachments.text += attachmentSizeText + + // This format is used in case above loop produces too long format. + if (attachments.implicitWidth > attachments.width) { + //: Number of attachments, should have singular and plurar formats. Text should be relatively short (max 24 chars). + //% "%n attachment(s)" + attachments.text = qsTrId("jolla-components_email-la-attachments", attachmentFiles.count) + attachmentSizeText + } + + attachments.text = attachmentFiles.count > 0 ? attachments.text : "" + } + + Component.onCompleted: { + if (accountListModel.numberOfAccounts) { + if (action) { + accountId = originalMessage.accountId + } + if (draft) { + accountId = message.accountId + } + + // If account is not set or is not sending capable, use the default one if it exists + if (!accountId || accountListModel.indexFromAccountId(accountId) < 0) { + accountId = defaultAccountConfig.value + } + + var currentIndex = 0 + + if (accountId) { + currentIndex = accountListModel.indexFromAccountId(accountId) + if (currentIndex >= 0) { + from.currentIndex = currentIndex + } else { + // Use first account in the model + accountId = accountListModel.accountId(0) + } + } else { + // If accountId is not valid(e.g default account got disabled) use first account in the model + accountId = accountListModel.accountId(0) + } + + if (accountListModel.appendSignature(accountId)) { + signature = '\n\n-- \n' + accountListModel.signature(accountId) + } + + var priority = EmailMessage.NormalPriority + + if (action) { + message.originalMessageId = originalMessage.messageId + var subjectText = originalMessage.subject + if (action == 'forward') { + // Not translated: + if (subjectText.slice(0, 4) != 'Fwd:') { + subjectText = 'Fwd: ' + subjectText + } + priority = originalMessage.priority + message.responseType = EmailMessage.Forward + if (!originalMessage.calendarInvitationSupportsEmailResponses) { + // Do not attach original attachments, since SmartForward will be used by EAS daemon + // and server will attach them automatically. This is valid only for EAS accounts. + // TODO: Update it with a different check if/when calendarInvitationSupportsEmailResponses + // will returns true for non-EAS accounts as well. + // TODO:2 User will not/shouldn't be able to remove original invitation attachments. + setOriginalMessageAttachments() + } + + if (originalMessage.contentType == EmailMessage.Plain) { + // to be removed, just temporary to provide at least same functionality as before + body.text = forwardPrecursor() + body.quote = originalMessage.quotedBody + signature + // Append max 10000 chars from the quote + body.appendQuote(10000) + } else { // originalMessage.contentType == EmailMessage.HTML + // forward as an attachment + attachmentFiles.append({ + "url": "id://" + originalMessageId, + "fileSize": originalMessage.size, + "title": originalMessage.subject, + "mimeType": "message/rfc822", + "FromOriginalMessage": "false" + }) + body.text = message.body + signature + } + } else { + // Not translated: + if (subjectText.slice(0, 3) != 'Re:') { + subjectText = 'Re: ' + subjectText + } + var replyTo = originalMessage.replyTo ? originalMessage.replyTo : originalMessage.fromAddress + + // Use slice() to create a new array object that can be modified (QML limitation, should implicitly happen when you start to modify array var) + var recipientsUsed = false + var toRecipients = originalMessage.toEmailAddresses.slice() + var ccRecipients = originalMessage.ccEmailAddresses.slice() + + // don't reply to yourself when choosing reply for message you sent + var usersEmailAddress = accountListModel.emailAddressFromAccountId(messageComposer.accountId) + if ((action == 'reply' || action == 'replyAll') && usersEmailAddress == replyTo && originalMessage.recipients.length > 0) { + recipientsUsed = true + replyTo = toRecipients + cc.setRecipients(ccRecipients) + } + + to.setRecipients(replyTo) + + if (action == 'replyAll') { + message.responseType = EmailMessage.ReplyToAll + + if (!recipientsUsed) { + var fromIndex = toRecipients.indexOf(accountListModel.emailAddress(currentIndex >= 0 ? currentIndex : 0)) + if (fromIndex != -1) { + // Remove current from address from the list + toRecipients.splice(fromIndex, 1) + } + + var fromCcIndex = ccRecipients.indexOf(accountListModel.emailAddress(currentIndex >= 0 ? currentIndex : 0)) + if (fromCcIndex != -1) { + ccRecipients.splice(fromCcIndex, 1) + } + + to.setRecipients(toRecipients) + cc.setRecipients(ccRecipients) + } + } else { + message.responseType = EmailMessage.Reply + } + } + + subject.text = subjectText + } else { + // Don't overwrite response type of existent draft + if (!draft) { + message.responseType = EmailMessage.NoResponse + } + priority = message.priority + to.setRecipients(message.to) + cc.setRecipients(message.cc) + bcc.setRecipients(message.bcc) + subject.text = message.subject + body.text = message.body + (draft ? "" : signature) + if (draft) { + setOriginalMessageAttachments() + } + } + + importance.currentIndex = (priority === EmailMessage.HighPriority) ? 1 : (priority === EmailMessage.LowPriority) ? 2 : 0 + + // Do not change request read receipt value of existent draft + if (!draft) { + requestReadReceiptSwitch.checked = false + } else { + requestReadReceiptSwitch.checked = message.requestReadReceipt + } + + if (to.empty && cc.empty && bcc.empty) { + to.forceActiveFocus() + } else { + if (subject.text == "") { + subject.forceActiveFocus() + } else { + body.forceActiveFocus() + if (!action) { + // Move the cursor to the end of the body, except with reply/fw + body.cursorPosition = body.text.length - signature.length + } + } + } + } + + createOnlineSearchModel() + } + + ConfigurationValue { + id: defaultAccountConfig + key: "/apps/jolla-email/settings/default_account" + defaultValue: 0 + } + + ConfigurationValue { + id: attachmentSizeWarningConfig + key: "/apps/jolla-email/settings/attachment_size_warning" + defaultValue: 10 * 1024 * 1024 // 10 MB + } + + ConfigurationValue { + id: attachmentSizeMaxConfig + key: "/apps/jolla-email/settings/attachment_size_max" + defaultValue: 25 * 1024 * 1024 // 25 MB + } +} diff --git a/usr/lib/qt5/qml/com/jolla/email/EmailRecipientField.qml b/usr/lib/qt5/qml/com/jolla/email/EmailRecipientField.qml new file mode 100644 index 00000000..b8265951 --- /dev/null +++ b/usr/lib/qt5/qml/com/jolla/email/EmailRecipientField.qml @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2013 – 2019 Jolla Ltd. + * Copyright (c) 2019 – 2020 Open Mobile Platform LLC. + * + * License: Proprietary + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Sailfish.Contacts 1.0 + +CompressibleItem { + id: root + + property alias placeholderText: recipientField.placeholderText + property alias summaryPlaceholderText: recipientField.summaryPlaceholderText + property alias summary: recipientField.summary + property alias contactSearchModel: recipientField.contactSearchModel + property alias onlineSearchModel: recipientField.onlineSearchModel + property alias onlineSearchDisplayName: recipientField.onlineSearchDisplayName + property alias empty: recipientField.empty + property alias showLabel: recipientField.showLabel + + signal lastFieldExited + + function recipientsToString() { + return recipientField.recipientsToString() + } + + function setRecipients(recipients) { + recipientField.setEmailRecipients(recipients) + } + + function forceActiveFocus() { + recipientField.forceActiveFocus() + } + + function updateSummary() { + recipientField.updateSummary() + } + + function saveNewContacts() { + recipientField.saveNewContacts() + } + + width: parent.width + compressible: recipientField.empty + expandedHeight: recipientField.height + + RecipientField { + id: recipientField + visible: !root.compressed + onLastFieldExited: root.lastFieldExited() + inputMethodHints: Qt.ImhNoPredictiveText | Qt.ImhNoAutoUppercase | Qt.ImhEmailCharactersOnly + } +} diff --git a/usr/lib/qt5/qml/com/jolla/email/Expander.qml b/usr/lib/qt5/qml/com/jolla/email/Expander.qml new file mode 100644 index 00000000..02e86c11 --- /dev/null +++ b/usr/lib/qt5/qml/com/jolla/email/Expander.qml @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2013 – 2019 Jolla Ltd. + * + * License: Proprietary + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 + +IconButton { + id: expander + + property real minimumHeight + property real maximumHeight + + property real expansion: _expansionRestartValue + + property int clickAnimationDuration: 300 + property int dragAnimationDuration: 200 + + property bool dragging: drag.active + property bool changing: dragging || expansionAnimation.running + + property bool _expanded + property real _initialExpansion + property real _expansionRestartValue + + icon.source: "image://theme/icon-lock-more" + + drag.target: expander + drag.axis: Drag.YAxis + drag.minimumY: 0 + drag.maximumY: 0 + + drag.onActiveChanged: { + if (drag.active) { + _initialExpansion = expansion + + // Only start dragging if we're currently at the boundary + if (expansion < 0.01) { + // Make sure animation is not running and we're at initial position + expansionAnimation.complete() + drag.minimumY = expander.y + drag.maximumY = expander.y + (maximumHeight - minimumHeight) + } else if (expansion > 0.99) { + // Make sure animation is not running and we're at initial position + expansionAnimation.complete() + drag.minimumY = expander.y - (maximumHeight - minimumHeight) + drag.maximumY = expander.y + } + } else { + // Reset drag bounds first in order to get out of "dragging" state + // before starting the animation (or resetting "expansion") + drag.maximumY = 0 + drag.minimumY = 0 + + // Animate to the final position + _expansionRestartValue = expansion + if (_initialExpansion < expansion) { + _expanded = (expansion > 0.33) + } else { + _expanded = (expansion > 0.66) + } + + // Only animate if we are not already at the boundary + if (expansion < 0.01) { + expansion = 0.0 + } else if (expansion > 0.99) { + expansion = 1.0 + } else { + expansionAnimation.duration = dragAnimationDuration + expansionAnimation.easing.type = Easing.OutQuad + expansionAnimation.restart() + } + } + } + + states: State { + name: "dragging" + when: expander.drag.maximumY && expander.drag.maximumY != expander.drag.minimumY + PropertyChanges { + target: expander + expansion: (expander.y - expander.drag.minimumY) / (expander.drag.maximumY - expander.drag.minimumY) + } + AnchorChanges { + target: expander + anchors { top: undefined; bottom: undefined } + } + } + + onClicked: { + _expanded = !_expanded + expansionAnimation.duration = clickAnimationDuration + expansionAnimation.easing.type = Easing.InOutQuad + expansionAnimation.restart() + } + + NumberAnimation on expansion { + id: expansionAnimation + to: expander._expanded ? 1.0 : 0.0 + } +} diff --git a/usr/lib/qt5/qml/com/jolla/email/MetaDataComboBox.qml b/usr/lib/qt5/qml/com/jolla/email/MetaDataComboBox.qml new file mode 100644 index 00000000..09215dcc --- /dev/null +++ b/usr/lib/qt5/qml/com/jolla/email/MetaDataComboBox.qml @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2013 – 2019 Jolla Ltd. + * + * License: Proprietary + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 + +CompressibleItem { + id: root + property alias menu: comboBox.menu + property alias label: comboBox.label + property alias value: comboBox.value + property alias currentIndex: comboBox.currentIndex + + expandedHeight: comboBox.height ? comboBox.height : comboBox.implicitHeight + + ComboBox { + id: comboBox + visible: !root.compressed + anchors.bottom: parent.bottom + } +} diff --git a/usr/lib/qt5/qml/com/jolla/email/MetaDataTextField.qml b/usr/lib/qt5/qml/com/jolla/email/MetaDataTextField.qml new file mode 100644 index 00000000..ab146e5a --- /dev/null +++ b/usr/lib/qt5/qml/com/jolla/email/MetaDataTextField.qml @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2013 – 2019 Jolla Ltd. + * + * License: Proprietary + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 + +CompressibleItem { + id: root + property alias placeholderText: textField.placeholderText + property alias text: textField.text + + signal enterKeyClicked + + function forceActiveFocus() { + textField.forceActiveFocus() + } + + compressible: textField.text.length === 0 + + TextField { + id: textField + visible: !root.compressed + anchors.bottom: parent.bottom + horizontalAlignment: Text.AlignLeft + + EnterKey.enabled: text.length > 0 + EnterKey.iconSource: "image://theme/icon-m-enter-next" + EnterKey.onClicked: root.enterKeyClicked() + } +} diff --git a/usr/lib/qt5/qml/com/jolla/email/NoAccountsPlaceholder.qml b/usr/lib/qt5/qml/com/jolla/email/NoAccountsPlaceholder.qml new file mode 100644 index 00000000..2b567d3a --- /dev/null +++ b/usr/lib/qt5/qml/com/jolla/email/NoAccountsPlaceholder.qml @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2018 – 2019 Jolla Ltd. + * + * License: Proprietary + */ + +import QtQuick 2.0 +import Sailfish.Policy 1.0 +import Sailfish.Silica 1.0 +import org.nemomobile.systemsettings 1.0 + +ViewPlaceholder { + //: No accounts empty state + //% "No accounts" + text: qsTrId("email-la_no_accounts") + hintText: AccessPolicy.accountCreationEnabled ? + //: Pull down to add account hint text + //% "Pull down to add an account" + qsTrId("email-la_no_accounts_hint_text") : + //: %1 is operating system name without OS suffix + //% "Account creation disabled by %1 Device Manager" + qsTrId("email-la-accounts_creation_disabled_by_device_manager") + .arg(aboutSettings.baseOperatingSystemName) + + AboutSettings { + id: aboutSettings + } +} diff --git a/usr/lib/qt5/qml/com/jolla/email/OnlineSearchModel.qml b/usr/lib/qt5/qml/com/jolla/email/OnlineSearchModel.qml new file mode 100644 index 00000000..0401ed3a --- /dev/null +++ b/usr/lib/qt5/qml/com/jolla/email/OnlineSearchModel.qml @@ -0,0 +1,10 @@ +/* + * Copyright (c) 2020 Open Mobile Platform LLC. + * + * License: Proprietary + */ + +import com.jolla.sailfisheas 1.0 + +GalSearchModel { +} diff --git a/usr/lib/qt5/qml/com/jolla/email/SignatureSwitch.qml b/usr/lib/qt5/qml/com/jolla/email/SignatureSwitch.qml new file mode 100644 index 00000000..8ea72d9c --- /dev/null +++ b/usr/lib/qt5/qml/com/jolla/email/SignatureSwitch.qml @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2019 Jolla Ltd. + * + * License: Proprietary + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Nemo.Email 0.1 + +TextSwitch { + id: root + property var protocol + property bool error + //: Numerically sign email + //% "Sign email" + text: qsTrId("jolla-email-la-sign_email") + description: { + if (error) { + //% "Signature failed" + return qsTrId("jolla-email-la-crypto_signature_failure") + } else { + switch (protocol) + { + case EmailMessage.OpenPGP: + //% "PGP" + return qsTrId("jolla-email-la-crypto_signature_pgp") + case EmailMessage.SecureMIME: + //% "S/MIME" + return qsTrId("jolla-email-la-crypto_signature_smime") + default: + //% "Unknown type" + return qsTrId("jolla-email-la-crypto_signature_unknown") + } + } + } + onCheckedChanged: if (!checked) error = false + Rectangle { + anchors.fill: parent + opacity: root.error ? Theme.opacityHigh : 0.0 + color: Theme.errorColor + Behavior on opacity { FadeAnimation{} } + z: -1 + } +} diff --git a/usr/lib/qt5/qml/com/jolla/email/qmldir b/usr/lib/qt5/qml/com/jolla/email/qmldir new file mode 100644 index 00000000..9410080d --- /dev/null +++ b/usr/lib/qt5/qml/com/jolla/email/qmldir @@ -0,0 +1,5 @@ +module com.jolla.email +plugin jollaemailplugin +singleton BatchedMessageDeletion 1.0 BatchedMessageDeletion.qml +EmailComposer 1.1 EmailComposer.qml +NoAccountsPlaceholder 1.1 NoAccountsPlaceholder.qml diff --git a/usr/lib/qt5/qml/com/jolla/email/settings/translations/qmldir b/usr/lib/qt5/qml/com/jolla/email/settings/translations/qmldir new file mode 100644 index 00000000..8ccc0040 --- /dev/null +++ b/usr/lib/qt5/qml/com/jolla/email/settings/translations/qmldir @@ -0,0 +1,2 @@ +module com.jolla.email.settings.translations +plugin emailsettingsplugin diff --git a/usr/lib/qt5/qml/com/jolla/eventsview/nextcloud/NextcloudFeedItem.qml b/usr/lib/qt5/qml/com/jolla/eventsview/nextcloud/NextcloudFeedItem.qml index ef5f8dd2..aabc6d87 100644 --- a/usr/lib/qt5/qml/com/jolla/eventsview/nextcloud/NextcloudFeedItem.qml +++ b/usr/lib/qt5/qml/com/jolla/eventsview/nextcloud/NextcloudFeedItem.qml @@ -69,7 +69,7 @@ NotificationGroupMember { Label { id: timestampLabel - text: Format.formatDate(root.timestamp, Format.DurationElapsed) + text: Format.formatDate(root.timestamp, Format.TimeElapsed) font.pixelSize: Theme.fontSizeExtraSmall color: Theme.secondaryColor } diff --git a/usr/lib/qt5/qml/com/jolla/gallery/ambience/AmbienceList.qml b/usr/lib/qt5/qml/com/jolla/gallery/ambience/AmbienceList.qml index 74fdbebe..46ee94bb 100644 --- a/usr/lib/qt5/qml/com/jolla/gallery/ambience/AmbienceList.qml +++ b/usr/lib/qt5/qml/com/jolla/gallery/ambience/AmbienceList.qml @@ -2,7 +2,7 @@ import QtQuick 2.6 import Sailfish.Silica 1.0 import Sailfish.Ambience 1.0 import Sailfish.Gallery 1.0 -import org.nemomobile.thumbnailer 1.0 +import Nemo.Thumbnailer 1.0 SilicaListView { id: ambienceList diff --git a/usr/lib/qt5/qml/com/jolla/gallery/ambience/AmbienceSettingsPage.qml b/usr/lib/qt5/qml/com/jolla/gallery/ambience/AmbienceSettingsPage.qml index 8b9af4a2..4784fc8a 100644 --- a/usr/lib/qt5/qml/com/jolla/gallery/ambience/AmbienceSettingsPage.qml +++ b/usr/lib/qt5/qml/com/jolla/gallery/ambience/AmbienceSettingsPage.qml @@ -28,7 +28,6 @@ Page { allowedOrientations: Orientation.All Wallpaper { - id: wallpaper width: parent.width height: Math.max(0, -view.contentY + view.backgroundHeight) sourceItem: view.applicationWallpaper diff --git a/usr/lib/qt5/qml/com/jolla/gallery/ambience/ToneAction.qml b/usr/lib/qt5/qml/com/jolla/gallery/ambience/ToneAction.qml index 1c7d2b91..0a46bb0a 100644 --- a/usr/lib/qt5/qml/com/jolla/gallery/ambience/ToneAction.qml +++ b/usr/lib/qt5/qml/com/jolla/gallery/ambience/ToneAction.qml @@ -4,6 +4,7 @@ import Sailfish.Ambience 1.0 import Sailfish.Media 1.0 import com.jolla.settings.system 1.0 import org.nemomobile.systemsettings 1.0 +import Nemo.FileManager 1.0 AmbienceAction { id: action @@ -18,7 +19,14 @@ AmbienceAction { if (displayName.length > 0) { return displayName } - return metadataReader.getTitle(tone.url) + + if (tone.url == "") { + return "" + } else if (fileInfo.exists) { + return metadataReader.getTitle(tone.url) + } else { + return fileInfo.fileName + } } else { //% "No sound" return qsTrId("jolla-gallery-ambience-sound-la-no-alarm-sound") @@ -36,14 +44,21 @@ AmbienceAction { property list _resources: [ MetadataReader { id: metadataReader + }, + FileInfo { + id: fileInfo + + url: tone.url } ] editor: ValueButton { - id: toneEditor - label: action.label value: action.title + descriptionColor: Theme.errorColor + //% "Error: file not found" + description: (tone.url != "" && !fileInfo.exists) + ? qsTrId("jolla-gallery-ambience-sound-la-file_not_found") : "" rightMargin: Theme.horizontalPageMargin + Theme.itemSizeSmall + Theme.paddingMedium @@ -51,8 +66,6 @@ AmbienceAction { } dialog: Component { - id: soundDialog - SoundDialog { activeFilename: tone.url activeSoundTitle: action.title diff --git a/usr/lib/qt5/qml/com/jolla/gallery/ambience/Wallpaper.qml b/usr/lib/qt5/qml/com/jolla/gallery/ambience/Wallpaper.qml index ff52135c..a495310d 100644 --- a/usr/lib/qt5/qml/com/jolla/gallery/ambience/Wallpaper.qml +++ b/usr/lib/qt5/qml/com/jolla/gallery/ambience/Wallpaper.qml @@ -10,8 +10,6 @@ import Sailfish.Silica 1.0 import Sailfish.Silica.Background 1.0 ThemeBackground { - id: wallpaper - visible: sourceItem && sourceItem.status === Image.Ready patternItem: glassTextureImage diff --git a/usr/lib/qt5/qml/com/jolla/gallery/facebook/AccessTokensProvider.qml b/usr/lib/qt5/qml/com/jolla/gallery/facebook/AccessTokensProvider.qml new file mode 100644 index 00000000..dbd61dba --- /dev/null +++ b/usr/lib/qt5/qml/com/jolla/gallery/facebook/AccessTokensProvider.qml @@ -0,0 +1,54 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Sailfish.Gallery 1.0 +import Sailfish.Accounts 1.0 +import org.nemomobile.socialcache 1.0 + +QtObject { + id: root + + property var _accounts: { return {} } + + signal accessTokenRetrieved(string accountId, string accessToken) + + function requestAccessToken(accountId) { + if (!_accounts.hasOwnProperty(accountId)) { + _accounts[accountId] = accountComponent.createObject(root, {"identifier": accountId}) + return + } + + if (_accounts[accountId].accessToken !== "") { + accessTokenRetrieved(accountId, _accounts[accountId].accessToken) + } + } + + property KeyProviderHelper keyProviderHelper: KeyProviderHelper {} + + property Component accountComponent: Component { + + Account { + property string accessToken + + onAccessTokenChanged: { + root.accessTokenRetrieved(identifier, accessToken) + } + + onStatusChanged: { + if (status == Account.Initialized) { + // Sign in, and get access token. + var params = signInParameters("facebook-sync") + params.setParameter("ClientId", root.keyProviderHelper.facebookClientId) + params.setParameter("UiPolicy", SignInParameters.NoUserInteractionPolicy) + signIn("Jolla", "Jolla", params) + } + } + + onSignInResponse: { + var accessTok = data["AccessToken"] + if (accessTok != "") { + accessToken = accessTok + } + } + } + } +} diff --git a/usr/lib/qt5/qml/com/jolla/gallery/facebook/AddCommentPage.qml b/usr/lib/qt5/qml/com/jolla/gallery/facebook/AddCommentPage.qml new file mode 100644 index 00000000..02e9649d --- /dev/null +++ b/usr/lib/qt5/qml/com/jolla/gallery/facebook/AddCommentPage.qml @@ -0,0 +1,218 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import org.nemomobile.social 1.0 + +Page { + property string nodeIdentifier + property alias commentsModel: commentsList.model + property FacebookPhoto photoItem + property string photoUserId + + allowedOrientations: window.allowedOrientations + + function formattedTimestamp(isostr) { + var parts = isostr.match(/\d+/g) + // Make sure to use UTC time. + var dateTime = new Date(Date.UTC(parts[0], parts[1] - 1, parts[2], parts[3], parts[4], parts[5])) + var today = new Date + + // return time, if it's today + if (dateTime.getFullYear() === today.getFullYear() && + dateTime.getMonth() === today.getMonth() && + dateTime.getDate() === today.getDate()) { + return Format.formatDate(dateTime, Formatter.TimepointRelative) + } + + return Format.formatDate(dateTime, Formatter.TimeElapsed) + } + + Connections { + target: photoItem + onStatusChanged: { + if (photoItem.status === Facebook.Idle) { + if (commentsModel.count > 0) { + commentsModel.loadNext() + } else { + commentsModel.repopulate() + } + } + } + } + + SilicaListView { + id: commentsList + + spacing: Theme.paddingMedium + anchors.fill: parent + currentIndex: -1 + focus: true + + //: "Facebook album comments page title + //% "Comments" + header: PageHeader { title: qsTrId("jolla-gallery-facebook-he-comments") } + + ViewPlaceholder { + //% "Error loading comments" + text: qsTrId("jolla-gallery-facebook-la-error_loading_comments") + enabled: commentsModel.count === 0 && (commentsModel.status === SocialNetwork.Error || commentsModel.status === SocialNetwork.Invalid) + } + + BusyIndicator { + size: BusyIndicatorSize.Large + anchors.centerIn: parent + running: commentsModel.count === 0 && (commentsModel.status === SocialNetwork.Initializing || commentsModel.status === SocialNetwork.Busy) + } + + delegate: Item { + property bool _showDelegate: commentsList.count + + width: commentsList.width + height: (likeCount.visible ? (likeCount.y + likeCount.height) : (commentColumn.y + commentColumn.height)) + + Theme.paddingSmall + opacity: _showDelegate ? 1 : 0 + Behavior on opacity { FadeAnimation {} } + + Rectangle { + id: avatarPlaceholder + width: Theme.itemSizeSmall + height: Theme.itemSizeSmall + color: Theme.highlightColor + opacity: 0.5 + x: Theme.horizontalPageMargin + } + + Image { + id: avatar + // Fetch the avatar from the constructed url + source: _showDelegate ? "http://graph.facebook.com/v2.6/"+ model.contentItem.from.objectIdentifier + "/picture" : "" + clip: true + anchors.fill: avatarPlaceholder + fillMode: Image.PreserveAspectCrop + smooth: true + } + + Column { + id: commentColumn + spacing: Theme.paddingSmall + anchors { + left: avatar.right + leftMargin: Theme.paddingMedium + top: avatar.top + right: parent.right + rightMargin: Theme.horizontalPageMargin + } + + Label { + text: _showDelegate ? model.contentItem.message : "" + width: parent.width + font.pixelSize: Theme.fontSizeExtraSmall + horizontalAlignment: Text.AlignLeft + wrapMode: Text.Wrap + } + + Flow { + width: parent.width + spacing: Theme.paddingSmall + + Label { + text: _showDelegate ? model.contentItem.from.objectName : "" + color: Theme.secondaryColor + horizontalAlignment: Text.AlignLeft + verticalAlignment: Text.AlignTop + font.pixelSize: Theme.fontSizeExtraSmall + } + + Label { + text: _showDelegate ? formattedTimestamp(model.contentItem.createdTime) : "" + color: Theme.secondaryHighlightColor + font.pixelSize: Theme.fontSizeExtraSmall + } + } + } + + Label { + id: likeCount + visible: _showDelegate ? model.contentItem.likeCount > 0 : "" + text: model.contentItem.likeCount + color: Theme.highlightColor + horizontalAlignment: Text.AlignRight + font.pixelSize: Theme.fontSizeExtraSmall + anchors { + top: commentColumn.bottom + topMargin: Theme.paddingSmall + right: commentColumn.left + rightMargin: Theme.paddingMedium + } + } + + Label { + text: _showDelegate + ? //: Text at the right side of like count, should have plural handling for like vs likes. + //% "likes" + qsTrId("jolla_gallery_facebook-la-number-of-likes-for-comment", model.contentItem.likeCount) + : "" + visible: likeCount.visible + font.pixelSize: Theme.fontSizeExtraSmall + anchors { + baseline: likeCount.baseline + left: commentColumn.left + } + } + } + + footer: Item { + height: addCommentTextField.height + width: commentsList.width + + TextArea { + id: addCommentTextField + + //% "Write comment" + label: qsTrId("jolla_gallery_facebook-la-write-comment-page") + placeholderText: label + anchors { left: parent.left; right: buttonArea.left } + focus: true + } + + MouseArea { + id: buttonArea + anchors { + top: buttonText.top + topMargin: -Theme.paddingLarge + leftMargin: -Theme.paddingLarge - Math.max(0, Theme.itemSizeSmall - buttonText.width) + left: buttonText.left + right: parent.right + bottom: parent.bottom + } + enabled: addCommentTextField.text.length > 0 + onClicked: { + if (addCommentTextField.text != "") { + photoItem.uploadComment(addCommentTextField.text) + addCommentTextField.focus = false + addCommentTextField.text = "" + } + } + } + + Label { + id: buttonText + anchors { + right: parent.right + rightMargin: Theme.horizontalPageMargin + verticalCenter: addCommentTextField.top + verticalCenterOffset: addCommentTextField.textVerticalCenterOffset + (addCommentTextField._editor.height - height) + } + + font.pixelSize: Theme.fontSizeSmall + color: !buttonArea.enabled ? Theme.secondaryColor + : (buttonArea.pressed ? Theme.highlightColor + : Theme.primaryColor) + + //: Send comment button in Facebook album's comment page + //% "Send" + text: qsTrId("jolla-gallery-facebook-bt-send-comment") + } + } + VerticalScrollDecorator {} + } +} diff --git a/usr/lib/qt5/qml/com/jolla/gallery/facebook/AlbumDelegate.qml b/usr/lib/qt5/qml/com/jolla/gallery/facebook/AlbumDelegate.qml new file mode 100644 index 00000000..ca519670 --- /dev/null +++ b/usr/lib/qt5/qml/com/jolla/gallery/facebook/AlbumDelegate.qml @@ -0,0 +1,71 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Sailfish.Silica.theme 1.0 +import org.nemomobile.socialcache 1.0 +import com.jolla.gallery.extensions 1.0 + +BackgroundItem { + id: root + + property string albumName + property string albumIdentifier + property string userIdentifier + property FacebookImageCacheModel imagesModel: FacebookImageCacheModel { + function nodeIdentifierValue() { + if (root.albumIdentifier == "" && root.userIdentifier == "") { + return "" + } else if (root.albumIdentifier == "" && root.userIdentifier != "") { + return "user-" + root.userIdentifier + } else { + return "album-" + root.albumIdentifier + } + } + + Component.onCompleted: refresh() + type: FacebookImageCacheModel.Images + nodeIdentifier: nodeIdentifierValue() + downloader: FacebookImageDownloader + } + + height: Theme.itemSizeExtraLarge + enabled: imagesModel.count > 0 + opacity: enabled ? 1.0 : 0.6 + + SlideshowIcon { + id: image + model: root.imagesModel + highlighted: root.highlighted + serviceIcon: "image://theme/graphic-service-facebook" + } + + Column { + anchors { + left: image.right + leftMargin: Theme.paddingLarge + right: parent.right + rightMargin: Theme.paddingMedium + verticalCenter: image.verticalCenter + } + + Label { + width: parent.width + text: albumName + font.family: Theme.fontFamilyHeading + font.pixelSize: Theme.fontSizeMedium + color: highlighted ? Theme.highlightColor : Theme.primaryColor + truncationMode: TruncationMode.Fade + } + + Label { + width: parent.width + + //: Photos count for facebook album + //% "%n photos" + text: qsTrId("jolla_gallery_facebook-album_photo_count", dataCount) + font.family: Theme.fontFamilyHeading + font.pixelSize: Theme.fontSizeSmall + color: highlighted ? Theme.secondaryHighlightColor : Theme.secondaryColor + truncationMode: TruncationMode.Fade + } + } +} diff --git a/usr/lib/qt5/qml/com/jolla/gallery/facebook/AlbumsPage.qml b/usr/lib/qt5/qml/com/jolla/gallery/facebook/AlbumsPage.qml new file mode 100644 index 00000000..71c5f217 --- /dev/null +++ b/usr/lib/qt5/qml/com/jolla/gallery/facebook/AlbumsPage.qml @@ -0,0 +1,63 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import org.nemomobile.socialcache 1.0 +import com.jolla.gallery 1.0 + +MediaSourcePage { + id: root + + property string userId // provided by the UsersPage.qml + + allowedOrientations: window.allowedOrientations + property bool _isPortrait: orientation === Orientation.Portrait + || orientation === Orientation.PortraitInverted + + SilicaListView { + anchors.fill: parent + header: PageHeader { title: root.title } + cacheBuffer: screen.height + delegate: AlbumDelegate { + albumName: model.title + albumIdentifier: model.facebookId + userIdentifier: model.userId + + onClicked: { + imagesModel.loadImages() + window.pageStack.animatorPush(Qt.resolvedUrl("PhotoGridPage.qml"), + {"albumName": albumName, + "albumIdentifier": albumIdentifier, + "model": imagesModel}) + } + } + + model: FacebookImageCacheModel { + id: fbAlbums + type: FacebookImageCacheModel.Albums + nodeIdentifier: root.userId + Component.onCompleted: refresh() + onNodeIdentifierChanged: refresh() + downloader: FacebookImageDownloader + } + + SyncHelper { + socialNetwork: SocialSync.Facebook + dataType: SocialSync.Images + onLoadingChanged: { + if (!loading) { + fbAlbums.refresh() + } + } + onProfileDeleted: { + var page = pageStack.currentPage + var prevPage = pageStack.previousPage(page) + while (prevPage) { + page = prevPage + prevPage = pageStack.previousPage(prevPage) + } + pageStack.pop(page) + } + } + + VerticalScrollDecorator {} + } +} diff --git a/usr/lib/qt5/qml/com/jolla/gallery/facebook/FacebookGalleryIcon.qml b/usr/lib/qt5/qml/com/jolla/gallery/facebook/FacebookGalleryIcon.qml new file mode 100644 index 00000000..6110b5e9 --- /dev/null +++ b/usr/lib/qt5/qml/com/jolla/gallery/facebook/FacebookGalleryIcon.qml @@ -0,0 +1,62 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import com.jolla.gallery 1.0 +import org.nemomobile.socialcache 1.0 + +MediaSourceIcon { + id: root + + property int modelCount: model ? model.count : 0 + timerEnabled: modelCount > 0 + + SyncHelper { + id: syncHelper + socialNetwork: SocialSync.Facebook + dataType: SocialSync.Images + } + + Item { + anchors.fill: parent + opacity: syncHelper.loading ? 0.3 : 1 + + ListView { + id: slideShow + visible: timerEnabled + interactive: false + currentIndex: 0 + clip: true + orientation: ListView.Horizontal + anchors.fill: parent + + model: root.model + + delegate: Image { + source: model.thumbnail != "" ? model.thumbnail + : "image://theme/graphic-service-facebook" + fillMode: Image.PreserveAspectCrop + clip: true + asynchronous: true + width: slideShow.width + height: slideShow.height + } + } + + Image { + anchors.fill: parent + Behavior on opacity { NumberAnimation { duration: 5000 }} + source: "image://theme/graphic-service-facebook" + fillMode: Image.PreserveAspectCrop + clip: true + opacity: timerEnabled ? 0 : 1 + } + } + + BusyIndicator { + visible: syncHelper.loading + size: BusyIndicatorSize.Medium + running: visible + anchors.centerIn: parent + } + + onTimerTriggered: slideShow.currentIndex = (slideShow.currentIndex + 1) % model.count +} diff --git a/usr/lib/qt5/qml/com/jolla/gallery/facebook/FullscreenPhotoPage.qml b/usr/lib/qt5/qml/com/jolla/gallery/facebook/FullscreenPhotoPage.qml new file mode 100644 index 00000000..79753889 --- /dev/null +++ b/usr/lib/qt5/qml/com/jolla/gallery/facebook/FullscreenPhotoPage.qml @@ -0,0 +1,415 @@ +import QtQuick 2.4 +import Sailfish.Silica 1.0 +import Sailfish.Silica.private 1.0 as Private +import com.jolla.gallery.facebook 1.0 +import org.nemomobile.social 1.0 +import org.nemomobile.socialcache 1.0 +import Sailfish.Gallery 1.0 + +FullscreenContentPage { + id: fullscreenPage + + property AccessTokensProvider accessTokensProvider + property FacebookImageCacheModel model + property int currentIndex: -1 + + // Private properties + property string _currentPhotoId + property string _prevPhotoId + property string _currentPhotoUserId + property real _rightMargin: pageStack.currentPage.isLandscape ? Theme.paddingLarge : Theme.horizontalPageMargin + + allowedOrientations: window.allowedOrientations + + // The following handlers make the Facebook elements to fetch new data about likes and comments. + // The data is being fetched only when overlay is visible with the likes and comments items. + // This way we decrease network load and don't request any data which user is not interested in. + property alias overlayActive: overlay.active + onOverlayActiveChanged: if (overlay.active) fetchData() + + Component.onCompleted: { + updateAccessToken() + slideshowView.positionViewAtIndex(currentIndex, PathView.Center) + } + + onCurrentIndexChanged: { + updateAccessToken() + if (!overlay.active) { + // Start timer to test if user hasn't flicked for a while, start downloading + // data from the network + imageFlickTimer.photoId = _currentPhotoId + imageFlickTimer.restart() + } + } + + function updateAccessToken() { + facebook.accessToken = "" + if (model) { + accessTokensProvider.requestAccessToken(model.getField(currentIndex, FacebookImageCacheModel.AccountId)) + } + } + + function fetchData() { + photoAndLikesModel.repopulate() + } + + function formattedTimestamp(isostr) { + var parts = isostr.match(/\d+/g) + var fixedDate = new Date(Date.UTC(parts[0], parts[1] - 1, parts[2], parts[3], parts[4], parts[5])) + return Format.formatDate(fixedDate, Formatter.TimepointRelative) + } + + // Returns string formatted e.g. "You, Mike M and 3 others like this + function likeInformation() { + // Not very pretty code but localization and how this message + // is expressed requires quite many variations + var isLikedByPhotoUser = photoAndLikesModel.node.liked + var photoUserName = "" + var users = new Array + for (var i = 0; i < photoAndLikesModel.count; i++) { + if (photoAndLikesModel.relatedItem(i).userIdentifier !== _currentPhotoUserId) { + users.push(photoAndLikesModel.relatedItem(i).userName) + } + } + + if (photoAndLikesModel.count == 1) { + if (isLikedByPhotoUser) { + //% "You like this" + return qsTrId("jolla_gallery_facebook-la-you-like-this") + } else { + //% "%1 likes this" + return qsTrId("gallery-fb-la-one-friend-likes-this") + .arg(users[0]) + } + } + if (photoAndLikesModel.count == 2) { + if (isLikedByPhotoUser) { + //% "You and %1 like this" + return qsTrId("jolla_gallery_facebook-la-you-and-another-friend-likes-this") + .arg(users[0]) + } else { + //% "%1 and %2 like this" + return qsTrId("jolla_gallery_facebook-la-two-friend-likes-this") + .arg(users[0]) + .arg(users[1]) + } + } + if (photoAndLikesModel.count > 2) { + if (isLikedByPhotoUser) { + //% "You, %1 and %n others like this" + return qsTrId("jolla_gallery_facebook-la-you-and-multiple-friend-like-this", photoAndLikesModel.likesCount - 2) + .arg(users[0]) + } else { + //% "%1 and %2 and %n others like this" + return qsTrId("jolla_gallery_facebook-la-multiple-friend-like-this", photoAndLikesModel.likesCount - 2) + .arg(users[0]) + .arg(users[1]) + } + } + // Return an empty string for 0 likes + return "" + } + + Connections { + target: accessTokensProvider + onAccessTokenRetrieved: { + var currentAccountId = fullscreenPage.model.getField(currentIndex, FacebookImageCacheModel.AccountId) + if (currentAccountId == accountId) { + facebook.accessToken = accessToken + if (overlay.active) { + fullscreenPage.fetchData() + } + } + } + } + + Facebook { id: facebook } + + // The likes model controls basically everything + // It's central node is used to perform like / unlike operations + // and also provide the number of likes and comments. + // This model is used to print a nice message about likes. + // Additionnal properties added helps to track if there is + // a loading in progress, and have persistant displays of + // the number of likes / comments during loading operations. + SocialNetworkModel { + id: photoAndLikesModel + property bool loading + property bool liked: true + property int likesCount: -1 + property int commentsCount: -1 + property string likeInfo + + function refreshLikesInfo() { + photoAndLikesModel.loading = false + photoAndLikesModel.liked = node.liked + photoAndLikesModel.likesCount = node.likesCount + photoAndLikesModel.commentsCount = node.commentsCount + photoAndLikesModel.likeInfo = fullscreenPage.likeInformation() + } + + socialNetwork: facebook + nodeIdentifier: fullscreenPage._currentPhotoId + // If you have a lot of likes, Facebook will provide + // them as paginated. So it is not reliable to get + // the likes by counting the number of elements in + // this model. + // + // We still need (up to) the first 3 people who liked + // that photo to display the "a, b and c liked that" + // string. So we only need to retrieve 3 likes. + filters: ContentItemTypeFilter { type: Facebook.Like; limit: 3 } + onNodeIdentifierChanged: { + photoAndLikesModel.loading = true + photoAndLikesModel.liked = false + photoAndLikesModel.likesCount = -1 + photoAndLikesModel.commentsCount = -1 + photoAndLikesModel.likeInfo = "" + } + + onStatusChanged: { + switch (status) { + case Facebook.Idle: + refreshLikesInfo() + break + } + } + } + + // This connection is used to react + // to changes of status of the node attached + // to likesModel + Connections { + target: photoAndLikesModel.node + onStatusChanged: { + switch (photoAndLikesModel.node.status) { + case Facebook.Idle: + photoAndLikesModel.repopulate() + break + default: + photoAndLikesModel.loading = true + break + } + } + } + + SocialNetworkModel { + id: commentsModel + socialNetwork: facebook + nodeIdentifier: fullscreenPage._currentPhotoId + filters: ContentItemTypeFilter { type: Facebook.Comment } + } + + // This timer is here to make data fetching a little more intelligent. Data is usually fetched + // from FB only when user taps view to show overlay controls and/or is flicking images while the likes + // and comment items are visible. This is the third case, when user flicks and overlay controls are hidden + // data is not fetched unless user stops flicking for 2 seconds. This might mean that user is interested + // in that image and will soon also show the controls that causes data fetch, but in this case the data + // will already be there. + Timer { + id: imageFlickTimer + property string photoId + interval: 2000 + onTriggered: { + if (photoId == _currentPhotoId && !overlay.active) { + fetchData() + } + } + } + + // Element for handling the actual flicking and image buffering + SlideshowView { + id: slideshowView + + model: fullscreenPage.model + currentIndex: fullscreenPage.currentIndex + onCurrentIndexChanged: fullscreenPage.currentIndex = currentIndex + interactive: model.count > 1 + clip: true + + delegate: MouseArea { + property url source: model.image + width: slideshowView.width + height: slideshowView.height + + onClicked: overlay.active = !overlay.active + + // Pass information about the current item to the top level view in a case + // user wants to check comments or add a new one, like etc.. + property bool isCurrentItem: PathView.isCurrentItem + onIsCurrentItemChanged: { + if (isCurrentItem) { + fullscreenPage._currentPhotoId = model.facebookId + fullscreenPage._currentPhotoUserId = model.userId + } + } + Image { + asynchronous: true + source: model.image + anchors.fill: parent + fillMode: Image.PreserveAspectFit + } + } + } + + GalleryOverlay { + id: overlay + + isImage: true + source: slideshowView.currentItem ? slideshowView.currentItem.source : "" + deletingAllowed: false + editingAllowed: false + sharingAllowed: false + anchors.fill: parent + z: model.count + 100 + topFade.height: socialHeader.height + Theme.itemSizeMedium + fadeOpacity: 0.7 + + additionalActions: Row { + spacing: Theme.paddingLarge + IconButton { + icon.source: "image://theme/icon-m-outline-like?" + Theme.lightPrimaryColor + highlighted: down || photoAndLikesModel.liked + enabled: !photoAndLikesModel.loading + + onClicked: { + var node = photoAndLikesModel.node + if (node.liked) { + node.unlike() + } else { + node.like() + } + } + } + + IconButton { + enabled: !photoAndLikesModel.loading + icon.source: "image://theme/icon-m-outline-chat?" + Theme.lightPrimaryColor + onClicked: { + // We load the comments when we need them + commentsModel.nodeIdentifier = fullscreenPage._currentPhotoId + commentsModel.repopulate() + pageStack.animatorPush(Qt.resolvedUrl("AddCommentPage.qml"), { + nodeIdentifier: _currentPhotoId, + commentsModel: commentsModel, + photoItem: photoAndLikesModel.node, + photoUserId: _currentPhotoUserId + }) + } + } + } + + Private.DismissButton { + id: dismissButton + } + + Column { + id: socialHeader + anchors { + top: dismissButton.top + left: parent.left + right: dismissButton.left + } + + Row { + x: Theme.horizontalPageMargin + spacing: Theme.paddingLarge + height: dismissButton.height + + Image { + source: "image://theme/icon-s-like" + "?" + Theme.highlightColor + anchors.verticalCenter: parent.verticalCenter + } + + Label { + color: Theme.highlightColor + text: photoAndLikesModel.likesCount == -1 ? "" : photoAndLikesModel.likesCount + anchors.verticalCenter: parent.verticalCenter + width: Theme.paddingLarge + } + + Image { + source: "image://theme/icon-s-chat" + "?" + Theme.highlightColor + anchors.verticalCenter: parent.verticalCenter + } + + Label { + color: Theme.highlightColor + opacity: photoAndLikesModel.loading ? 0.5 : 1 + text: photoAndLikesModel.commentsCount == -1 ? "" : photoAndLikesModel.commentsCount + anchors.verticalCenter: parent.verticalCenter + width: Theme.paddingLarge + } + } + + FontMetrics { + id: fontMetrics + font.pixelSize: Theme.fontSizeExtraSmall + } + + Item { + width: 1 + height: Theme.paddingSmall + } + + Label { + text: photoAndLikesModel.likeInfo + wrapMode: Text.Wrap + verticalAlignment: Text.AlignTop + x: Theme.horizontalPageMargin + height: text == "" ? fontMetrics.height : implicitHeight + width: parent.width - x + opacity: text == "" ? 0 : 1 + font.pixelSize: Theme.fontSizeExtraSmall + color: Theme.highlightColor + Behavior on height { FadeAnimation { property: "height" } } + Behavior on opacity { FadeAnimation {} } + } + + Item { + width: 1 + height: Theme.paddingMedium + Theme.paddingSmall + } + + Label { + //% "No title" + property string unknownNameStr: qsTrId("jolla_gallery_facebook-la-unnamed_photo") + property string photoNameStr: fullscreenPage.model.getField(fullscreenPage.currentIndex, + FacebookImageCacheModel.Title) + text: photoNameStr == "" ? unknownNameStr : photoNameStr + height: text == "" ? fontMetrics.height : implicitHeight + width: parent.width - x + wrapMode: Text.Wrap + x: Theme.horizontalPageMargin + color: Theme.highlightColor + font.pixelSize: Theme.fontSizeExtraSmall + } + + Item { + width: 1 + height: Theme.paddingLarge + } + + Row { + x: Theme.horizontalPageMargin + spacing: Theme.paddingMedium + + Image { + source: "image://theme/icon-s-service-facebook" + asynchronous: true + width: height + } + + Label { + property string dateTime: fullscreenPage.model.getField(fullscreenPage.currentIndex, + FacebookImageCacheModel.DateTaken) + text: formattedTimestamp(dateTime) + color: Theme.secondaryHighlightColor + font.pixelSize: Theme.fontSizeExtraSmall + anchors.verticalCenter: parent.verticalCenter + } + } + } + } +} + diff --git a/usr/lib/qt5/qml/com/jolla/gallery/facebook/PhotoGridPage.qml b/usr/lib/qt5/qml/com/jolla/gallery/facebook/PhotoGridPage.qml new file mode 100644 index 00000000..23fc6a9d --- /dev/null +++ b/usr/lib/qt5/qml/com/jolla/gallery/facebook/PhotoGridPage.qml @@ -0,0 +1,44 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Sailfish.Gallery 1.0 +import Sailfish.Accounts 1.0 +import com.jolla.gallery.facebook 1.0 +import org.nemomobile.socialcache 1.0 + +Page { + id: gridPage + + property string albumName + property alias model: grid.model + + // ----------------------------- + + property alias currentIndex: grid.currentIndex + allowedOrientations: window.allowedOrientations + + AccessTokensProvider {id: accessTokensProvider} + + ImageGridView { + id: grid + anchors.fill: parent + + header: PageHeader { title: gridPage.albumName } + + delegate: ThumbnailImage { + source: thumbnail + size: grid.cellSize + onReleased: { + pageStack.push(Qt.resolvedUrl("FullscreenPhotoPage.qml"), { + accessTokensProvider: accessTokensProvider, + currentIndex: index, + model: grid.model + }) + } + } + } + + // Requesting the accessToken for the first picture ASAP will work + // nicely for the most common use case in which we are checking + // the pictures from just one Facebook user. + Component.onCompleted: if (model.count > 0) accessTokensProvider.requestAccessToken(model.getField(0, FacebookImageCacheModel.AccountId)) +} diff --git a/usr/lib/qt5/qml/com/jolla/gallery/facebook/UsersPage.qml b/usr/lib/qt5/qml/com/jolla/gallery/facebook/UsersPage.qml new file mode 100644 index 00000000..d79061ec --- /dev/null +++ b/usr/lib/qt5/qml/com/jolla/gallery/facebook/UsersPage.qml @@ -0,0 +1,100 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import com.jolla.gallery 1.0 +import org.nemomobile.socialcache 1.0 +import com.jolla.gallery.extensions 1.0 + +Page { + id: root + allowedOrientations: window.allowedOrientations + + SilicaListView { + anchors.fill: parent + header: PageHeader {} + model: FacebookImageCacheModel { + id: fbUsers + Component.onCompleted: refresh() + type: FacebookImageCacheModel.Users + onCountChanged: { + if (count === 0) { + // no users left, return to gallery main level + pageStack.pop(null) + } + } + } + + delegate: BackgroundItem { + id: delegateItem + property string userId: model.facebookId + anchors { + left: parent.left + right: parent.right + } + height: thumbnail.height + + Label { + elide: Text.ElideRight + font.pixelSize: Theme.fontSizeLarge + text: model.title + color: delegateItem.down ? Theme.highlightColor : Theme.primaryColor + anchors { + right: thumbnail.left + rightMargin: Theme.paddingLarge + verticalCenter: parent.verticalCenter + } + } + + SlideshowIcon { + id: thumbnail + anchors.left: parent.horizontalCenter + opacity: delegateItem.down ? 0.5 : 1 + highlighted: delegateItem.highlighted + serviceIcon: "image://theme/graphic-service-facebook" + model: FacebookImageCacheModel { + Component.onCompleted: refresh() + type: FacebookImageCacheModel.Images + nodeIdentifier: delegateItem.userId == "" ? "" : "user-" + delegateItem.userId + downloader: FacebookImageDownloader + } + } + + Label { + anchors { + right: parent.right + leftMargin: Theme.horizontalPageMargin + left: thumbnail.right + verticalCenter: parent.verticalCenter + } + text: model.dataCount + color: Theme.highlightColor + font.pixelSize: Theme.fontSizeLarge + } + + onClicked: { + window.pageStack.animatorPush(Qt.resolvedUrl("AlbumsPage.qml"), + { "userId": delegateItem.userId }) + } + } + + SyncHelper { + socialNetwork: SocialSync.Facebook + dataType: SocialSync.Images + onLoadingChanged: { + if (!loading) { + fbUsers.refresh() + } + } + onProfileDeleted: { + if (window.pageStack.currentPage === root) { + var page = pageStack.currentPage + var prevPage = pageStack.previousPage(page) + while (prevPage) { + page = prevPage + prevPage = pageStack.previousPage(prevPage) + } + pageStack.pop(page) + } + } + } + } +} diff --git a/usr/lib/qt5/qml/com/jolla/gallery/facebook/qmldir b/usr/lib/qt5/qml/com/jolla/gallery/facebook/qmldir new file mode 100644 index 00000000..6e294011 --- /dev/null +++ b/usr/lib/qt5/qml/com/jolla/gallery/facebook/qmldir @@ -0,0 +1,10 @@ +module com.jolla.gallery.facebook +plugin jollagalleryfacebookplugin +AccessTokensProvider 1.0 AccessTokensProvider.qml +AddCommentPage 1.0 AddCommentPage.qml +AlbumDelegate 1.0 AlbumDelegate.qml +AlbumsPage 1.0 AlbumsPage.qml +FacebookGalleryIcon 1.0 FacebookGalleryIcon.qml +FullscreenPhotoPage 1.0 FullscreenPhotoPage.qml +PhotoGridPage 1.0 PhotoGridPage.qml +UsersPage 1.0 UsersPage.qml diff --git a/usr/lib/qt5/qml/com/jolla/gallery/vk/VKAlbumsPage.qml b/usr/lib/qt5/qml/com/jolla/gallery/vk/VKAlbumsPage.qml deleted file mode 100644 index f1692cd0..00000000 --- a/usr/lib/qt5/qml/com/jolla/gallery/vk/VKAlbumsPage.qml +++ /dev/null @@ -1,67 +0,0 @@ -import QtQuick 2.0 -import Sailfish.Silica 1.0 -import org.nemomobile.socialcache 1.0 -import com.jolla.gallery.extensions 1.0 - -AlbumsPage { - id: root - - property int accountId - - accessTokenService: "vk-sync" - clientId: keyProviderHelper.vkClientId - syncService: "vk-images" - socialNetwork: SocialSync.VK - albumDelegate: AlbumDelegate { - id: albumDelegate - albumName: model.text - albumIdentifier: model.albumId - userIdentifier: model.userId - property int accountIdentifier: model.accountId - serviceIcon: "image://theme/graphic-service-vk" - imagesModel: VKImageCacheModel { - function nodeIdentifierValue() { - return imagesModel.constructNodeIdentifier(albumDelegate.accountIdentifier, albumDelegate.userIdentifier, albumDelegate.albumIdentifier, "") - } - - Component.onCompleted: refresh() - type: VKImageCacheModel.Images - nodeIdentifier: nodeIdentifierValue() - downloader: VKImageDownloader - } - - Component { - id: photoGridComponent - PhotoGridPage { - onImageClicked: { - pageStack.push(Qt.resolvedUrl("VKFullscreenPhotoPage.qml"), { - "currentIndex": currentIndex, - "model": model, - "downloader": root.fullSizeDownloader, - "connectedToNetwork": Qt.binding(function() { return root.connectedToNetwork }), - "accessTokensProvider": root.accessTokensProvider - }) - } - } - } - - onClicked: { - imagesModel.loadImages() - window.pageStack.animatorPush(photoGridComponent, - { "albumName": albumName, - "albumIdentifier": albumIdentifier, - "userIdentifier": userIdentifier, - "model": imagesModel, - "syncHelper": root.syncHelper }) - } - } - - albumModel: VKImageCacheModel { - id: vkAlbums - type: VKImageCacheModel.Albums - nodeIdentifier: vkAlbums.constructNodeIdentifier(root.accountId, root.userId, "", "") - Component.onCompleted: refresh() - onNodeIdentifierChanged: refresh() - downloader: VKImageDownloader - } -} diff --git a/usr/lib/qt5/qml/com/jolla/gallery/vk/VKFullscreenPhotoPage.qml b/usr/lib/qt5/qml/com/jolla/gallery/vk/VKFullscreenPhotoPage.qml deleted file mode 100644 index f5b0ab21..00000000 --- a/usr/lib/qt5/qml/com/jolla/gallery/vk/VKFullscreenPhotoPage.qml +++ /dev/null @@ -1,35 +0,0 @@ -import QtQuick 2.0 -import Sailfish.Silica 1.0 -import org.nemomobile.socialcache 1.0 -import com.jolla.gallery.extensions 1.0 - -FullscreenPhotoPage { - id: fullscreenPage - - delegate: CloudImage { - imageId: model.photoId - accountId: model.accountId - width: slideshowView.width - height: slideshowView.height - directUrl: model.imageSource - } - - onDeletePhoto: { - var imageId = model.getField(index, VKImageCacheModel.PhotoId) - - var doc = new XMLHttpRequest() - doc.onreadystatechange = function() { - if (doc.readyState === XMLHttpRequest.DONE) { - if (doc.status == 200) { - model.removeImage(imageId) - } else { - console.warn("Failed to delete VK image") - } - } - } - - var url = "https://api.vk.com/method/photos.delete?photo_id="+ imageId + "&access_token=" + accessToken - doc.open("POST", url) - doc.send() - } -} diff --git a/usr/lib/qt5/qml/com/jolla/gallery/vk/VKGalleryIcon.qml b/usr/lib/qt5/qml/com/jolla/gallery/vk/VKGalleryIcon.qml deleted file mode 100644 index 85684833..00000000 --- a/usr/lib/qt5/qml/com/jolla/gallery/vk/VKGalleryIcon.qml +++ /dev/null @@ -1,11 +0,0 @@ -import QtQuick 2.0 -import Sailfish.Silica 1.0 -import com.jolla.gallery 1.0 -import org.nemomobile.socialcache 1.0 -import com.jolla.gallery.extensions 1.0 - -GalleryIcon { - socialNetwork: SocialSync.VK - dataType: SocialSync.Images - serviceIcon: "image://theme/graphic-service-vk" -} diff --git a/usr/lib/qt5/qml/com/jolla/gallery/vk/VKUsersPage.qml b/usr/lib/qt5/qml/com/jolla/gallery/vk/VKUsersPage.qml deleted file mode 100644 index aee2a390..00000000 --- a/usr/lib/qt5/qml/com/jolla/gallery/vk/VKUsersPage.qml +++ /dev/null @@ -1,41 +0,0 @@ -import QtQuick 2.0 -import Sailfish.Silica 1.0 -import com.jolla.gallery 1.0 -import org.nemomobile.socialcache 1.0 -import com.jolla.gallery.extensions 1.0 - -UsersPage { - id: root - - socialNetwork: SocialSync.VK - dataType: SocialSync.Images - usersModel: VKImageCacheModel { - Component.onCompleted: refresh() - type: VKImageCacheModel.Users - onCountChanged: { - if (count === 0) { - // no users left, return to gallery main level - pageStack.pop(null) - } - } - } - userDelegate: UserDelegate { - id: delegateItem - property int accountId: model.accountId - userId: model.userId - title: model.text - serviceIcon: "image://theme/graphic-service-vk" - slideshowModel: VKImageCacheModel { - Component.onCompleted: refresh() - type: VKImageCacheModel.Images - nodeIdentifier: constructNodeIdentifier(delegateItem.accountId, delegateItem.userId, "", "") - downloader: VKImageDownloader - } - onClicked: { - window.pageStack.animatorPush(Qt.resolvedUrl("VKAlbumsPage.qml"), - { "userId": delegateItem.userId, - "accountId": delegateItem.accountId, - "title": root.title}) - } - } -} diff --git a/usr/lib/qt5/qml/com/jolla/gallery/vk/qmldir b/usr/lib/qt5/qml/com/jolla/gallery/vk/qmldir deleted file mode 100644 index 3aff0170..00000000 --- a/usr/lib/qt5/qml/com/jolla/gallery/vk/qmldir +++ /dev/null @@ -1,6 +0,0 @@ -module com.jolla.gallery.vk -plugin jollagalleryvkplugin -VKGalleryIcon 1.0 VKGalleryIcon.qml -VKUsersPage 1.0 VKUsersPage.qml -VKAlbumsPage 1.0 VKAlbumsPage.qml -VKFullscreenPhotoPage 1.0 VKFullscreenPhotoPage.qml diff --git a/usr/lib/qt5/qml/com/jolla/hwr/qmldir b/usr/lib/qt5/qml/com/jolla/hwr/qmldir new file mode 100644 index 00000000..d5850339 --- /dev/null +++ b/usr/lib/qt5/qml/com/jolla/hwr/qmldir @@ -0,0 +1,2 @@ +module com.jolla.hwr +plugin jollahwrplugin diff --git a/usr/lib/qt5/qml/com/jolla/mediaplayer/AddToPlaylistPage.qml b/usr/lib/qt5/qml/com/jolla/mediaplayer/AddToPlaylistPage.qml new file mode 100644 index 00000000..5cdca850 --- /dev/null +++ b/usr/lib/qt5/qml/com/jolla/mediaplayer/AddToPlaylistPage.qml @@ -0,0 +1,97 @@ +// -*- qml -*- + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Sailfish.Media 1.0 +import com.jolla.mediaplayer 1.0 + +Page { + id: page + + property var media + + MediaPlayerListView { + id: view + + model: GriloTrackerModel { + id: playlistModel + query: PlaylistTrackerHelpers.getPlaylistsQuery(playlistsHeader.searchText, + {"location": playlistsLocation, + "editablePlaylistsOnly": true}) + } + + Connections { + target: playlists + onUpdated: playlistModel.refresh() + } + + PullDownMenu { + + MenuItem { + //: Menu label for adding a new playlist + //% "New playlist" + text: qsTrId("mediaplayer-me-new-playlist") + onClicked: pageStack.animatorPush("com.jolla.mediaplayer.NewPlaylistDialog", {media: page.media, pageToPop: pageStack.previousPage()}) + } + + MenuItem { + //: Search menu entry + //% "Search" + text: qsTrId("mediaplayer-me-search") + onClicked: playlistsHeader.enableSearch() + enabled: view.count > 0 || playlistsHeader.searchText !== '' + } + } + + header: SearchPageHeader { + id: playlistsHeader + + width: parent.width + + //: page header for the Playlists page + //% "Add to" + title: qsTrId("mediaplayer-he-add-to-playlist") + + //: Playlists search field placeholder text + //% "Search playlist" + placeholderText: qsTrId("mediaplayer-tf-playlists-search") + } + + delegate: MediaContainerPlaylistDelegate { + formatFilter: playlistsHeader.searchText + title: media.title + songCount: media.childCount + color: model.title != "" ? PlaylistColors.nameToColor(model.title) + : "transparent" + highlightColor: model.title != "" ? PlaylistColors.nameToHighlightColor(model.title) + : "transparent" + onClicked: { + // TODO: Notify user? + if (playlists.appendToPlaylist(media, page.media)) { + pageStack.pop() + } + } + } + + ViewPlaceholder { + text: { + if (playlistsHeader.searchText !== '') { + //: Placeholder text for an empty search view + //% "No items found" + return qsTrId("mediaplayer-la-empty-search") + } else { + //: Placeholder text for an empty playlists view + //% "Create a playlist" + return qsTrId("mediaplayer-la-create-a-playlist") + } + } + enabled: view.count === 0 && !busyIndicator.running + } + + PageBusyIndicator { + id: busyIndicator + + running: playlistModel.fetching + } + } +} diff --git a/usr/lib/qt5/qml/com/jolla/mediaplayer/AlbumArt.qml b/usr/lib/qt5/qml/com/jolla/mediaplayer/AlbumArt.qml new file mode 100644 index 00000000..f83e0dca --- /dev/null +++ b/usr/lib/qt5/qml/com/jolla/mediaplayer/AlbumArt.qml @@ -0,0 +1,35 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Jolla Ltd. +** Contact: Raine Mäkeläinen +** +****************************************************************************/ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 + +Image { + id: albumArt + + property bool highlighted + + height: Theme.itemSizeExtraLarge + width: Theme.itemSizeExtraLarge + sourceSize.width: Theme.itemSizeExtraLarge + sourceSize.height: Theme.itemSizeExtraLarge + + Rectangle { + anchors.fill: parent + visible: albumArt.source == "" + gradient: Gradient { + GradientStop { position: 0.0; color: Theme.rgba(Theme.primaryColor, Theme.opacityFaint) } + GradientStop { position: 1.0; color: Theme.rgba(Theme.primaryColor, 0.05) } + } + + Image { + source: "image://theme/icon-m-media-albums" + (albumArt.highlighted ? ("?" + Theme.highlightColor) + : "") + anchors.centerIn: parent + } + } +} diff --git a/usr/lib/qt5/qml/com/jolla/mediaplayer/AudioPlayer.qml b/usr/lib/qt5/qml/com/jolla/mediaplayer/AudioPlayer.qml new file mode 100644 index 00000000..b1f6252f --- /dev/null +++ b/usr/lib/qt5/qml/com/jolla/mediaplayer/AudioPlayer.qml @@ -0,0 +1,573 @@ +// -*- qml -*- + +pragma Singleton +import QtQuick 2.0 +import com.jolla.mediaplayer 1.0 +import Amber.Mpris 1.0 +import Nemo.Notifications 1.0 + +Container { + id: player + + property bool shuffle + property bool repeat + property bool repeatOne + property bool rewinding + property bool forwarding + property bool playerVisible + + property bool _resume + property int _seekOffset + property bool _seekRepeat + property var _metadata: ({}) + property AlbumArtProvider albumArtProvider + + property ProxyMprisPlayer mprisPlayerOverride + property ProxyMprisPlayer _mprisPlayer: mprisPlayerOverride != null + ? mprisPlayerOverride + : mprisPlayerDefault + + readonly property alias currentItem: audio.currentItem + + readonly property alias metadata: player._metadata + readonly property alias duration: audio.duration + readonly property alias state: audio.playbackState + readonly property alias playModel: audio.model + readonly property bool active: audio.model.count > 0 + readonly property int position: audio.position + _seekOffset + readonly property bool playing: audio.playbackState == Audio.Playing || _resume + + readonly property bool _seeking: player.rewinding || player.forwarding + + signal tryingToPlay + + onRepeatChanged: if (!repeat) repeatOne = false + + function setPosition(position) { + audio.position = position + mprisPlayerDefault.emitSeeked() + } + + function setSeekRepeat(repeat) { + _seekRepeat = repeat + } + + function seekForward(time) { + _seekOffset += time + if (position > duration) { + _seekOffset = duration - audio.position + } + mprisPlayerDefault.emitSeeked() + } + + function seekBackward(time) { + _seekOffset -= time + if (position < 0) { + _seekOffset = -audio.position + } + mprisPlayerDefault.emitSeeked() + } + + function playIndex(index) { + if (!playModel) { + return + } + + audio.model.currentIndex = index + _play() + } + + function play(model, index) { + if (model !== undefined && index !== undefined) { + audio.setPlayModel(model) + audio.model.currentIndex = playModel.shuffledIndex(index) + } + + _play() + } + + function shuffleAndPlay(model, modelSize) { + audio.setPlayModel(model) + + audio.model.currentIndex = Math.floor(Math.random() * modelSize) + playModel.shuffle() + _play() + } + + function addToQueue(mediaOrModel) { + audio.addToQueue(mediaOrModel) + } + + function removeFromQueue(index) { + if (index >= playModel.count || index < 0) { + console.warn("Invalid index passed to removeFromQueue()") + return + } + + // If it's the current item then we try to play the next one: + if (index == playModel.currentIndex) { + if (repeat || index !== playModel.count - 1) { + audio.playNext() + } else { + stop() + audio.model.currentIndex = 0 + } + + if (state != Audio.Playing) { + stop() + } + } + + // If it's still the currentIndex then we just stop playback. + if (index == playModel.currentIndex) { + audio.model.currentIndex = -1 + } + + audio.removeFromQueue(index) + } + + function removeItemFromQueue(mediaItem) + { + for (var i = audio.indexOf(mediaItem, 0); i != -1; i = audio.indexOf(mediaItem, i)) { + removeFromQueue(i) + } + } + + function playUrl(url) { + if (!File.isLocalFile(url) || File.exists(url)) { + playModel.clear() + playModel.appendUrl(url) + playIndex(0) + } else { + //% "Unable to open: %1" + errorNotification.previewBody = qsTrId("mediaplayer-la-unable_to_open").arg(File.fileName(url)) + errorNotification.publish() + } + } + + function playPause() { + if (playing) { + pause() + } else { + _play() + } + } + + function _play() { + if (_seeking) { + _resume = true + } else if (audio.isEndOfMedia()) { + audio.playNext() + } else { + audio.play() + } + tryingToPlay() + } + + function pause() { + _resume = false + audio.pause() + } + + function stop() { + audio.stop() + } + + function playPrevious(warn) { + audio.playPrevious() + if (warn) { + tryingToPlay() + } + } + + function playNext(warn) { + audio.playNext() + if (warn) { + tryingToPlay() + } + } + + function remove(itemMedia, listItem, playlists) { + listItem.remorseDelete(function() { + // Remove item from the playqueue + removeItemFromQueue(itemMedia) + + if (File.removeFile(itemMedia.url)) { + // Remove the item from the playlists + playlists.removeItem(itemMedia.url) + } + }) + } + + on_SeekingChanged: { + if (_seeking) { + _resume = state == Audio.Playing + audio.pause() + } else { + audio.position += _seekOffset + _seekOffset = 0 + if (_resume) { + _resume = false + audio.play() + } + } + } + + onShuffleChanged: if (audio.model.shuffled != shuffle) audio.model.shuffled = !audio.model.shuffled + + onRewindingChanged: { + if (rewinding) { + if (_seekRepeat) { + _seekRepeat = false + seekBackward(1000) + previousTimer.stop() + } else { + seekBackward(5000) + + // Wired headsets can overload the fast forward key to mean next if held, but + // bluetooth headsets will manage this themselves, and will auto repeat the key if held. + // To support the wired headset we restart a timer on each key press and cancel it on + // release, triggering the next song action on the timer expiring. If the key auto + // repeats the restart will prevent the timer expiring and holding will act as a + // series of successive presses. + previousTimer.restart() + } + } else { + _seekRepeat = false + previousTimer.stop() + } + } + + onForwardingChanged: { + if (forwarding) { + if (_seekRepeat) { + _seekRepeat = false + seekForward(1000) + nextTimer.stop() + } else { + seekForward(5000) + nextTimer.restart() + } + } else { + _seekRepeat = false + nextTimer.stop() + } + } + + Timer { id: nextTimer; interval: 500; onTriggered: audio.playNext() } + Timer { id: previousTimer; interval: 500; onTriggered: audio.playPrevious() } + + Notification { + id: errorNotification + isTransient: true + urgency: Notification.Critical + icon: "icon-system-warning" + } + + Audio { + id: audio + + property int playbackState + property bool changingItem + + onEndOfMedia: { + if (repeatOne) { + audio.playCurrent() + } else if (repeat || model.currentIndex + 1 < model.count) { + audio.playNext() + } else { + stop() + playbackState = Audio.Stopped + } + } + onErrorChanged: { + if (error === Audio.FormatError) { + //: %1 is replaced with specific codec + //% "Unsupported codec: %1" + errorNotification.previewBody = qsTrId("mediaplayer-la-unsupported-codec").arg(errorString) + errorNotification.publish() + } + } + model.onShuffledChanged: if (player.shuffle != model.shuffled) player.shuffle = !player.shuffle + + onCurrentItemChanged: { + player._seekOffset = 0 + + var metadata = {} + if (currentItem) { + metadata = { + 'trackId' : audio.currentTrackId, + 'url' : audio.currentItem.url, + 'title' : audio.currentItem.title, + 'artist' : audio.currentItem.author, + 'album' : audio.currentItem.album, + 'genre' : "", + 'track' : audio.model.currentIndex, + 'trackCount': audio.model.count, + 'duration' : audio.currentItem.duration + } + } + + player._metadata = metadata + } + + onStateChanged: { + if (playbackState == state) return + + // We don't want the transition to stop state when + // choosing to play the next or previous song, or when the + // current song has finished and it will transit + // automatically to the next one. + if (Audio.Stopped == state && (changingItem || isEndOfMedia())) return + + playbackState = state + } + + onPlaybackStateChanged: { + if (playbackState == Audio.Playing && !player._resume) { + player.tryingToPlay() + } else if (playbackState == Audio.Stopped) { + player._resume = false + } + } + + function playCurrent() { + changingItem = true + play() + changingItem = false + playbackState = state + } + + function playNext() { + changingItem = true + model.currentIndex = model.currentIndex < model.count - 1 + ? model.currentIndex + 1 + : 0 + + play() + changingItem = false + playbackState = state + } + + function playPrevious() { + // We play previous if less than 5 seconds have elapsed. + // otherwise we rewind the playing song + if (playModel.count === 1 || audio.position >= 5000) { + player.setPosition(0) + return + } + + changingItem = true + model.currentIndex = model.currentIndex >= 1 + ? model.currentIndex - 1 + : model.count - 1 + play() + changingItem = false + playbackState = state + } + } + + BluetoothMediaPlayer { + id: bluetoothMediaPlayer + + status: { + if (audio.playbackState == Audio.Playing) { + return BluetoothMediaPlayer.Playing + } else if (audio.playbackState == Audio.Stopped) { + return BluetoothMediaPlayer.Stopped + } else if (player.rewinding) { + return BluetoothMediaPlayer.ReverseSeek + } else if (player.forwarding) { + return BluetoothMediaPlayer.ForwardSeek + } else { + return BluetoothMediaPlayer.Paused + } + } + + repeat: player.repeat + ? BluetoothMediaPlayer.RepeatAllTracks + : BluetoothMediaPlayer.RepeatOff + + shuffle: player.shuffle + ? BluetoothMediaPlayer.ShuffleAllTracks + : BluetoothMediaPlayer.ShuffleOff + + position: audio.position + + metadata: player.metadata ? player.metadata : {} + + onChangeRepeat: { + if (repeat == BluetoothMediaPlayer.RepeatOff) { + player.repeat = false + } else if (repeat == BluetoothMediaPlayer.RepeatAllTracks) { + player.repeat = true + } + } + + onChangeShuffle: { + if (shuffle == BluetoothMediaPlayer.ShuffleOff) { + player.shuffle = false + } else if (shuffle == BluetoothMediaPlayer.ShuffleAllTracks) { + player.shuffle = true + } + } + + onNextRequested: audio.playNext() + onPreviousRequested: audio.playPrevious() + onPlayRequested: player._play() + onPauseRequested: player.pause() + onSeekRequested: { + var position = audio.position + offset + + if (offset > 0) { + position = (Math.ceil(position / 1000) + 1) * 1000 + } else if (offset < 0) { + position = (Math.floor(position / 1000) - 1) * 1000 + } + + player.setPosition(Math.max(0, position)) + } + + } + + ProxyMprisPlayer { + id: mprisPlayerDefault + + metaData { + trackId: audio.currentTrackId + url: audio.currentItem ? audio.currentItem.url : null + title: audio.currentItem ? audio.currentItem.title : null + contributingArtist: audio.currentItem ? audio.currentItem.author : null + albumTitle: audio.currentItem ? audio.currentItem.album : null + duration: audio.currentItem ? audio.currentItem.duration: null + artUrl: (audio.currentItem && albumArtProvider && (albumArtProvider.extracting || true) + ? albumArtProvider.albumArt(audio.currentItem.album, audio.currentItem.author) + : null) + } + + property var localMetadata: playerVisible ? player.metadata : null + + function emitSeeked() { + mprisPlayer.seeked(audio.position) + } + + // Mpris2 Player Interface + canControl: true + + canGoNext: { + if (!active || !playerVisible) return false + if ((audio.model.currentIndex + 1 >= audio.model.count) && (loopStatus != Mpris.LoopPlaylist)) return false + return true + } + canGoPrevious: { + if (!active || !playerVisible) return false + + // Always possible to go to the beginning of the song + // This is NOT how Mpris should behave but ... oh, well ... + if (position >= 5000000) return true + + if (audio.model.currentIndex < 1) return false + return true + } + // Do we have an item URL in the metadata? + canPause: localMetadata ? 'url' in localMetadata : false + canPlay: localMetadata ? 'url' in localMetadata : false + canSeek: localMetadata ? 'url' in localMetadata : false + + loopStatus: { + if (player.repeatOne) { + return Mpris.LoopTrack + } else if (player.repeat) { + return Mpris.LoopPlaylist + } else { + return Mpris.LoopNone + } + } + playbackStatus: { + if (audio.playbackState == Audio.Playing) { + return Mpris.Playing + } else if (audio.playbackState == Audio.Stopped) { + return Mpris.Stopped + } else { + return Mpris.Paused + } + } + shuffle: player.shuffle + volume: 1 + + onPositionRequested: position = audio.position + onPauseRequested: player.pause() + onPlayRequested: player._play() + onPlayPauseRequested: player.playPause() + onStopRequested: audio.stop() + + // This will start playback in any case. Mpris says to keep + // paused/stopped if we were before but I suppose this is just + // our general behavior decision here. + onNextRequested: audio.playNext() + onPreviousRequested: audio.playPrevious() + + onSeekRequested: { + var position = audio.position + offset + player.setPosition(position < 0 ? 0 : position) + } + onSetPositionRequested: player.setPosition(position) + onOpenUriRequested: playUrl(uri) + + onLoopStatusRequested: { + if (loopStatus == Mpris.LoopNone) { + player.repeat = false + } else if (loopStatus == Mpris.LoopPlaylist) { + player.repeat = true + player.repeatOne = false + } else if (loopStatus == Mpris.LoopTrack) { + player.repeat = true + player.repeatOne = true + } + } + onShuffleRequested: player.shuffle = shuffle + } + + MprisPlayer { + id: mprisPlayer + + serviceName: "jolla-mediaplayer" + + // Mpris2 Root Interface + identity: qsTrId("mediaplayer-ap-name") + desktopEntry: "jolla-mediaplayer" + supportedUriSchemes: ["file", "http", "https"] + supportedMimeTypes: ["audio/x-wav", "audio/mp4", "audio/mpeg", "audio/x-vorbis+ogg"] + + metaData.fillFrom: _mprisPlayer.metaData + + // Mpris2 Player Interface + canControl: _mprisPlayer.canControl + canGoNext: _mprisPlayer.canGoNext + canGoPrevious: _mprisPlayer.canGoPrevious + canPause: _mprisPlayer.canPause + canPlay: _mprisPlayer.canPlay + canSeek: _mprisPlayer.canSeek + loopStatus: _mprisPlayer.loopStatus + maximumRate: _mprisPlayer.maximumRate + minimumRate: _mprisPlayer.minimumRate + playbackStatus: _mprisPlayer.playbackStatus + position: _mprisPlayer.position + rate: _mprisPlayer.rate + shuffle: _mprisPlayer.shuffle + volume: _mprisPlayer.volume + + onPositionRequested: _mprisPlayer.positionRequested() + onPauseRequested: _mprisPlayer.pauseRequested() + onPlayRequested: _mprisPlayer.playRequested() + onPlayPauseRequested: _mprisPlayer.playPauseRequested() + onStopRequested: _mprisPlayer.stopRequested() + onNextRequested: _mprisPlayer.nextRequested() + onPreviousRequested: _mprisPlayer.previousRequested() + onSeekRequested: _mprisPlayer.seekRequested() + onSetPositionRequested: _mprisPlayer.setPositionRequested(trackId, position) + onOpenUriRequested: _mprisPlayer.openUriRequested(url) + onLoopStatusRequested: _mprisPlayer.loopStatusRequested(loopStatus) + onShuffleRequested: _mprisPlayer.shuffleRequested(shuffle) + } +} diff --git a/usr/lib/qt5/qml/com/jolla/mediaplayer/AudioTrackerHelpers.js b/usr/lib/qt5/qml/com/jolla/mediaplayer/AudioTrackerHelpers.js new file mode 100644 index 00000000..07cff235 --- /dev/null +++ b/usr/lib/qt5/qml/com/jolla/mediaplayer/AudioTrackerHelpers.js @@ -0,0 +1,199 @@ +.pragma library +.import "RegExpHelpers.js" as RegExpHelpers +.import "TrackerHelpers.js" as TrackerHelpers +.import org.nemomobile.grilo 0.1 as Grilo + +// The columns matching grilo column names, see grl-tracker-source-api.c. +// First is media type as int matching grilo media type enum + +// %1 unknown artist string +// %2 unknown album string +// %3 extra inner rules +// %4 extra outer rules +var songsQuery = "" + + "SELECT " + + Grilo.GriloMedia.TypeAudio + " AS ?type" + + " ?song AS ?id " + + " ?url " + + " ?duration " + + " ?author " + + " ?title " + + " ?album " + + "WHERE { SERVICE {" + + " GRAPH tracker:Audio { " + + " SELECT ?song ?url " + + " nfo:duration(?song) AS ?duration " + + " tracker:coalesce(nmm:artistName(nmm:artist(?song)), \"%1\") AS ?author " + + " tracker:coalesce(nie:title(?song), tracker:string-from-filename(?filename)) AS ?title " + + " tracker:coalesce(nie:title(nmm:musicAlbum(?song)), \"%2\") AS ?album " + + " nmm:setNumber(nmm:musicAlbumDisc(?song)) AS ?setnumber " + + " nmm:trackNumber(?song) AS ?tracknumber " + + " WHERE { " + + " ?song a nmm:MusicPiece ; " + + " nie:isStoredAs ?url . " + + " ?url nfo:fileName ?filename . " + + " ?url nie:dataSource/tracker:available true . " + + " %3 " + + " } " + + " } } " + +function getSongsQuery(aSearchText, opts) { + var unknownArtistText = "unknownArtist" in opts ? TrackerHelpers.escapeSparql(opts["unknownArtist"]) : "Unknown artist" + var unknownAlbumText = "unknownAlbum" in opts ? TrackerHelpers.escapeSparql(opts["unknownAlbum"]) : "Unknown album" + var artistId = "authorId" in opts ? TrackerHelpers.escapeSparql(opts["authorId"]) : "" + var albumId = "albumId" in opts ? TrackerHelpers.escapeSparql(opts["albumId"]) : "" + + var extraRules = "" + if (albumId != "") { + if (albumId == "0") { + // special case for unknown album + extraRules = "FILTER NOT EXISTS { ?song nmm:musicAlbum ?anyAlbum } " + } else { + extraRules = "?song nmm:musicAlbum \"%1\" . ".arg(albumId) + } + } + + if (artistId != "") { + if (artistId == "0") { + // unknown artist + extraRules += "FILTER NOT EXISTS { ?song nmm:artist ?anyArtist } " + } else { + extraRules += "?song nmm:artist \"%1\" . ".arg(artistId) + } + } + + var extraOuterRules = "" + if (aSearchText != "") { + extraOuterRules += TrackerHelpers.getSearchFilter(aSearchText, "?title") + } + + var orderRule = "" + if (albumId != "") { + orderRule = "" + + "ORDER BY " + + " ASC(?setnumber) " + + " ASC(?tracknumber) " + + " ASC(fn:lower-case(?title)) " + } else { + orderRule = "" + + "ORDER BY " + + " ASC(fn:lower-case(?author)) " + + " ASC(fn:lower-case(?album)) " + + " ASC(?setnumber) " + + " ASC(?tracknumber) " + + " ASC(fn:lower-case(?title)) " + } + + return songsQuery.arg(unknownArtistText).arg(unknownAlbumText).arg(extraRules) + extraOuterRules + " } " + orderRule +} + +// We are resolving several times "nmm:performer" and "nmm:musicAlbum" +// as a property functions in the "SELECT" side instead of using the +// "OPTIONAL" keyword in the "WHERE" part. In terms of performance, it +// is better to use property functions than the "OPTIONAL" keyword, as +// explained at: +// https://wiki.gnome.org/Projects/Tracker/Documentation/SparqlTipsTricks#Use_property_functions +// +// We are using this strategy also in other similar queries. + +// We are "overloading" tracker-urn to hold the artists id + +// %1 tracker_urn +// %2 unknown artist name string +// %3 multiple artists text +// %4 unknown album text +// %5 extra inner rules +// %6 extra outer rules +var albumsQuery = "" + + "SELECT " + + Grilo.GriloMedia.TypeContainer + " AS ?type " + + " ?album as ?id " + + " ?title " + + " ?author " + + " ?childcount " + + " \"%1\" AS ?tracker_urn " + + "WHERE { SERVICE {" + + " GRAPH tracker:Audio { " + + " SELECT " + + " tracker:coalesce(nmm:musicAlbum(?song), 0) as ?album " + + " tracker:coalesce(nie:title(nmm:musicAlbum(?song)), \"%4\") AS ?title " + + " IF(COUNT(DISTINCT(tracker:coalesce(nmm:artist(?song), 0))) > 1, " + + " \"%3\", tracker:coalesce(nmm:artistName(nmm:artist(?song)), \"%2\"))" + + " AS ?author " + + " COUNT(DISTINCT(?song)) AS ?childcount " + + " WHERE { " + + " ?song a nmm:MusicPiece ; " + + " nie:isStoredAs ?file . " + + " ?file nie:dataSource/tracker:available true . " + + " %5" + + " } " + + " GROUP BY ?album " + + " } } " + + " %6 " + + "} " + + "ORDER BY " + + " ASC(fn:lower-case(?author)) " + + " ASC(fn:lower-case(?title)) " + +function getAlbumsQuery(aSearchText, opts) { + var artistId = "authorId" in opts ? TrackerHelpers.escapeSparql(opts["authorId"]) : "" + var unknownArtistText = "unknownArtist" in opts ? TrackerHelpers.escapeSparql(opts["unknownArtist"]) : "Unknown artist" + var multipleArtistsText = "multipleArtists" in opts ? TrackerHelpers.escapeSparql(opts["multipleArtists"]) : "Multiple artists" + var unknownAlbumText = "unknownAlbum" in opts ? TrackerHelpers.escapeSparql(opts["unknownAlbum"]) : "Unknown album" + var extraRules = "" + if (artistId != "") { + if (artistId == "0") { + // special case for unknown artist + extraRules += "FILTER NOT EXISTS { ?song nmm:artist ?anyArtist }" + } else { + extraRules += "?song nmm:artist \"%1\" . ".arg(artistId) + } + } + + var extraOuterRules = "" + if (aSearchText != "") { + extraOuterRules = TrackerHelpers.getSearchFilter(aSearchText, "?title") + } + + return albumsQuery.arg(artistId).arg(unknownArtistText).arg(multipleArtistsText).arg(unknownAlbumText).arg(extraRules).arg(extraOuterRules) +} + + +// We are "overloading" childcount to hold the total duration. Just think +// our container as a container of seconds and then all that would start to make sense :P +// %1 = unknown artist text +// %2 = extra filters +var artistsQuery = "" + + "SELECT " + + Grilo.GriloMedia.TypeContainer + " AS ?type " + + " ?artist AS ?id " + + " ?title " + + " ?childcount " + + "WHERE { SERVICE {" + + " GRAPH tracker:Audio { " + + " SELECT " + + " tracker:coalesce(nmm:albumArtist(nmm:musicAlbum(?song)), nmm:artist(?song), 0) AS ?artist " + + " tracker:coalesce(nmm:artistName(nmm:albumArtist(nmm:musicAlbum(?song))), nmm:artistName(nmm:artist(?song))) AS ?artistName " + + " tracker:coalesce(nmm:artistName(nmm:albumArtist(nmm:musicAlbum(?song))), nmm:artistName(nmm:artist(?song)), \"%1\") AS ?title" + + " SUM(nfo:duration(?song)) AS ?childcount " + + " WHERE { " + + " ?song a nmm:MusicPiece ; " + + " nie:isStoredAs ?file . " + + " ?file nie:dataSource/tracker:available true . " + + " } " + + " GROUP BY ?artist " + + " } } " + + " %2 " + + "} " + + "ORDER BY ASC(fn:lower-case(?title))" + +function getArtistsQuery(aSearchText, opts) { + var unknownArtistText = "unknownArtist" in opts ? TrackerHelpers.escapeSparql(opts["unknownArtist"]) : "Unknown artist" + var extraRules = "" + + if (aSearchText != "") { + extraRules = TrackerHelpers.getSearchFilter(aSearchText, "?artistName") + } + + return artistsQuery.arg(unknownArtistText).arg(extraRules) +} diff --git a/usr/lib/qt5/qml/com/jolla/mediaplayer/Container.qml b/usr/lib/qt5/qml/com/jolla/mediaplayer/Container.qml new file mode 100644 index 00000000..59948983 --- /dev/null +++ b/usr/lib/qt5/qml/com/jolla/mediaplayer/Container.qml @@ -0,0 +1,14 @@ +// -*- qml -*- + +import QtQuick 2.0 + +QtObject { + id: container + + // This is a non visual QML object intended just to make it easier + // the creation of other non visual objects by adding inline + // children since the basic QtObject doesn't allow that. + + property list _data + default property alias data: container._data +} diff --git a/usr/lib/qt5/qml/com/jolla/mediaplayer/CoverArt.qml b/usr/lib/qt5/qml/com/jolla/mediaplayer/CoverArt.qml new file mode 100644 index 00000000..54cf0376 --- /dev/null +++ b/usr/lib/qt5/qml/com/jolla/mediaplayer/CoverArt.qml @@ -0,0 +1,27 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 + +Item { + property alias status: coverImage.status + property alias source: coverImage.source + + anchors.fill: parent + + Image { + id: coverImage + + asynchronous: true + anchors.fill: parent + sourceSize.width: width + sourceSize.height: width + fillMode: Image.PreserveAspectFit + + Rectangle { + anchors.fill: parent + gradient: Gradient { + GradientStop { position: 0.0; color: Qt.rgba(0, 0, 0, Theme.opacityHigh) } + GradientStop { position: 0.3; color: "transparent" } + } + } + } +} diff --git a/usr/lib/qt5/qml/com/jolla/mediaplayer/GriloTrackerModel.qml b/usr/lib/qt5/qml/com/jolla/mediaplayer/GriloTrackerModel.qml new file mode 100644 index 00000000..ddca3a2a --- /dev/null +++ b/usr/lib/qt5/qml/com/jolla/mediaplayer/GriloTrackerModel.qml @@ -0,0 +1,62 @@ +// -*- qml -*- + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import org.nemomobile.grilo 0.1 + +GriloModel { + id: griloModel + + property string pluginId: "grl-tracker3" + property alias query: querySource.query + property alias fetching: querySource.fetching + + signal finished() + + onPluginIdChanged: griloRegistry.safeLoadPluginById() + + function refresh() { + querySource.safeRefresh() + } + + source: GriloQuery { + id: querySource + + property bool canRefresh: applicationActive || cover.status != Cover.Inactive + property bool shouldRefresh: true + property Timer delayedRefresh: Timer { + interval: 3000 + onTriggered: querySource.safeRefresh() + } + + source: "grl-tracker3-source" + registry: GriloRegistry { + id: griloRegistry + + function safeLoadPluginById() { + if (griloModel.pluginId != "") loadPluginById(griloModel.pluginId) + } + + Component.onCompleted: safeLoadPluginById() + } + + function safeRefresh() { + if (!canRefresh) { + shouldRefresh = true + return + } + + shouldRefresh = false + + if (query && query != "" && available) { + refresh() + } + } + + onQueryChanged: safeRefresh() + onAvailableChanged: safeRefresh() + onContentUpdated: delayedRefresh.restart() + onCanRefreshChanged: if (canRefresh && shouldRefresh) safeRefresh() + Component.onCompleted: finished.connect(griloModel.finished) + } +} diff --git a/usr/lib/qt5/qml/com/jolla/mediaplayer/MediaContainerIconDelegate.qml b/usr/lib/qt5/qml/com/jolla/mediaplayer/MediaContainerIconDelegate.qml new file mode 100644 index 00000000..259609de --- /dev/null +++ b/usr/lib/qt5/qml/com/jolla/mediaplayer/MediaContainerIconDelegate.qml @@ -0,0 +1,19 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 + +MediaContainerListDelegate { + id: root + + property string iconSource + property alias iconSourceSize: mediaContainerIcon.sourceSize + + leftPadding: Theme.itemSizeExtraLarge + Theme.paddingLarge + + Image { + id: mediaContainerIcon + x: Theme.itemSizeExtraLarge - width + source: root.iconSource + (root.highlighted && root.iconSource !== "" ? ("?" + Theme.highlightColor) + : "") + anchors.verticalCenter: parent.verticalCenter + } +} diff --git a/usr/lib/qt5/qml/com/jolla/mediaplayer/MediaContainerListDelegate.qml b/usr/lib/qt5/qml/com/jolla/mediaplayer/MediaContainerListDelegate.qml new file mode 100644 index 00000000..7345f40b --- /dev/null +++ b/usr/lib/qt5/qml/com/jolla/mediaplayer/MediaContainerListDelegate.qml @@ -0,0 +1,51 @@ +// -*- qml -*- + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import com.jolla.mediaplayer 1.0 + +ListItem { + id: item + + property string title + property alias titleFont: titleLabel.font + property alias subtitle: subtitleLabel.text + property alias subtitleFont: subtitleLabel.font + property var formatFilter + property real leftPadding: Theme.horizontalPageMargin + + contentHeight: Math.max(Theme.itemSizeMedium, column.height + 2 * Theme.paddingMedium) + + Column { + id: column + + anchors { + left: parent.left + leftMargin: item.leftPadding + right: parent.right + rightMargin: Theme.horizontalPageMargin + verticalCenter: parent.verticalCenter + } + + Label { + id: titleLabel + width: parent.width + font.family: Theme.fontFamilyHeading + font.pixelSize: Theme.fontSizeMedium + text: Theme.highlightText(item.title, RegExpHelpers.regExpFromSearchString(formatFilter, false), Theme.highlightColor) + textFormat: Text.StyledText + color: highlighted ? Theme.highlightColor : Theme.primaryColor + truncationMode: TruncationMode.Fade + } + + Label { + id: subtitleLabel + visible: text != "" + width: parent.width + font.family: Theme.fontFamilyHeading + font.pixelSize: Theme.fontSizeExtraSmall + color: highlighted ? Theme.secondaryHighlightColor : Theme.secondaryColor + truncationMode: TruncationMode.Fade + } + } +} diff --git a/usr/lib/qt5/qml/com/jolla/mediaplayer/MediaContainerPlaylistDelegate.qml b/usr/lib/qt5/qml/com/jolla/mediaplayer/MediaContainerPlaylistDelegate.qml new file mode 100644 index 00000000..a8894fd3 --- /dev/null +++ b/usr/lib/qt5/qml/com/jolla/mediaplayer/MediaContainerPlaylistDelegate.qml @@ -0,0 +1,30 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 + +MediaContainerListDelegate { + id: root + + property color color: Theme.primaryColor + property color highlightColor: Theme.highlightColor + property int songCount + + leftPadding: Theme.itemSizeExtraLarge + Theme.paddingLarge + + //: This is for the playlists page. Shows the number of songs in a playlist. + //% "%n songs" + subtitle: qsTrId("mediaplayer-le-number-of-songs", songCount) + + Rectangle { + width: Theme.iconSizeMedium + height: Theme.iconSizeMedium + anchors.verticalCenter: parent.verticalCenter + x: Theme.itemSizeExtraLarge - Theme.paddingSmall - width + radius: Theme.paddingSmall / 2 + color: root.highlighted ? root.highlightColor : root.color + + Image { + source: "image://theme/graphic-media-playlist-medium" + anchors.centerIn: parent + } + } +} diff --git a/usr/lib/qt5/qml/com/jolla/mediaplayer/MediaListDelegate.qml b/usr/lib/qt5/qml/com/jolla/mediaplayer/MediaListDelegate.qml new file mode 100644 index 00000000..3f98d55c --- /dev/null +++ b/usr/lib/qt5/qml/com/jolla/mediaplayer/MediaListDelegate.qml @@ -0,0 +1,19 @@ +// -*- qml -*- + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Sailfish.Media 1.0 +import com.jolla.mediaplayer 1.0 + +MediaListItem { + property var formatFilter + + highlighted: down || menuOpen + playing: media.url == visualAudioAppModel.metadata.url + duration: media.duration + title: Theme.highlightText(media.title, RegExpHelpers.regExpFromSearchString(formatFilter, false), Theme.highlightColor) + textFormat: Text.StyledText + subtitleTextFormat: Text.AutoText + subtitle: media.author + onPlayingChanged: if (playing) ListView.view.currentIndex = model.index +} diff --git a/usr/lib/qt5/qml/com/jolla/mediaplayer/MediaPlayerDockedPanel.qml b/usr/lib/qt5/qml/com/jolla/mediaplayer/MediaPlayerDockedPanel.qml new file mode 100644 index 00000000..045762b9 --- /dev/null +++ b/usr/lib/qt5/qml/com/jolla/mediaplayer/MediaPlayerDockedPanel.qml @@ -0,0 +1,75 @@ +// -*- qml -*- + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Sailfish.Media 1.0 +import com.jolla.mediaplayer 1.0 + +MediaPlayerControlsPanel { + id: panel + + property int state + property Page addToPlaylistPage + property alias author: authorLabel.text + property alias title: titleLabel.text + + visible: root.applicationActive + playing: state == Audio.Playing + repeat: AudioPlayer.repeat ? (AudioPlayer.repeatOne ? MediaPlayerControls.RepeatTrack : MediaPlayerControls.RepeatPlaylist) + : MediaPlayerControls.NoRepeat + shuffle: AudioPlayer.shuffle ? MediaPlayerControls.ShuffleTracks + : MediaPlayerControls.NoShuffle + showAddToPlaylist: addToPlaylistPage == null + forwardEnabled: AudioPlayer.playModel.count > 1 // there needs to be something to forward to + + onPreviousClicked: AudioPlayer.playPrevious(true) + onPlayPauseClicked: AudioPlayer.playPause() + onNextClicked: AudioPlayer.playNext(true) + + onRepeatClicked: { + if (AudioPlayer.repeat && !AudioPlayer.repeatOne) { + AudioPlayer.repeatOne = true + } else { + AudioPlayer.repeat = !AudioPlayer.repeat + } + } + onShuffleClicked: AudioPlayer.shuffle = !AudioPlayer.shuffle + onAddToPlaylist: { + hideMenu() + var obj = pageStack.animatorPush(Qt.resolvedUrl("AddToPlaylistPage.qml"), { media: AudioPlayer.currentItem }) + obj.pageCompleted.connect(function(page) { + addToPlaylistPage = page + }) + } + + onOpenChanged: { + if (!open) AudioPlayer.pause() + AudioPlayer.playerVisible = open + } + onSliderReleased: AudioPlayer.setPosition(value * 1000) + + Column { + parent: extraContentItem + width: panel.width + + Label { + id: titleLabel + + width: Math.min(parent.width - 2*Theme.paddingMedium, implicitWidth) + anchors.horizontalCenter: parent.horizontalCenter + color: Theme.primaryColor + truncationMode: TruncationMode.Fade + visible: text.length > 0 + } + Label { + id: authorLabel + + width: Math.min(parent.width - 2*Theme.paddingMedium, implicitWidth) + anchors.horizontalCenter: parent.horizontalCenter + color: Theme.secondaryColor + font.pixelSize: Theme.fontSizeExtraSmall + truncationMode: TruncationMode.Fade + visible: text.length > 0 + } + } +} diff --git a/usr/lib/qt5/qml/com/jolla/mediaplayer/MediaPlayerListView.qml b/usr/lib/qt5/qml/com/jolla/mediaplayer/MediaPlayerListView.qml new file mode 100644 index 00000000..e1313afe --- /dev/null +++ b/usr/lib/qt5/qml/com/jolla/mediaplayer/MediaPlayerListView.qml @@ -0,0 +1,71 @@ +// -*- qml -*- + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import com.jolla.mediaplayer 1.0 + +FocusScope { + id: scope + + property alias model: listView.model + property alias count: listView.count + property alias delegate: listView.delegate + property alias header: headerContainer.children + property alias headerItem: listView.headerItem + property alias footer: footerContainer.children + property alias contentItem: listView.contentItem + property alias contentWidth: listView.contentWidth + default property alias _data: listView.flickableData + + anchors.fill: parent + + SilicaListView { + id: listView + + anchors.fill: parent + + header: Item { + onYChanged: headerContainer.y = y + height: headerContainer.height + } + + footer: Item { + onYChanged: footerContainer.y = y + height: footerContainer.height + } + + VerticalScrollDecorator {} + + // close the vkb when the list is scrolled + onMovementStarted: listView.focus = true + } + + /* This is a hack to place the header and footer over the */ + /* SilicaListView to avoid losing the focus upon new search filters, */ + /* as explained at JB#19789 */ + Item { + y: listView.contentItem.y + height: listView.contentItem.height + + Column { + id: headerContainer + width: scope.width + + property real _prevHeight: height + + // prevent contentY from jumping upwards when the header grows in height + onHeightChanged: { + var delta = (height - _prevHeight) + _prevHeight = height + if (delta > 0) { + listView.contentY -= delta + } + } + } + + Column { + id: footerContainer + width: scope.width + } + } +} diff --git a/usr/lib/qt5/qml/com/jolla/mediaplayer/NewPlaylistDialog.qml b/usr/lib/qt5/qml/com/jolla/mediaplayer/NewPlaylistDialog.qml new file mode 100644 index 00000000..c6598359 --- /dev/null +++ b/usr/lib/qt5/qml/com/jolla/mediaplayer/NewPlaylistDialog.qml @@ -0,0 +1,53 @@ +// -*- qml -*- + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import com.jolla.mediaplayer 1.0 + +Dialog { + id: dialog + + property var media + property Item pageToPop + property bool hasTitle: playlistName.text.trim().length > 0 + + canNavigateForward: hasTitle + + onAccepted: { + if (playlists.createPlaylist(playlistName.text, dialog.media)) { + // TODO: Should we provide feedback? + if (pageToPop) { + pageStack.pop(pageToPop) + } + } + // TODO: should we dismiss the previous page too? + } + + SilicaFlickable { + anchors.fill: parent + contentHeight: column.height + Column { + id: column + + width: parent.width + spacing: Theme.paddingLarge + + DialogHeader { } + + TextField { + id: playlistName + width: parent.width + focus: true + focusOutBehavior: FocusBehavior.KeepFocus + + //: placeholder for the text field in add playlist dialog. + //% "Playlist name" + placeholderText: qsTrId("mediaplayer-ph-playlist-name") + EnterKey.enabled: dialog.hasTitle + EnterKey.highlighted: dialog.hasTitle + EnterKey.iconSource: "image://theme/icon-m-enter-accept" + EnterKey.onClicked: dialog.accept() + } + } + } +} diff --git a/usr/lib/qt5/qml/com/jolla/mediaplayer/NowPlayingMenuItem.qml b/usr/lib/qt5/qml/com/jolla/mediaplayer/NowPlayingMenuItem.qml new file mode 100644 index 00000000..e574673e --- /dev/null +++ b/usr/lib/qt5/qml/com/jolla/mediaplayer/NowPlayingMenuItem.qml @@ -0,0 +1,17 @@ +// -*- qml -*- + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import com.jolla.mediaplayer 1.0 + +MenuItem { + visible: visualAudioAppModel.active + + //% "Play queue" + text: qsTrId("mediaplayer-he-play-queue") + + // Avoid font fitting that menu item does by default for too long labels + fontSizeMode: Text.FixedSize + + onClicked: pageStack.animatorPush(Qt.resolvedUrl("PlayQueuePage.qml")) +} diff --git a/usr/lib/qt5/qml/com/jolla/mediaplayer/PlayQueuePage.qml b/usr/lib/qt5/qml/com/jolla/mediaplayer/PlayQueuePage.qml new file mode 100644 index 00000000..d914ef73 --- /dev/null +++ b/usr/lib/qt5/qml/com/jolla/mediaplayer/PlayQueuePage.qml @@ -0,0 +1,82 @@ +// -*- qml -*- + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Sailfish.Media 1.0 +import com.jolla.mediaplayer 1.0 + +Page { + objectName: "PlayQueuePage" + + FilterModel { + id: playModel + + sourceModel: AudioPlayer.playModel + filterRegExp: RegExpHelpers.regExpFromSearchString(playQueueHeader.searchText, true) + } + + MediaPlayerListView { + id: view + + model: playModel + anchors.fill: parent + + PullDownMenu { + visible: view.count > 1 + + MenuItem { + //: Search menu entry + //% "Search" + text: qsTrId("mediaplayer-me-search") + onClicked: playQueueHeader.enableSearch() + } + } + + ViewPlaceholder { + //: Placeholder text for an empty search view + //% "No items found" + text: qsTrId("mediaplayer-la-empty-search") + enabled: view.count === 0 + } + + header: SearchPageHeader { + id: playQueueHeader + width: parent.width + + //: Title for the play queue page + //% "Play queue" + title: qsTrId("mediaplayer-he-play-queue") + + //: Playlist search field placeholder text + //% "Search song" + placeholderText: qsTrId("mediaplayer-tf-playlist-search") + } + + delegate: MediaListDelegate { + property bool requestRemove + + formatFilter: playQueueHeader.searchText + + menu: menuComponent + onClicked: AudioPlayer.playIndex(playModel.mapRowToSource(index)) + onMenuOpenChanged: { + if (!menuOpen && requestRemove) { + requestRemove = false + AudioPlayer.removeFromQueue(playModel.mapRowToSource(index)) + } + } + ListView.onRemove: animateRemoval() + Component { + id: menuComponent + ContextMenu { + MenuItem { + //: Remove song context menu entry in playqueue page + //% "Remove" + text: qsTrId("mediaplayer-me-playqueue-page-remove") + onClicked: requestRemove = true + } + } + } + } + } +} diff --git a/usr/lib/qt5/qml/com/jolla/mediaplayer/PlaylistColors.js b/usr/lib/qt5/qml/com/jolla/mediaplayer/PlaylistColors.js new file mode 100644 index 00000000..9ce84151 --- /dev/null +++ b/usr/lib/qt5/qml/com/jolla/mediaplayer/PlaylistColors.js @@ -0,0 +1,16 @@ +.pragma library + +var colors = ["#b3b35f54", "#b3b37948", "#b3bfa058", "#b38ca646", "#b3519c3f", "#b32ea3a1", "#b3458dba", "#b3506fc7", "#b3b3609b"] +var highlightColors = ["#b35f54", "#b37948", "#bfa058", "#8ca646", "#519c3f", "#2ea3a1", "#458dba", "#506fc7", "#b3609b"] + +function nameToColor(name) +{ + var index = parseInt(Qt.md5(name).substring(0, 8), 16) % colors.length + return colors[index] +} + +function nameToHighlightColor(name) +{ + var index = parseInt(Qt.md5(name).substring(0, 8), 16) % highlightColors.length + return highlightColors[index] +} diff --git a/usr/lib/qt5/qml/com/jolla/mediaplayer/PlaylistManager.qml b/usr/lib/qt5/qml/com/jolla/mediaplayer/PlaylistManager.qml new file mode 100644 index 00000000..6eafcb77 --- /dev/null +++ b/usr/lib/qt5/qml/com/jolla/mediaplayer/PlaylistManager.qml @@ -0,0 +1,185 @@ +// -*- qml -*- + +import QtQuick 2.0 +import Sailfish.Media 1.0 +import com.jolla.mediaplayer 1.0 + +Item { + id: root + + property int count + property bool populated + property string _pendingNewPlaylist + + signal updated(url playlistUrl) + + onUpdated: playlistListModel.refresh() + + function isEditable(uri) { + return saver.isEditable(uri, playlistsLocation) + } + + function appendToPlaylist(playlist, media) { + playlistModel.url = playlist.url + playlistModel.clear() + playlistModel.populate() + playlistModel.append(media.url, media.title, media.author, media.duration) + + var success = saver.save(playlistModel, playlist.title, playlist.url) + + if (success) { + store.updateEntryCounter(playlist.url, playlistModel.count) + + root.updated(playlist.url) + } + + return success + } + + function createPlaylist(title, media) { + var url = saver.create(title, playlistsLocation, media) + if (url != "") { + // signal once tracker picks up the new file + root._pendingNewPlaylist = url + return true + } + + return false + } + + function savePlaylist(media, model) { + var success = false + + if (model.count == 0) { + success = saver.clear(media.url, media.title) + } else { + success = saver.save(model, media.title, media.url) + } + + if (success) { + store.updateEntryCounter(media.url, model.count) + + root.updated(model.url) + } + + return success + } + + function clearPlaylist(media, model) { + var success = saver.clear(media.url, media.title) + if (success) { + store.updateEntryCounter(media.url, 0) + + root.updated(model.url) + } + + return success + } + + function removePlaylist(media) { + var success = saver.removePlaylist(media.url) + if (success) { + // This is a hack, we are filtering on nfo:entryCounter in the queries below + // Because it would take tracker ~2 seconds to index + store.updateEntryCounter(media.url, -1) + + root.updated("") + } + + return success + } + + + function removeItem(url) { + var playlists = new Array + var plModels = new Array + var refreshModels = false + + for (var i = 0; i < playlistListModel.count; ++i) { + // First iterate through top level playlist items + var playlist = playlistListModel.get(i) + var playlistModel = plComponent.createObject(null) + + // Check if the pl is editable + if (!isEditable(playlist.url)) { + continue + } + // Remove song instances from all the playlists. Note that the same + // song can be multiple times in the same playlist and that's why + // it's removed by URL not by index. + if (playlistModel) { + playlistModel.url = playlist.url + playlistModel.populate() + + if (playlistModel.removeItemByUrl(url) > 0) { + + refreshModels = playlistModel.count == 0 + ? saver.clear(playlist.url, playlist.title) + : saver.save(playlistModel, playlist.title, playlist.url) + + if (refreshModels) { + store.updateEntryCounter(playlist.url, playlistModel.count) + } + } + + playlistModel.destroy(1000) + } else { + console.log("Failed to create playlist model for ", playlist.url) + } + } + + // Update models at once. Otherwise there seem to be weird behavior + if (refreshModels) { + root.updated("") + } + } + + function updateAccessTime(url) { + store.updateAccessTime(url) + root.updated("") + } + + Timer { + id: timer + interval: 50 + onTriggered: root.count = playlistListModel.count + } + + Connections { + // Due to JB#21453, we are using a timer to change the count + // property in a delayed/lazy way so we won't update the UI so + // often with a lot of count changes instead of a big one. + target: playlistListModel + onCountChanged: timer.restart() + } + + PlaylistSaver { + id: saver + } + + PlaylistModel { + id: playlistModel + } + + Component { + id: plComponent + PlaylistModel {} + } + + TrackerStore { + id: store + + onGraphUpdated: { + if (graph == "http://tracker.api.gnome.org/ontology/v3/tracker#Audio" && root._pendingNewPlaylist != "") { + root.updated(root._pendingNewPlaylist) + root._pendingNewPlaylist = "" + } + } + } + + GriloTrackerModel { + id: playlistListModel + query: PlaylistTrackerHelpers.getPlaylistsQuery("", {}) + onFinished: populated = true + } +} diff --git a/usr/lib/qt5/qml/com/jolla/mediaplayer/PlaylistTrackerHelpers.js b/usr/lib/qt5/qml/com/jolla/mediaplayer/PlaylistTrackerHelpers.js new file mode 100644 index 00000000..0124e0a8 --- /dev/null +++ b/usr/lib/qt5/qml/com/jolla/mediaplayer/PlaylistTrackerHelpers.js @@ -0,0 +1,62 @@ +.pragma library +.import "RegExpHelpers.js" as RegExpHelpers +.import "TrackerHelpers.js" as TrackerHelpers +.import org.nemomobile.grilo 0.1 as Grilo + +// %1 = extra inner rules +// %2 = extra outer rules +var playlistsQuery = "" + + "SELECT " + + Grilo.GriloMedia.TypeContainer + " AS ?type " + + " ?urn AS ?id " + + " ?url " + + " ?title " + + " ?childcount " + + "WHERE { SERVICE {" + + " GRAPH tracker:Audio { " + + " SELECT ?urn ?url " + + " tracker:coalesce(nie:title(?urn), tracker:string-from-filename(nfo:fileName(?url))) AS ?title " + + " tracker:coalesce(nfo:entryCounter(?urn), 0) AS ?childcount " + + " nie:contentAccessed(?urn) AS ?contentAccessed " + + " WHERE { " + + " ?urn a nmm:Playlist ; " + + " nie:isStoredAs ?url . " + + " ?url nie:dataSource/tracker:available true . " + + " %1 " + + " } " + + " } }" + + " %2 " + + "}" + +function getPlaylistsQuery(aSearchText, opts) { + var location = "location" in opts ? opts["location"] : "" + var editablePlaylistsOnly = "editablePlaylistsOnly" in opts ? opts["editablePlaylistsOnly"] : false + var sortByUsage = "sortByUsage" in opts ? opts["sortByUsage"] : false + + // The entry counter because playlist removal has a hack for setting counter -1 + var extraInnerRules = "" + if (editablePlaylistsOnly) { + extraInnerRules = " FILTER ((nfo:entryCounter(?urn) >= 0 || !bound(nfo:entryCounter(?urn))) && fn:ends-with(?url, \".pls\") ) " + } else { + extraInnerRules = " FILTER ( (nfo:entryCounter(?urn) >= 0 || !bound(nfo:entryCounter(?urn))) ) " + } + + var extraOuterRules = "" + if (aSearchText != "") { + extraOuterRules = TrackerHelpers.getSearchFilter(aSearchText, "?title") + } + + if (location != "") { + extraOuterRules += " FILTER (tracker:uri-is-descendant(\"file://%1\", ?url) ) ".arg(TrackerHelpers.escapeSparql(location)) + } + + var orderByRule = "" + if (sortByUsage) { + // contentAccessed inserted only by us + orderByRule = " ORDER BY DESC(?contentAccessed)" + } else { + orderByRule = " ORDER BY ASC(fn:lower-case(?title))" + } + + return playlistsQuery.arg(extraInnerRules).arg(extraOuterRules) + orderByRule +} diff --git a/usr/lib/qt5/qml/com/jolla/mediaplayer/ProxyMprisMetaData.qml b/usr/lib/qt5/qml/com/jolla/mediaplayer/ProxyMprisMetaData.qml new file mode 100644 index 00000000..128c8800 --- /dev/null +++ b/usr/lib/qt5/qml/com/jolla/mediaplayer/ProxyMprisMetaData.qml @@ -0,0 +1,27 @@ +import QtQuick 2.0 + +QtObject { + property var trackId + property var duration + property var artUrl + property var albumTitle + property var albumArtist + property var contributingArtist + property var lyrics + property var audioBpm + property var autoRating + property var comment + property var composer + property var year + property var date + property var discNumber + property var firstUsed + property var genre + property var lastUsed + property var writer + property var title + property var trackNumber + property var url + property var useCount + property var userRating +} diff --git a/usr/lib/qt5/qml/com/jolla/mediaplayer/ProxyMprisPlayer.qml b/usr/lib/qt5/qml/com/jolla/mediaplayer/ProxyMprisPlayer.qml new file mode 100644 index 00000000..8a31fec2 --- /dev/null +++ b/usr/lib/qt5/qml/com/jolla/mediaplayer/ProxyMprisPlayer.qml @@ -0,0 +1,35 @@ +import QtQuick 2.0 +import Amber.Mpris 1.0 + +QtObject { + // Proxy for the Mpris2 Player + property bool canControl + property bool canGoNext + property bool canGoPrevious + property bool canPause + property bool canPlay + property bool canSeek + property int loopStatus: Mpris.LoopNone + property real maximumRate: 1 + property real minimumRate: 1 + property int playbackStatus: Mpris.Stopped + property int position + property real rate: 1 + property bool shuffle + property real volume + + property ProxyMprisMetaData metaData: ProxyMprisMetaData {} + + signal positionRequested() + signal pauseRequested() + signal playRequested() + signal playPauseRequested() + signal stopRequested() + signal nextRequested() + signal previousRequested() + signal seekRequested(int offset) + signal setPositionRequested(string trackId, int position) + signal openUriRequested(url uri) + signal loopStatusRequested(int loopStatus) + signal shuffleRequested(bool shuffle) +} diff --git a/usr/lib/qt5/qml/com/jolla/mediaplayer/RegExpHelpers.js b/usr/lib/qt5/qml/com/jolla/mediaplayer/RegExpHelpers.js new file mode 100644 index 00000000..2310ce46 --- /dev/null +++ b/usr/lib/qt5/qml/com/jolla/mediaplayer/RegExpHelpers.js @@ -0,0 +1,16 @@ +.pragma library + +function escapeRegExp(string) { + // As described at https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#Using_Special_Characters + return string ? string.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1") : "" +} + +function regExpFromSearchString(string, alwaysRegExp) { + // Emacs search style: only be case sensitive + // if there are capitals. + if (string && string == string.toLowerCase()) { + return alwaysRegExp ? new RegExp(escapeRegExp(string), "i") : string + } else { + return new RegExp(escapeRegExp(string)) + } +} diff --git a/usr/lib/qt5/qml/com/jolla/mediaplayer/SearchPageHeader.qml b/usr/lib/qt5/qml/com/jolla/mediaplayer/SearchPageHeader.qml new file mode 100644 index 00000000..8c22afad --- /dev/null +++ b/usr/lib/qt5/qml/com/jolla/mediaplayer/SearchPageHeader.qml @@ -0,0 +1,126 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 + +FocusScope { + id: scope + + property bool searchAsHeader: isLandscape + property alias title: pageHeader.title + property alias placeholderText: searchField.placeholderText + property alias searchText: searchField.text + property Item coverArt + default property alias _data: bottomCol.data + readonly property int _animationDuration: 200 + + implicitHeight: col.height + bottomCol.height + + onCoverArtChanged: if (coverArt) coverArt.parent = coverHolder + + function enableSearch() { + searchField.active = true + searchField.forceActiveFocus() + } + + Column { + id: col + width: parent.width + state: "HEADER" + + states: [ + State { + name: "HEADER" + when: !searchField.active && (!coverArt || coverArt.status !== Image.Ready) + PropertyChanges { target: coverHolder; opacity: 0.0 } + PropertyChanges { target: pageHeader; opacity: 1.0 } + PropertyChanges { target: header; height: pageHeader.height } + }, + State { + name: "SEARCH-HEADER" + when: searchField.active && searchAsHeader + PropertyChanges { target: coverHolder; opacity: 0.0 } + PropertyChanges { target: pageHeader; opacity: 0.0 } + PropertyChanges { target: header; height: pageHeader.height } + PropertyChanges { target: searchField; y: 0 } + }, + State { + name: "SEARCH" + when: searchField.active && !searchAsHeader + PropertyChanges { target: coverHolder; opacity: 0.0 } + PropertyChanges { target: pageHeader; opacity: 1.0 } + PropertyChanges { target: header; height: pageHeader.height + searchField.height } + }, + State { + name: "IMAGE" + when: !searchField.active && (coverArt && coverArt.status === Image.Ready) + PropertyChanges { target: pageHeader; opacity: 0.0 } + PropertyChanges { target: header; height: coverArt.height + Theme.paddingMedium } + PropertyChanges { target: coverHolder; opacity: 1.0 } + } + ] + + transitions: [ + Transition { + from: "SEARCH" + to: "HEADER" + PropertyAnimation { duration: scope._animationDuration; target: header; property: "height" } + }, + Transition { + from: "HEADER" + to: "SEARCH-HEADER" + FadeAnimation { duration: scope._animationDuration; target: pageHeader } + }, + Transition { + from: "SEARCH-HEADER" + to: "HEADER" + FadeAnimation { duration: scope._animationDuration; target: pageHeader } + } + ] + + Item { + id: header + + width: parent.width + height: pageHeader.height + + PageHeader { + id: pageHeader + width: parent.width + } + + SearchField { + id: searchField + + y: pageHeader.height + width: parent.width + canHide: text.length === 0 + active: false + + // Only animate in non-cover mode, to prevent height stuttering + transitionDuration: (coverArt && coverArt.status === Image.Ready) ? 0 : scope._animationDuration + + // We prefer lowercase + inputMethodHints: Qt.ImhNoAutoUppercase | Qt.ImhPreferLowercase | Qt.ImhNoPredictiveText + + onHideClicked: active = false + + EnterKey.iconSource: "image://theme/icon-m-enter-close" + EnterKey.onClicked: focus = false + } + + Item { + id: coverHolder + + property real coverArtSize: parent.width + + width: coverArtSize + height: coverArtSize + } + } + } + + Column { + id: bottomCol + width: parent.width + anchors.top: col.bottom + } +} diff --git a/usr/lib/qt5/qml/com/jolla/mediaplayer/TrackerHelpers.js b/usr/lib/qt5/qml/com/jolla/mediaplayer/TrackerHelpers.js new file mode 100644 index 00000000..21fc389d --- /dev/null +++ b/usr/lib/qt5/qml/com/jolla/mediaplayer/TrackerHelpers.js @@ -0,0 +1,31 @@ +.pragma library +.import "RegExpHelpers.js" as RegExpHelpers + +function escapeSparql(string) { + if (string == undefined) { + return "" + } + + // As described at http://www.w3.org/TR/rdf-sparql-query/#grammarEscapes + string = string.replace("\\", "\\\\") + .replace("\t", "\\t") + .replace("\n", "\\n") + .replace("\r", "\\r") + .replace("\b", "\\b") + .replace("\f", "\\f") + .replace("\"", "\\\"") + .replace("'", "\\'") + return string +} + +function getSearchFilter(searchText, variable) { + // Emacs search style: only be case sensitive if there are capitals. + var rule = "" + if (searchText == searchText.toLowerCase()) { + rule = "regex(%1, \"%2\", \"i\")".arg(variable).arg(escapeSparql(RegExpHelpers.escapeRegExp(searchText))) + } else { + rule = "fn:contains(%1, \"%2\")".arg(variable).arg(escapeSparql(searchText)) + } + + return " FILTER (%1) ".arg(rule) +} diff --git a/usr/lib/qt5/qml/com/jolla/mediaplayer/VisualAudioAppModel.qml b/usr/lib/qt5/qml/com/jolla/mediaplayer/VisualAudioAppModel.qml new file mode 100644 index 00000000..2a217736 --- /dev/null +++ b/usr/lib/qt5/qml/com/jolla/mediaplayer/VisualAudioAppModel.qml @@ -0,0 +1,32 @@ +// -*- qml -*- + +import QtQuick 2.0 +import com.jolla.mediaplayer 1.0 + +VisualAudioModel { + id: visualAudioAppModel + + readonly property alias playing: visualAudioAppModel._playing + readonly property alias repeat: visualAudioAppModel._repeat + readonly property alias shuffle: visualAudioAppModel._shuffle + + property bool _playing + property bool _repeat + property bool _shuffle + + function appModelUpdate() { + modelUpdate() + if (modelActive) { + _playing = Qt.binding(function () { return AudioPlayer.playing }) + _repeat = Qt.binding(function () { return AudioPlayer.repeat }) + _shuffle = Qt.binding(function () { return AudioPlayer.shuffle }) + } else { + _playing = _playing + _repeat = _repeat + _shuffle = _shuffle + } + } + + onModelActiveChanged: appModelUpdate() + Component.onCompleted: appModelUpdate() +} diff --git a/usr/lib/qt5/qml/com/jolla/mediaplayer/VisualAudioModel.qml b/usr/lib/qt5/qml/com/jolla/mediaplayer/VisualAudioModel.qml new file mode 100644 index 00000000..701054c1 --- /dev/null +++ b/usr/lib/qt5/qml/com/jolla/mediaplayer/VisualAudioModel.qml @@ -0,0 +1,58 @@ +// -*- qml -*- + +import QtQuick 2.0 +import com.jolla.mediaplayer 1.0 + +QtObject { + id: visualAudioModel + + property bool modelActive + + readonly property alias metadata: visualAudioModel._metadata + readonly property alias duration: visualAudioModel._duration + readonly property alias state: visualAudioModel._state + readonly property alias active: visualAudioModel._active + readonly property alias position: visualAudioModel._position + + property var _metadata: ({}) + property int _duration + property int _state + property bool _active + property int _position + + function cloneMetadata(metadata) { + if (metadata && 'url' in metadata) { + return { + 'url' : metadata.url, + 'title' : metadata.title, + 'artist' : metadata.artist, + 'album' : metadata.album, + 'genre' : metadata.genre, + 'track' : metadata.track, + 'trackCount': metadata.trackCount, + 'duration' : metadata.duration + } + } + + return {} + } + + function modelUpdate() { + if (modelActive) { + _metadata = Qt.binding(function () { return cloneMetadata(AudioPlayer.metadata) }) + _duration = Qt.binding(function () { return AudioPlayer.duration }) + _state = Qt.binding(function () { return AudioPlayer.state }) + _active = Qt.binding(function () { return AudioPlayer.active }) + _position = Qt.binding(function () { return AudioPlayer.position }) + } else { + _metadata = _metadata + _duration = _duration + _state = _state + _active = _active + _position = _position + } + } + + onModelActiveChanged: modelUpdate() + Component.onCompleted: modelUpdate() +} diff --git a/usr/lib/qt5/qml/com/jolla/mediaplayer/qmldir b/usr/lib/qt5/qml/com/jolla/mediaplayer/qmldir new file mode 100644 index 00000000..2422b10f --- /dev/null +++ b/usr/lib/qt5/qml/com/jolla/mediaplayer/qmldir @@ -0,0 +1,27 @@ +module com.jolla.mediaplayer +plugin mediaplayerplugin +AddToPlaylistPage 1.0 AddToPlaylistPage.qml +AlbumArt 1.0 AlbumArt.qml +singleton AudioPlayer 1.0 AudioPlayer.qml +Container 1.0 Container.qml +CoverArt 1.0 CoverArt.qml +GriloTrackerModel 1.0 GriloTrackerModel.qml +MediaContainerListDelegate 1.0 MediaContainerListDelegate.qml +MediaContainerIconDelegate 1.0 MediaContainerIconDelegate.qml +MediaContainerPlaylistDelegate 1.0 MediaContainerPlaylistDelegate.qml +MediaListDelegate 1.0 MediaListDelegate.qml +MediaPlayerDockedPanel 1.0 MediaPlayerDockedPanel.qml +MediaPlayerListView 1.0 MediaPlayerListView.qml +ProxyMprisPlayer 1.0 ProxyMprisPlayer.qml +NewPlaylistDialog 1.0 NewPlaylistDialog.qml +NowPlayingMenuItem 1.0 NowPlayingMenuItem.qml +PlayQueuePage 1.0 PlayQueuePage.qml +PlaylistColors 1.0 PlaylistColors.js +PlaylistManager 1.0 PlaylistManager.qml +SearchPageHeader 1.0 SearchPageHeader.qml +VisualAudioAppModel 1.0 VisualAudioAppModel.qml +VisualAudioModel 1.0 VisualAudioModel.qml +TrackerHelpers 1.0 TrackerHelpers.js +RegExpHelpers 1.0 RegExpHelpers.js +PlaylistTrackerHelpers 1.0 PlaylistTrackerHelpers.js +AudioTrackerHelpers 1.0 AudioTrackerHelpers.js diff --git a/usr/lib/qt5/qml/com/jolla/notes/settings/translations/qmldir b/usr/lib/qt5/qml/com/jolla/notes/settings/translations/qmldir new file mode 100644 index 00000000..f998fe73 --- /dev/null +++ b/usr/lib/qt5/qml/com/jolla/notes/settings/translations/qmldir @@ -0,0 +1,2 @@ +module com.jolla.notes.settings.translations +plugin notessettingsplugin diff --git a/usr/lib/qt5/qml/com/jolla/settings/ApplicationsModel.qml b/usr/lib/qt5/qml/com/jolla/settings/ApplicationsModel.qml new file mode 100644 index 00000000..19dcd5c0 --- /dev/null +++ b/usr/lib/qt5/qml/com/jolla/settings/ApplicationsModel.qml @@ -0,0 +1,14 @@ +/* + * Copyright (c) 2022 Jolla Ltd. + * + * License: Proprietary + */ + +pragma Singleton +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import com.jolla.settings 1.0 + +ApplicationSettingsModel { + iconDirectories: Theme.launcherIconDirectories +} diff --git a/usr/lib/qt5/qml/com/jolla/settings/SettingsErrorNotification.qml b/usr/lib/qt5/qml/com/jolla/settings/SettingsErrorNotification.qml index 593a3c39..2df7ca9f 100644 --- a/usr/lib/qt5/qml/com/jolla/settings/SettingsErrorNotification.qml +++ b/usr/lib/qt5/qml/com/jolla/settings/SettingsErrorNotification.qml @@ -51,6 +51,10 @@ Notification { //% "SIM card disabled" previewBody = qsTrId("settings-la-sim_card_disabled") break + case SettingsControlError.NoEthernetDevice: + //% "No ethernet device active" + previewBody = qsTrId("settings-la-no_ethernet_device") + break default: // No notification console.warn("Trying to send an unknown error notification. Source of programming error.") diff --git a/usr/lib/qt5/qml/com/jolla/settings/SettingsPage.qml b/usr/lib/qt5/qml/com/jolla/settings/SettingsPage.qml index 6b0ec745..e5c444fc 100644 --- a/usr/lib/qt5/qml/com/jolla/settings/SettingsPage.qml +++ b/usr/lib/qt5/qml/com/jolla/settings/SettingsPage.qml @@ -1,7 +1,7 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 import com.jolla.settings 1.0 -import org.nemomobile.configuration 1.0 +import Nemo.Configuration 1.0 Page { id: page diff --git a/usr/lib/qt5/qml/com/jolla/settings/SettingsSectionView.qml b/usr/lib/qt5/qml/com/jolla/settings/SettingsSectionView.qml index b983d019..56577957 100644 --- a/usr/lib/qt5/qml/com/jolla/settings/SettingsSectionView.qml +++ b/usr/lib/qt5/qml/com/jolla/settings/SettingsSectionView.qml @@ -1,7 +1,7 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 import com.jolla.settings 1.0 -import org.nemomobile.configuration 1.0 +import Nemo.Configuration 1.0 Column { id: root diff --git a/usr/lib/qt5/qml/com/jolla/settings/accounts/AccountCreationManager.qml b/usr/lib/qt5/qml/com/jolla/settings/accounts/AccountCreationManager.qml index ee7394ad..f89aced0 100644 --- a/usr/lib/qt5/qml/com/jolla/settings/accounts/AccountCreationManager.qml +++ b/usr/lib/qt5/qml/com/jolla/settings/accounts/AccountCreationManager.qml @@ -1,5 +1,5 @@ /* - * Copyright (c) 2013 - 2019 Jolla Ltd. + * Copyright (c) 2013 - 2022 Jolla Ltd. * Copyright (c) 2020 Open Mobile Platform LLC. * * License: Proprietary @@ -9,9 +9,10 @@ import QtQuick 2.6 import Sailfish.Silica 1.0 import Sailfish.Accounts 1.0 import Sailfish.Policy 1.0 -import MeeGo.Connman 0.2 +import Sailfish.Settings.Networking 1.0 +import Connman 0.2 import com.jolla.settings.accounts 1.0 -import org.nemomobile.configuration 1.0 +import Nemo.Configuration 1.0 import org.nemomobile.systemsettings 1.0 import Nemo.Notifications 1.0 diff --git a/usr/lib/qt5/qml/com/jolla/settings/accounts/AccountSyncAdapter.qml b/usr/lib/qt5/qml/com/jolla/settings/accounts/AccountSyncAdapter.qml index 4a0e3580..49dd307f 100644 --- a/usr/lib/qt5/qml/com/jolla/settings/accounts/AccountSyncAdapter.qml +++ b/usr/lib/qt5/qml/com/jolla/settings/accounts/AccountSyncAdapter.qml @@ -8,7 +8,7 @@ import QtQuick 2.6 import Sailfish.Accounts 1.0 import Nemo.DBus 2.0 -import MeeGo.Connman 0.2 +import Connman 0.2 QtObject { id: root diff --git a/usr/lib/qt5/qml/com/jolla/settings/accounts/CountryValueButton.qml b/usr/lib/qt5/qml/com/jolla/settings/accounts/CountryValueButton.qml index 267be7a2..67fd43d6 100644 --- a/usr/lib/qt5/qml/com/jolla/settings/accounts/CountryValueButton.qml +++ b/usr/lib/qt5/qml/com/jolla/settings/accounts/CountryValueButton.qml @@ -2,7 +2,7 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 import Sailfish.Timezone 1.0 import com.jolla.settings.accounts 1.0 -import MeeGo.QOfono 0.2 +import QOfono 0.2 ValueButton { id: root @@ -69,8 +69,10 @@ ValueButton { Component { id: countryPickerComponent + CountryPicker { model: countryModel + showUndefinedCountry: true } } } diff --git a/usr/lib/qt5/qml/com/jolla/settings/accounts/JollaAccountAddOnsPage.qml b/usr/lib/qt5/qml/com/jolla/settings/accounts/JollaAccountAddOnsPage.qml new file mode 100644 index 00000000..7d999854 --- /dev/null +++ b/usr/lib/qt5/qml/com/jolla/settings/accounts/JollaAccountAddOnsPage.qml @@ -0,0 +1,175 @@ +import QtQuick 2.0 +import Sailfish.Accounts 1.0 +import Sailfish.Silica 1.0 +import Sailfish.Store 1.0 +import com.jolla.settings.accounts 1.0 + +Page { + id: root + + AddOnModel { + id: activeAddOns + licenseStateFilter: AddOnModel.LicenseActiveOnly + Component.onCompleted: populate() + } + + AddOnModel { + id: inactiveAddOns + licenseStateFilter: AddOnModel.LicenseInactiveOnly + Component.onCompleted: populate() + } + + Connections { + target: Qt.application + onActiveChanged: { + if (Qt.application.active) { + activeAddOns.populate() + inactiveAddOns.populate() + } + } + } + + PageBusyIndicator { + running: !activeAddOns.populated || !inactiveAddOns.populated + } + + SilicaFlickable { + anchors.fill: parent + + Column { + width: parent.width + + PageHeader { + //: Heading for page that lists Add-Ons to Sailfish OS + //% "Sailfish Add-Ons" + title: qsTrId("settings_accounts-he-add_ons_page") + } + + SectionHeader { + visible: activeAddOns.count > 0 + //: Heading for section that lists Add-Ons with active license + //% "My" + text: qsTrId("settings_accounts-he-my_add_ons") + } + + Repeater { + model: activeAddOns + delegate: itemComponent + } + + SectionHeader { + visible: inactiveAddOns.count > 0 + //: Heading for section that lists Add-Ons without active license + //% "Available" + text: qsTrId("settings_accounts-he-available_add_ons") + } + + Repeater { + model: inactiveAddOns + delegate: itemComponent + } + + Item { + width: 1 + height: Theme.paddingLarge + } + + Label { + x: Theme.horizontalPageMargin + width: root.width - 2*x + visible: inactiveAddOns.populated && !inactiveAddOns.error + && inactiveAddOns.count == 0 && activeAddOns.count > 0 + font { + pixelSize: Theme.fontSizeLarge + family: Theme.fontFamilyHeading + } + color: palette.secondaryHighlightColor + wrapMode: Text.Wrap + horizontalAlignment: Text.AlignHCenter + //% "All Add-Ons available for your device are enabled" + text: qsTrId("settings_accounts-la-no_more_add_ons") + } + } + + ViewPlaceholder { + enabled: activeAddOns.populated + && ((activeAddOns.count == 0 && inactiveAddOns.count == 0) + || activeAddOns.error) + text: activeAddOns.error + ? activeAddOns.error + //% "No Add-On available for your device" + : qsTrId("settings_accounts-la-no_add_on") + } + } + + Component { + id: itemComponent + ListItem { + id: item + + width: parent.width + contentHeight: Math.max(contentColumn.height, image.height) + Theme.paddingMedium + + menu: licenseActive ? null : contextMenu + + property int fontSize: Screen.sizeCategory > Screen.Medium ? Theme.fontSizeMedium : Theme.fontSizeSmall + property int smallFontSize: Screen.sizeCategory > Screen.Medium ? Theme.fontSizeSmall : Theme.fontSizeExtraSmall + property int iconSize: Screen.sizeCategory > Screen.Medium ? Theme.iconSizeLauncher : Theme.iconSizeMedium + + onClicked: openMenu() + + Image { + id: image + width: height + height: item.iconSize + anchors { + left: parent.left + leftMargin: Theme.horizontalPageMargin + verticalCenter: parent.verticalCenter + } + source: icon + onStatusChanged: { + if (status == Image.Error) + console.log("Error loading image. source: " + source) + } + } + + Column { + id: contentColumn + anchors { + left: image.right + leftMargin: Theme.paddingMedium + right: parent.right + rightMargin: Theme.horizontalPageMargin + verticalCenter: parent.verticalCenter + } + + Label { + width: parent.width + truncationMode: TruncationMode.Fade + font.pixelSize: item.fontSize + text: displayName + } + + Label { + width: parent.width + text: summary + font.pixelSize: item.smallFontSize + wrapMode: Text.Wrap + truncationMode: TruncationMode.Fade + } + } + + Component { + id: contextMenu + ContextMenu { + MenuItem { + //% "Get this Add-On" + text: qsTrId("settings_accounts-me-get_add_on") + onClicked: Qt.openUrlExternally(shopLink) + } + } + } + } + } +} diff --git a/usr/lib/qt5/qml/com/jolla/settings/accounts/JollaAccountCreationSecondDialog.qml b/usr/lib/qt5/qml/com/jolla/settings/accounts/JollaAccountCreationSecondDialog.qml index 5656c38b..6cf7c193 100644 --- a/usr/lib/qt5/qml/com/jolla/settings/accounts/JollaAccountCreationSecondDialog.qml +++ b/usr/lib/qt5/qml/com/jolla/settings/accounts/JollaAccountCreationSecondDialog.qml @@ -20,7 +20,6 @@ Dialog { property alias firstName: firstNameField.text property alias lastName: lastNameField.text property alias email: emailField.text - property alias phoneNumber: phoneField.text property alias countryCode: countryButton.countryCode property alias countryName: countryButton.countryName property string languageLocale: languageModel.locale(languageModel.currentIndex) @@ -47,7 +46,6 @@ Dialog { firstNameField.text, lastNameField.text, birthday, - phoneField.text, countryCode, languageLocale, "Jolla", "Jolla") @@ -83,16 +81,6 @@ Dialog { }) _selfPerson.emailDetails = emails } - var myPhone = phoneNumber.trim() - if (myPhone !== "") { - var numbers = _selfPerson.phoneDetails - numbers.push({ - 'type': Person.PhoneNumberType, - 'number': myPhone, - 'index': -1 - }) - _selfPerson.phoneDetails = numbers - } if (birthday && !isNaN(birthday.getTime())) { _selfPerson.birthday = birthday } @@ -116,11 +104,9 @@ Dialog { return details[details.length - 1][property] } - // note phone field is optional canAccept: firstNameField.text !== "" && lastNameField.text !== "" && !emailField.errorHighlight - && countryCode !== "" && languageLocale !== "" && birthdayButton.isOldEnough(birthday) @@ -270,39 +256,14 @@ Dialog { : "" EnterKey.enabled: text || inputMethodComposing - EnterKey.iconSource: "image://theme/icon-m-enter-next" - EnterKey.onClicked: phoneField.focus = true - errorHighlight: (!text || !root._emailRegex.test(text)) && checkMandatoryFields - } - - TextField { - id: phoneField - width: parent.width - inputMethodHints: Qt.ImhDialableCharactersOnly - - //% "Phone number (optional)" - label: qsTrId("settings_accounts-la-phone_optional") - - //% "Enter phone number (optional)" - placeholderText: qsTrId("settings_accounts-ph-phone_optional") - - text: root._selfPerson != null - ? root._selectSelfPersonMultiValueField(root._selfPerson.phoneDetails, 'number') - : "" - - EnterKey.enabled: true EnterKey.iconSource: "image://theme/icon-m-enter-close" EnterKey.onClicked: root.focus = true + errorHighlight: (!text || !root._emailRegex.test(text)) && checkMandatoryFields } CountryValueButton { id: countryButton - // TODO: change hardcoded color to upcoming theme error color - valueColor: countryCode === "" && checkMandatoryFields - ? "#ff4d4d" - : Theme.highlightColor - onCountrySelected: { root.focus = true } @@ -418,7 +379,8 @@ Dialog { color: Theme.highlightColor //: Explains why it's necessary to ask for the user's birthday and country information when creating a Jolla account. - //% "This information is needed to show appropriate content in the Jolla Store. Some apps or content may be age-restricted or only released in certain areas." + //% "This information is needed to show appropriate content in the Jolla Store. " + //% "Some apps or content may be age-restricted or only released in certain areas." text: qsTrId("settings_accounts-la-why_ask_for_personal_info") } } diff --git a/usr/lib/qt5/qml/com/jolla/settings/accounts/JollaAccountCredentialsInput.qml b/usr/lib/qt5/qml/com/jolla/settings/accounts/JollaAccountCredentialsInput.qml index d67f2bf1..80f4a944 100644 --- a/usr/lib/qt5/qml/com/jolla/settings/accounts/JollaAccountCredentialsInput.qml +++ b/usr/lib/qt5/qml/com/jolla/settings/accounts/JollaAccountCredentialsInput.qml @@ -16,7 +16,8 @@ Column { property bool highlightInvalidFields property bool canValidateCredentials: !busy && username !== "" - && ((root.state == "signIn" && passwordValidator.hasValidValue && !signInFailed) || (root.state == "createNewAccount" && confirmPasswordValidator.hasValidValue)) + && ((root.state == "signIn" && passwordValidator.hasValidValue && !signInFailed) + || (root.state == "createNewAccount" && confirmPasswordValidator.hasValidValue)) property bool busy: usernameValidator.validating || confirmPasswordValidator.validating || signInFactory.signingIn property bool usernameValid: _usernameStatus == AccountFactory.UsernameAvailable property bool signInFailed: _signInStatus < 0 @@ -140,7 +141,9 @@ Column { AccountUsernameField { id: usernameField errorHighlight: (!text && root.highlightInvalidFields) - || (root.state == "createNewAccount" && root._usernameStatus != AccountFactory.UsernameAvailable && _usernameStatus != AccountFactory.UsernameNotChecked) + || (root.state == "createNewAccount" + && root._usernameStatus != AccountFactory.UsernameAvailable + && _usernameStatus != AccountFactory.UsernameNotChecked) || (root.state == "signIn" && root.signInFailed) onTextChanged: { @@ -294,16 +297,17 @@ Column { Item { id: passwordConfirmContainer + width: parent.width height: 0 clip: true PasswordField { id: passwordConfirmField + width: parent.width errorHighlight: !text && root.highlightInvalidFields || (!confirmPasswordValidator.hasValidValue && confirmPasswordValidator.progressDisplayed) - //% "Re-enter password" label: qsTrId("settings_accounts-la-password_confirm") diff --git a/usr/lib/qt5/qml/com/jolla/settings/accounts/JollaAccountSettingsDisplay.qml b/usr/lib/qt5/qml/com/jolla/settings/accounts/JollaAccountSettingsDisplay.qml index 558ad8d8..96bacefa 100644 --- a/usr/lib/qt5/qml/com/jolla/settings/accounts/JollaAccountSettingsDisplay.qml +++ b/usr/lib/qt5/qml/com/jolla/settings/accounts/JollaAccountSettingsDisplay.qml @@ -15,7 +15,7 @@ StandardAccountSettingsDisplay { font.pixelSize: Theme.fontSizeSmall //: Brief description for the Jolla account page - //% "Your Jolla account is your pathway to applications and software updates." + //% "Your Jolla account is your pathway to applications, add-ons and software updates." text: qsTrId("settings_accounts-la-jolla_account_description") } diff --git a/usr/lib/qt5/qml/com/jolla/settings/accounts/JollaAccountSetupDialog.qml b/usr/lib/qt5/qml/com/jolla/settings/accounts/JollaAccountSetupDialog.qml index 3dd68e4a..66c1af70 100644 --- a/usr/lib/qt5/qml/com/jolla/settings/accounts/JollaAccountSetupDialog.qml +++ b/usr/lib/qt5/qml/com/jolla/settings/accounts/JollaAccountSetupDialog.qml @@ -183,7 +183,7 @@ Dialog { FileWatcher { id: startupWizardDoneWatcher Component.onCompleted: { - var markerFile = StandardPaths.home + "/.jolla-startupwizard-usersession-done" + var markerFile = StandardPaths.home + "/.config/jolla-startupwizard-usersession-done" if (!testFileExists(markerFile)) { fileName = markerFile } @@ -255,7 +255,7 @@ Dialog { text: qsTrId("settings_accounts-la-have_jolla_account_heading") //: User selects this option if he/she already has a Jolla account - //% "You probably have a Jolla account if you have used a Sailfish device, created an account at account.jolla.com or used our community platform at together.jolla.com." + //% "You probably have a Jolla account if you have used a Sailfish device, created an account at account.jolla.com or used our community platform at forum.sailfishos.org." description: qsTrId("settings_accounts-la-have_jolla_account_description") onClicked: { @@ -410,6 +410,7 @@ Dialog { Component { id: accountCreationComponent + JollaAccountCreationSecondDialog { acceptDestination: busyComponent @@ -436,6 +437,7 @@ Dialog { Component { id: busyComponent + AccountBusyPage { function showError(errorCode, errorMessage) { if (errorCode != AccountFactory.UnknownError && errorCode != AccountFactory.InternalError) { @@ -458,6 +460,7 @@ Dialog { Component { id: skipConfirmationComponent + Dialog { acceptDestination: root.skipDestination acceptDestinationAction: root.skipDestinationAction diff --git a/usr/lib/qt5/qml/com/jolla/settings/accounts/OAuthAccountSetupPage.qml b/usr/lib/qt5/qml/com/jolla/settings/accounts/OAuthAccountSetupPage.qml index 9534f941..665ba19c 100644 --- a/usr/lib/qt5/qml/com/jolla/settings/accounts/OAuthAccountSetupPage.qml +++ b/usr/lib/qt5/qml/com/jolla/settings/accounts/OAuthAccountSetupPage.qml @@ -2,6 +2,7 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 import Sailfish.Accounts 1.0 import com.jolla.settings.accounts 1.0 +import com.jolla.signonuiservice 1.0 AccountBusyPage { id: root @@ -18,6 +19,10 @@ AccountBusyPage { signal accountCredentialsUpdated(int accountId, var responseData) signal accountCredentialsUpdateError(string errorMessage) + SignonUiService { + id: _jolla_signon_ui_service + } + function prepareAccountCreation(accountProvider, signonServiceName, signonSessionData) { if (_busy) { console.log("OAuthAccountSetupPage: operation in progress") @@ -62,11 +67,12 @@ AccountBusyPage { } // also ensure that we set up embedding / etc correctly: - if (typeof jolla_signon_ui_service !== "undefined") { + if (typeof _jolla_signon_ui_service !== "undefined") { sip.setParameter("Title", accountProvider.displayName) - sip.setParameter("InProcessServiceName", jolla_signon_ui_service.inProcessServiceName) - sip.setParameter("InProcessObjectPath", jolla_signon_ui_service.inProcessObjectPath) - jolla_signon_ui_service.inProcessParent = webViewContainer + sip.setParameter("InProcessServiceName", _jolla_signon_ui_service.inProcessServiceName) + sip.setParameter("InProcessObjectPath", _jolla_signon_ui_service.inProcessObjectPath) + _jolla_signon_ui_service.inProcessParent = webViewContainer + _jolla_signon_ui_service.openListeningPort() } account.signInCredentialsUpdated.connect(_accountUpdateSucceeded) @@ -145,11 +151,17 @@ AccountBusyPage { } // also ensure that we set up embedding / etc correctly: - if (typeof jolla_signon_ui_service !== "undefined") { + if (typeof _jolla_signon_ui_service !== "undefined") { params["Title"] = accountProvider.displayName - params["InProcessServiceName"] = jolla_signon_ui_service.inProcessServiceName - params["InProcessObjectPath"] = jolla_signon_ui_service.inProcessObjectPath - jolla_signon_ui_service.inProcessParent = webViewContainer + params["InProcessServiceName"] = _jolla_signon_ui_service.inProcessServiceName + params["InProcessObjectPath"] = _jolla_signon_ui_service.inProcessObjectPath + _jolla_signon_ui_service.inProcessParent = webViewContainer + // The listening port will be available immediately or not at all + _jolla_signon_ui_service.openListeningPort() + // AccountFactory will replace Callback instances of "localhost" with "http://127.0.0.1:" + params["Port"] + // This is needed for the request_token step of the 3-legged OAuth 1.0a flow (twitter) + // The request is sent by signond, but the reply is handled by libjollasignonuiservice + params["Port"] = _jolla_signon_ui_service.listeningPort } return params diff --git a/usr/lib/qt5/qml/com/jolla/settings/accounts/StandardAccountSettingsLoader.qml b/usr/lib/qt5/qml/com/jolla/settings/accounts/StandardAccountSettingsLoader.qml index 9506ab43..ae092d6c 100644 --- a/usr/lib/qt5/qml/com/jolla/settings/accounts/StandardAccountSettingsLoader.qml +++ b/usr/lib/qt5/qml/com/jolla/settings/accounts/StandardAccountSettingsLoader.qml @@ -91,7 +91,7 @@ QtObject { for (var profileId in profiles) { var syncOptions = profiles[profileId] if (syncOptions.modified) { - return true; + return true } } } diff --git a/usr/lib/qt5/qml/com/jolla/settings/accounts/ValidatedTextInput.qml b/usr/lib/qt5/qml/com/jolla/settings/accounts/ValidatedTextInput.qml index 04e32d6b..37713e9a 100644 --- a/usr/lib/qt5/qml/com/jolla/settings/accounts/ValidatedTextInput.qml +++ b/usr/lib/qt5/qml/com/jolla/settings/accounts/ValidatedTextInput.qml @@ -16,7 +16,8 @@ Item { // if textfield has a label, only show error banner when the textfield has text, else there // would be an odd gap between the textfield text and the banner - readonly property bool progressDisplayed: (!textField || !textField.labelVisible || textField.text.length > 0) && validationProgressLabel.text.length + readonly property bool progressDisplayed: (!textField || !textField.labelVisible || textField.text.length > 0) + && validationProgressLabel.text.length signal validationRequested() signal validationCanceled() diff --git a/usr/lib/qt5/qml/com/jolla/settings/accounts/qmldir b/usr/lib/qt5/qml/com/jolla/settings/accounts/qmldir index 293cb771..0f9120c0 100644 --- a/usr/lib/qt5/qml/com/jolla/settings/accounts/qmldir +++ b/usr/lib/qt5/qml/com/jolla/settings/accounts/qmldir @@ -6,6 +6,7 @@ JollaAccountCreationSecondDialog 1.0 JollaAccountCreationSecondDialog.qml JollaAccountSignInDialog 1.0 JollaAccountSignInDialog.qml JollaAccountSettingsDialog 1.0 JollaAccountSettingsDialog.qml JollaAccountSettingsDisplay 1.0 JollaAccountSettingsDisplay.qml +JollaAccountAddOnsPage 1.0 JollaAccountAddOnsPage.qml AccountsPage 1.0 AccountsPage.qml AccountsView 1.0 AccountsView.qml AccountsViewLogic 1.0 AccountsViewLogic.qml @@ -23,7 +24,6 @@ AccountBusyPage 1.0 AccountBusyPage.qml AccountMainSettingsDisplay 1.0 AccountMainSettingsDisplay.qml AccountServiceSettingsDisplay 1.0 AccountServiceSettingsDisplay.qml AccountSyncAdapter 1.0 AccountSyncAdapter.qml -NetworkCheckDialog 1.0 NetworkCheckDialog.qml AccountUsernameField 1.0 AccountUsernameField.qml SyncPastPeriodOptions 1.0 SyncPastPeriodOptions.qml SyncScheduleOptions 1.0 SyncScheduleOptions.qml diff --git a/usr/lib/qt5/qml/com/jolla/settings/crashreporter/PrivacyNotice.qml b/usr/lib/qt5/qml/com/jolla/settings/crashreporter/PrivacyNotice.qml new file mode 100644 index 00000000..008fd585 --- /dev/null +++ b/usr/lib/qt5/qml/com/jolla/settings/crashreporter/PrivacyNotice.qml @@ -0,0 +1,113 @@ +/* + * This file is part of crash-reporter + * + * Copyright (C) 2013 Jolla Ltd. + * Contact: Jakub Adam + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * version 2.1 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, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import com.jolla.settings.crashreporter 1.0 + +Item { + id: root + + property Page page + readonly property bool pageActive: page.status === PageStatus.Active + property bool privacyNoticeShown + + onPageActiveChanged: { + if (pageActive) { + if (PrivacySettings.privacyNoticeAccepted) { + pageStack.pop() + } else if (!privacyNoticeShown) { + privacyNoticeShown = true + pageStack.push(privacyNoticeDialog, null, PageStackAction.Immediate) + } else { + pageStack.pop() + } + } + } + + Component { + id: privacyNoticeDialog + + Dialog { + onAccepted: { + PrivacySettings.privacyNoticeAccepted = true + } + + onRejected: { + PrivacySettings.privacyNoticeAccepted = false + } + + SilicaFlickable { + anchors.fill: parent + contentHeight: column.height + Column { + id: column + + width: parent.width + spacing: Theme.paddingLarge + DialogHeader {} + Label { + x: Theme.horizontalPageMargin + width: parent.width - x*2 + + font.pixelSize: Theme.fontSizeLarge + //% "Privacy notice" + text: qsTrId("quick-feedback_privacy_notice") + wrapMode: Text.Wrap + color: Theme.highlightColor + } + Label { + x: Theme.horizontalPageMargin + width: parent.width - x*2 + + font.pixelSize: Theme.fontSizeSmall + //% "Please be warned that Crash Reporter and Quick " + //% "Feedback upload statistics of device usage to a remote " + //% "server, including pieces of information like IMEI " + //% "number that can uniquely identify your device. Crash " + //% "reports may also include partial or full snapshots of " + //% "program memory, potentially including sensitive data " + //% "like your unencrypted passwords, credit card numbers " + //% "etc." + text: qsTrId("quick-feedback_privacy_notice_text_1") + wrapMode: Text.Wrap + color: Theme.highlightColor + } + Label { + x: Theme.horizontalPageMargin + width: parent.width - x*2 + + font.pixelSize: Theme.fontSizeSmall + //% "By accepting this dialog you declare you are aware of " + //% "the potential security risks and give consent to " + //% "process the data collected from your device for the " + //% "purpose of analyzing bugs in the applications or the " + //% "operating system." + text: qsTrId("quick-feedback_privacy_notice_text_2") + wrapMode: Text.Wrap + color: Theme.highlightColor + } + } + VerticalScrollDecorator {} + } + } + } +} diff --git a/usr/lib/qt5/qml/com/jolla/settings/crashreporter/SystemdServiceSwitch.qml b/usr/lib/qt5/qml/com/jolla/settings/crashreporter/SystemdServiceSwitch.qml new file mode 100644 index 00000000..15a9118a --- /dev/null +++ b/usr/lib/qt5/qml/com/jolla/settings/crashreporter/SystemdServiceSwitch.qml @@ -0,0 +1,73 @@ +/* + * This file is part of crash-reporter + * + * Copyright (C) 2014 Jolla Ltd. + * Contact: Jakub Adam + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * version 2.1 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, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import com.jolla.settings.crashreporter 1.0 + +TextSwitch { + id: serviceSwitch + + property alias serviceName: service.serviceName + property alias managerType: service.managerType + property alias serviceEnabled: service.enabled + + signal beforeStateChange(bool newState) + signal afterStateChange(bool newState) + + automaticCheck: false + + SystemdService { + id: service + } + + onClicked: { + var newState = !checked + + beforeStateChange(newState) + + if (newState) { + service.start() + } else { + service.stop() + } + + afterStateChange(newState) + } + + states: [ + State { + name: "inactive" + when: (service.state == SystemdService.Inactive) + PropertyChanges { target: serviceSwitch; checked: false } + }, + State { + name: "activating" + when: (service.state == SystemdService.Activating || service.state == SystemdService.Deactivating) + PropertyChanges { target: serviceSwitch; checked: true; busy: true } + }, + State { + name: "active" + when: (service.state == SystemdService.Active) + PropertyChanges { target: serviceSwitch; checked: true } + } + ] +} diff --git a/usr/lib/qt5/qml/com/jolla/settings/crashreporter/qmldir b/usr/lib/qt5/qml/com/jolla/settings/crashreporter/qmldir new file mode 100644 index 00000000..fa629636 --- /dev/null +++ b/usr/lib/qt5/qml/com/jolla/settings/crashreporter/qmldir @@ -0,0 +1,4 @@ +module com.jolla.settings.crashreporter +plugin settingsplugin +PrivacyNotice 1.0 PrivacyNotice.qml +SystemdServiceSwitch 1.0 SystemdServiceSwitch.qml diff --git a/usr/lib/qt5/qml/com/jolla/settings/qmldir b/usr/lib/qt5/qml/com/jolla/settings/qmldir index 3f85731e..906035fb 100644 --- a/usr/lib/qt5/qml/com/jolla/settings/qmldir +++ b/usr/lib/qt5/qml/com/jolla/settings/qmldir @@ -18,3 +18,4 @@ SettingsErrorNotification 1.0 SettingsErrorNotification.qml SettingsTabItem 1.0 SettingsTabItem.qml ApplicationSettings 1.0 ApplicationSettings.qml PermissionsSection 1.0 PermissionsSection.qml +ApplicationsModel 1.0 ApplicationsModel.qml diff --git a/usr/lib/qt5/qml/com/jolla/settings/sync/SyncEndpointDelegate.qml b/usr/lib/qt5/qml/com/jolla/settings/sync/SyncEndpointDelegate.qml new file mode 100644 index 00000000..4103f0d2 --- /dev/null +++ b/usr/lib/qt5/qml/com/jolla/settings/sync/SyncEndpointDelegate.qml @@ -0,0 +1,110 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import com.jolla.settings.sync 1.0 + +Item { + id: root + + property QtObject endpointData + property bool interactive + + property string identifier + property string icon: endpointData ? endpointData.icon : "" + property string name: endpointData ? endpointData.name : "" + property var lastSync: endpointData ? endpointData.lastSync: undefined + property int status: endpointData ? endpointData.status : SyncEndpoint.UnknownStatus + + function _statusText() { + switch (status) { + case SyncEndpoint.Queued: + //: Displayed when the sync operation is currently queued + //% "Waiting to sync" + return qsTrId("settings_sync-la-sync_waiting") + case SyncEndpoint.Syncing: + // (Note: revert to this 'Syncing' text when we can sync more than just contacts) + //: Displayed when the sync operation is in progress + //% "Syncing" + var s = qsTrId("settings_sync-la-syncing") + + //: Displayed when in the process of transferring contacts from another device + //% "Receiving contacts" + return qsTrId("settings_sync-la-download_contacts") + case SyncEndpoint.UnknownStatus: + case SyncEndpoint.Succeeded: + if (lastSync && lastSync.toString() !== "Invalid Date") { + //: Shows the last time this sync operation was completed successfully + //% "Last sync: %1" + return qsTrId("settings_sync-la-last_sync_date").arg(Format.formatDate(lastSync, Format.TimepointRelativeCurrentDay)) + } + if (status == SyncEndpoint.Succeeded) { + //: Displayed if the device successfully synced to the endpoint + //% "Success" + return qsTrId("settings_sync-la-success") + } + return "" + case SyncEndpoint.Failed: + //: Displayed if the sync operation failed + //% "Sync failed" + return qsTrId("settings_sync-la-sync_failed") + case SyncEndpoint.Canceled: + //: Displayed if the sync operation was canceled + //% "Sync canceled" + return qsTrId("settings_sync-la-sync_canceled") + default: + return "unknown" + } + } + + width: parent.width + height: Theme.itemSizeLarge + + Image { + id: iconImage + x: Theme.horizontalPageMargin + y: parent.height/2 - height/2 + source: root.icon + } + + Label { + id: deviceNameLabel + anchors { + left: iconImage.right + leftMargin: Theme.paddingMedium + right: busySpinner.running ? busySpinner.left : parent.right + rightMargin: Theme.paddingLarge + verticalCenter: parent.verticalCenter + verticalCenterOffset: syncStatusLabel.text !== "" ? -syncStatusLabel.implicitHeight/2 : 0 + } + color: root.interactive ? Theme.primaryColor: Theme.highlightColor + truncationMode: TruncationMode.Fade + text: root.name.length + ? root.name + //: Default text for a Bluetooth device that does not have a name + //% "Unnamed device" + : qsTrId("settings_sync-la-unnamed_device") + } + + Label { + id: syncStatusLabel + anchors { + top: deviceNameLabel.bottom + left: deviceNameLabel.left + right: busySpinner.running ? busySpinner.left : parent.right + rightMargin: busySpinner.running ? Theme.paddingMedium : Theme.horizontalPageMargin + } + truncationMode: TruncationMode.Fade + color: root.status == SyncEndpoint.Failed ? Theme.highlightColor : Theme.secondaryColor + text: root._statusText() + } + + BusyIndicator { + id: busySpinner + running: root.status == SyncEndpoint.Syncing || root.status == SyncEndpoint.Queued + size: BusyIndicatorSize.Medium + anchors { + verticalCenter: iconImage.verticalCenter + right: parent.right + rightMargin: Theme.horizontalPageMargin + } + } +} diff --git a/usr/lib/qt5/qml/com/jolla/settings/sync/SyncEndpointList.qml b/usr/lib/qt5/qml/com/jolla/settings/sync/SyncEndpointList.qml new file mode 100644 index 00000000..7a18f5a9 --- /dev/null +++ b/usr/lib/qt5/qml/com/jolla/settings/sync/SyncEndpointList.qml @@ -0,0 +1,102 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import com.jolla.settings.sync 1.0 + +SilicaListView { + id: root + + signal endpointClicked(string identifier) + signal syncClicked(string identifier) + signal cancelClicked(string identifier) + signal removeClicked(string identifier) + + // (This context menu entry is not required at the moment, but keep the text here to be translated) + //: Triggers sync with the sync endpoint + //% "Sync" + property string _syncText: qsTrId("settings_sync-me-sync_endpoint") + + model: SyncEndpointModel { } + delegate: delegateComponent + + VerticalScrollDecorator {} + + Component { + id: delegateComponent + + ListItem { + property bool syncInProgress: model.status == SyncEndpoint.Syncing || model.status == SyncEndpoint.Queued + property bool _prevSyncInProgress + + width: parent.width + contentHeight: syncEndpointDelegate.height + menu: menuComponent + openMenuOnPressAndHold: false + + Component.onCompleted: { + _prevSyncInProgress = syncInProgress + } + + SyncEndpointDelegate { + id: syncEndpointDelegate + interactive: true + identifier: model.identifier + icon: model.icon + name: model.name + lastSync: model.lastSync + status: model.status + } + + onSyncInProgressChanged: { + if (_prevSyncInProgress != syncInProgress) { + closeMenu() + } + _prevSyncInProgress = syncInProgress + } + onClicked: { + if (syncInProgress) { + openMenu() + } else { + root.syncClicked(model.identifier) + } + } + onPressAndHold: { + if (!syncInProgress) { + openMenu() + } + } + ListView.onRemove: animateRemoval() + + function removeEndpoint(index) { + //: Removing this sync endpoint in 5 seconds + //% "Removing" + remorseAction(qsTrId("settings_sync-la-removing"), + function() { removeClicked(index) }) + } + + Component { + id: menuComponent + ContextMenu { + MenuItem { + //: Stops the data synchronization that is in progress + //% "Stop" + text: qsTrId("settings_sync-la-stop_sync") + visible: syncInProgress + onClicked: { + if (syncInProgress) { + root.cancelClicked(model.identifier) + } + } + } + + MenuItem { + //: Removes the sync endpoint + //% "Remove" + text: qsTrId("settings_sync-me-remove_endpoint") + visible: !syncInProgress + onClicked: removeEndpoint(model.identifier) + } + } + } + } + } +} diff --git a/usr/lib/qt5/qml/com/jolla/settings/sync/SyncEndpointSettingsDialog.qml b/usr/lib/qt5/qml/com/jolla/settings/sync/SyncEndpointSettingsDialog.qml new file mode 100644 index 00000000..5a0b106f --- /dev/null +++ b/usr/lib/qt5/qml/com/jolla/settings/sync/SyncEndpointSettingsDialog.qml @@ -0,0 +1,162 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import com.jolla.settings.sync 1.0 + +Dialog { + id: root + + property string identifier + property bool isNewEndpoint + property bool autoDismiss + + signal saveAndSync(var endpointProps) + signal remove(string identifier) + + onStatusChanged: { + if (status == PageStatus.Active && autoDismiss) { + pageStack.pop() + } + } + + onAutoDismissChanged: { + if (status == PageStatus.Active && autoDismiss) { + pageStack.pop() + } + } + + onAccepted: { + var direction = endpoint.direction + switch (directionCombo.currentIndex) { + case 0: + direction = SyncEndpoint.TwoWaySync + break + case 1: + direction = SyncEndpoint.DownloadSync + break + case 2: + direction = SyncEndpoint.UploadSync + break + } + var syncDataTypes = 0 + if (contactsSwitch.checked) { + syncDataTypes |= SyncEndpoint.SyncContacts + } + if (calendarsSwitch.checked) { + syncDataTypes |= SyncEndpoint.SyncCalendars + } + var endpointProps = { + "identifier": endpoint.identifier, + "direction": direction, + "syncDataTypes": syncDataTypes + } + saveAndSync(endpointProps) + } + + SyncEndpoint { + id: endpoint + identifier: root.identifier + } + + BusyIndicator { + anchors.centerIn: parent + size: BusyIndicatorSize.Large + running: root.identifier == "" + } + + SilicaFlickable { + id: flick + anchors.fill: parent + contentHeight: col.height + contentWidth: width + opacity: root.identifier == "" ? 0 : 1 + Behavior on opacity { FadeAnimation {} } + + PullDownMenu { + visible: !root.isNewEndpoint + enabled: visible + + MenuItem { + //: Removes the sync endpoint + //% "Remove" + text: qsTrId("settings_sync-me-remove_endpoint") + onClicked: { + if (root === pageStack.currentPage) { + pageStack.pop() + } + root.remove(root.identifier) + } + } + } + + DialogHeader { + id: dialogHeader + dialog: root + + // Clicking on this will save the selected settings and trigger a sync + //% "Sync" + title: qsTrId("settings_sync-he-sync") + } + + Column { + id: col + width: parent.width + anchors.top: dialogHeader.bottom + + SyncEndpointDelegate { + endpointData: endpoint + } + + TextSwitch { + id: contactsSwitch + //: Select this option to sync contacts + //% "Contacts" + text: qsTrId("settings_sync-sw-contacts") + checked: endpoint.syncDataTypes & SyncEndpoint.SyncContacts + } + + TextSwitch { + id: calendarsSwitch + //: Select this option to sync calendar events + //% "Calendar events" + text: qsTrId("settings_sync-sw-calendar_events") + checked: endpoint.syncDataTypes & SyncEndpoint.SyncCalendars + } + + ComboBox { + id: directionCombo + width: parent.width + currentIndex: { + switch (endpoint.direction) { + case SyncEndpoint.TwoWaySync: + return 0 + case SyncEndpoint.DownloadSync: + return 1 + case SyncEndpoint.UploadSync: + return 2 + } + } + + //: Determines the direction in which sync operations will be performed (two-way, upload only, or download only) + //% "Direction:" + label: qsTrId("settings_sync-la-direction") + menu: ContextMenu { + MenuItem { + //: Sync mode in which the Jolla device will send data to, and also receive data from, the other device + //% "Two-way sync" + text: qsTrId("settings_sync-la-twoway") + } + MenuItem { + //: Sync mode in which the Jolla device will receive data from the other device (but will not send any) + //% "One-way from %1" + text: qsTrId("settings_sync-la-one_way_from_remote").arg(endpoint.name) + } + MenuItem { + //: Sync mode in which the Jolla device will send data to the other device (but will not receive any data back) + //% "One-way to %1" + text: qsTrId("settings_sync-la-one_way_to_remote").arg(endpoint.name) + } + } + } + } + } +} diff --git a/usr/lib/qt5/qml/com/jolla/settings/sync/SyncSettingsPage.qml b/usr/lib/qt5/qml/com/jolla/settings/sync/SyncSettingsPage.qml new file mode 100644 index 00000000..1a5f4100 --- /dev/null +++ b/usr/lib/qt5/qml/com/jolla/settings/sync/SyncSettingsPage.qml @@ -0,0 +1,170 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Sailfish.Bluetooth 1.0 +import com.jolla.settings.sync 1.0 + +Page { + id: root + + property int _baseStackDepth: -1 + property var _objectsToDestroy: [] + + function _removeEndpoint(identifier) { + //: Deleting this account in 5 seconds + //% "Removing" + remorsePopup.execute(qsTrId("settings_sync-la-removing"), + function() { endpointManager.removeEndpoint(identifier) } ) + } + + function _startSync(identifier) { + syncingEndpointComponent.createObject(root, {"identifier": identifier}) + } + + function _finishSync(obj) { + _objectsToDestroy.push(obj) + delayedDestroyTimer.start() + } + + Timer { + id: delayedDestroyTimer + interval: 1 + onTriggered: { + var objects = root._objectsToDestroy + root._objectsToDestroy = [] + for (var i=0; i= 0 && pageStack.depth < _baseStackDepth) { + btSession.releaseSession() + _baseStackDepth = -1 + } + } + } + + Connections { + target: Qt.application + onActiveChanged: { + if (Qt.application.active) { + btSession.holdSession() + } else { + btSession.releaseSession() + } + } + } + + RemorsePopup { + id: remorsePopup + } + + SyncEndpointManager { + id: endpointManager + } + + BluetoothSession { + id: btSession + } + + Component { + id: syncingEndpointComponent + SyncEndpoint { + id: syncEndpoint + property bool _syncStarted + property bool _calledFinish + + onStatusChanged: { + if (_syncStarted && !_calledFinish + && (status == SyncEndpoint.Succeeded + || status == SyncEndpoint.Canceled + || status == SyncEndpoint.Failed)) { + root._finishSync(syncEndpoint) + _calledFinish = true + } + } + + Component.onCompleted: { + btSession.startSession() + endpointManager.triggerSync(identifier) + _syncStarted = true + } + Component.onDestruction: { + btSession.endSession() + } + } + } + + SyncEndpointList { + id: endpointsView + anchors.fill: parent + + header: PageHeader { + //: Heading of the Bluetooth Sync settings page + //% "Bluetooth sync" + title: qsTrId("settings_sync-he-bluetooth_sync") + } + + PullDownMenu { + MenuItem { + //: Initiates adding a new sync endpoint + //% "Add new device" + text: qsTrId("settings_sync-me-add_device") + onClicked: { + pageStack.push(bluetoothPickerComponent) + } + } + } + + ViewPlaceholder { + enabled: endpointsView.count == 0 + //: Description of what the Sync settings page is used for + //% "Sync enables you to get contacts from another Bluetooth device" + text: qsTrId("settings_sync-he-bluetooth_sync_contacts_download") + //: Hint to the user that they can perform a sync by using the pulley menu + //% "Pull down to add a device" + hintText: qsTrId("settings_sync-he-pull_down_to_add_device") + } + + onEndpointClicked: { + pageStack.push(settingsComponent, {"identifier": identifier}) + } + + onSyncClicked: { + root._startSync(identifier) + } + + onCancelClicked: { + endpointManager.abortSync(identifier) + } + + onRemoveClicked: { + endpointManager.removeEndpoint(identifier) + } + } + + Component { + id: bluetoothPickerComponent + BluetoothDevicePickerDialog { + requirePairing: true + excludedDevices: endpointManager.bluetoothEndpointAddresses() + preferredProfileHint: BluetoothProfiles.SyncMLServer + + onAccepted: { + var identifier = endpointManager.createBluetoothEndpoint(selectedDevice) + endpointManager.updateSyncEndpoint(identifier, + SyncEndpoint.DownloadSync, + SyncEndpoint.SyncContacts, + SyncEndpointManager.TransferAllData) + root._startSync(identifier) + } + } + } +} diff --git a/usr/lib/qt5/qml/com/jolla/settings/sync/qmldir b/usr/lib/qt5/qml/com/jolla/settings/sync/qmldir new file mode 100644 index 00000000..b7c53844 --- /dev/null +++ b/usr/lib/qt5/qml/com/jolla/settings/sync/qmldir @@ -0,0 +1,5 @@ +module com.jolla.settings.sync +plugin syncsettingsplugin +SyncSettingsPage 1.0 SyncSettingsPage.qml +SyncEndpointList 1.0 SyncEndpointList.qml +SyncEndpointSettingsPage 1.0 SyncEndpointSettingsPage.qml diff --git a/usr/lib/qt5/qml/com/jolla/settings/system/AllDateTimeSettingsDisplay.qml b/usr/lib/qt5/qml/com/jolla/settings/system/AllDateTimeSettingsDisplay.qml index e67e1ceb..09da3289 100644 --- a/usr/lib/qt5/qml/com/jolla/settings/system/AllDateTimeSettingsDisplay.qml +++ b/usr/lib/qt5/qml/com/jolla/settings/system/AllDateTimeSettingsDisplay.qml @@ -47,6 +47,17 @@ Column { } } + TextSwitch { + //% "Automatic timezone update" + text: qsTrId("settings_datetime-la-automatic_timezone_update") + automaticCheck: false + checked: dateTimeSettings.automaticTimezoneUpdate + onClicked: { + var newValue = !checked + dateTimeSettings.automaticTimezoneUpdate = newValue + } + } + CurrentTimeZoneSettingDisplay { enabled: !disabledByMdmBanner.active && (!dateTimeSettings.automaticTimezoneUpdate || root.overrideAutoUpdatedValues) diff --git a/usr/lib/qt5/qml/com/jolla/settings/system/CurrentDateSettingDisplay.qml b/usr/lib/qt5/qml/com/jolla/settings/system/CurrentDateSettingDisplay.qml index b8f86b09..0feca928 100644 --- a/usr/lib/qt5/qml/com/jolla/settings/system/CurrentDateSettingDisplay.qml +++ b/usr/lib/qt5/qml/com/jolla/settings/system/CurrentDateSettingDisplay.qml @@ -2,7 +2,7 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 import com.jolla.settings.system 1.0 import org.nemomobile.systemsettings 1.0 -import org.nemomobile.time 1.0 +import Nemo.Time 1.0 ValueButton { id: root diff --git a/usr/lib/qt5/qml/com/jolla/settings/system/CurrentTimeSettingDisplay.qml b/usr/lib/qt5/qml/com/jolla/settings/system/CurrentTimeSettingDisplay.qml index 944d9f8c..55dc3e1f 100644 --- a/usr/lib/qt5/qml/com/jolla/settings/system/CurrentTimeSettingDisplay.qml +++ b/usr/lib/qt5/qml/com/jolla/settings/system/CurrentTimeSettingDisplay.qml @@ -2,7 +2,7 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 import com.jolla.settings.system 1.0 import org.nemomobile.systemsettings 1.0 -import org.nemomobile.time 1.0 +import Nemo.Time 1.0 ValueButton { id: root diff --git a/usr/lib/qt5/qml/com/jolla/settings/system/DeviceLockFeedback.qml b/usr/lib/qt5/qml/com/jolla/settings/system/DeviceLockFeedback.qml index 7ca95552..8a7b225d 100644 --- a/usr/lib/qt5/qml/com/jolla/settings/system/DeviceLockFeedback.qml +++ b/usr/lib/qt5/qml/com/jolla/settings/system/DeviceLockFeedback.qml @@ -61,14 +61,12 @@ Connections { case AuthenticationInput.Authorize: //% "Authorize" ui.titleText = qsTrId("settings_devicelock-he-authorize") - ui.okText = confirmText ui.descriptionText = data.message || "" ui.suggestionsEnabled = false ui.requireSecurityCode = false break case AuthenticationInput.EnterSecurityCode: ui.titleText = acceptTitle - ui.okText = confirmText ui.descriptionText = data.message || "" ui.suggestionsEnabled = false ui.requireSecurityCode = true @@ -78,9 +76,10 @@ Connections { case AuthenticationInput.SuggestSecurityCode: ui.titleText = enterNewSecurityCode ui.descriptionText = data.message || "" - ui.okText = enterText ui.suggestionsEnabled = agent.codeGeneration === AuthenticationInput.OptionalCodeGeneration ui.requireSecurityCode = true + ui.validator = /^[a-zA-Z0-9 ,.!?;:&%#()=+-]*$/ + if (data.securityCode) { ui.suggestionsEnabled = true ui.suggestSecurityCode(data.securityCode) @@ -93,7 +92,6 @@ Connections { //% "Re-enter new security code" ui.titleText = qsTrId("settings_devicelock-he-reenter_new_security_code") ui.descriptionText = data.message || "" - ui.okText = enterText break case AuthenticationInput.SecurityCodesDoNotMatch: //: Shown when a new security code is entered twice for confirmation but the two entered lock codes are not the same. diff --git a/usr/lib/qt5/qml/com/jolla/settings/system/DeviceLockInput.qml b/usr/lib/qt5/qml/com/jolla/settings/system/DeviceLockInput.qml index ae40452a..22f9331c 100644 --- a/usr/lib/qt5/qml/com/jolla/settings/system/DeviceLockInput.qml +++ b/usr/lib/qt5/qml/com/jolla/settings/system/DeviceLockInput.qml @@ -23,7 +23,6 @@ PinInput { } titleText: feedbackHandler.acceptTitle - okText: feedbackHandler.confirmText subTitleText: descriptionText showOkButton: authenticationInput && authenticationInput.status === AuthenticationInput.Authenticating @@ -33,9 +32,8 @@ PinInput { minimumLength: authenticationInput ? authenticationInput.minimumCodeLength : 0 maximumLength: authenticationInput ? authenticationInput.maximumCodeLength : 64 - inputMethodHints: authenticationInput && authenticationInput.codeInputIsKeyboard - ? Qt.ImhPreferNumbers - : Qt.ImhDigitsOnly + digitInputOnly: false + enableInputMethodChange: true suggestionsEnforced: authenticationInput && authenticationInput.codeGeneration === AuthenticationInput.MandatoryCodeGeneration passwordMaskDelay: 0 diff --git a/usr/lib/qt5/qml/com/jolla/settings/system/DeviceLockInputPage.qml b/usr/lib/qt5/qml/com/jolla/settings/system/DeviceLockInputPage.qml index 9b8d0287..beed8ee4 100644 --- a/usr/lib/qt5/qml/com/jolla/settings/system/DeviceLockInputPage.qml +++ b/usr/lib/qt5/qml/com/jolla/settings/system/DeviceLockInputPage.qml @@ -16,8 +16,6 @@ Page { property alias enterNewPinText: devicelockinput.enterNewPinText property alias confirmNewPinText: devicelockinput.confirmNewPinText property alias showCancelButton: devicelockinput.showCancelButton - property alias okText: devicelockinput.okText - property alias cancelText: devicelockinput.cancelText property alias securityCode: devicelockinput.securityCode diff --git a/usr/lib/qt5/qml/com/jolla/settings/system/PinInput.qml b/usr/lib/qt5/qml/com/jolla/settings/system/PinInput.qml index b259607f..dda5a5e1 100644 --- a/usr/lib/qt5/qml/com/jolla/settings/system/PinInput.qml +++ b/usr/lib/qt5/qml/com/jolla/settings/system/PinInput.qml @@ -1,8 +1,9 @@ import QtQuick 2.6 import Sailfish.Silica 1.0 -import MeeGo.QOfono 0.2 +import QOfono 0.2 import org.nemomobile.lipstick 0.1 import org.nemomobile.ofono 1.0 +import org.nemomobile.systemsettings 1.0 FocusScope { id: root @@ -22,16 +23,6 @@ FocusScope { // modem for emergency calls property string modemPath: modemManager.defaultVoiceModem || manager.defaultModem - // okText and cancelText are no longer in use. See JB#46010 and JB#46275 - - //: Confirms the entered PIN - //% "Enter" - property string okText: qsTrId("settings_pin-bt-enter_pin") - - //: Cancels PIN entry - //% "Cancel" - property string cancelText: qsTrId("settings_pin-bt-cancel_pin") - property string titleText property color titleColor: Theme.secondaryHighlightColor property string subTitleText @@ -45,14 +36,15 @@ FocusScope { property bool dimmerBackspace property color emergencyTextColor: "#ff4d4d" - property int inputMethodHints: showDigitPad ? Qt.ImhDigitsOnly : Qt.ImhNone - property int echoMode: TextInput.Password property alias passwordMaskDelay: pinInput.passwordMaskDelay property alias _passwordCharacter: pinInput.passwordCharacter property alias _displayedPin: pinInput.displayText property string _oldPin property string _newPin + readonly property bool _validInput: pinInput.length >= minimumLength + && (maximumLength <= 0 || pinInput.length <= maximumLength) + && (!validator || validator.test(pinInput.text)) property real headingVerticalOffset @@ -60,8 +52,9 @@ FocusScope { property string _badPinWarning property string _overridingTitleText property string _emergencyWarningText - property bool lastChance + // TODO: suggestions now only for digit mode. also the properties could be refactored, + // suggestionsEnabled does not enable suggestions. JB#57962 property bool suggestionsEnabled property bool suggestionsEnforced readonly property bool _showSuggestionButton: suggestionsEnabled @@ -69,12 +62,13 @@ FocusScope { readonly property bool suggestionVisible: pinInput.length > 0 && pinInput.selectionStart !== pinInput.selectionEnd + // Allow requesting acknowledgement without needing to input pin property bool requirePin: true property bool showEmergencyButton: true //: Warns that the entered PIN was too long. - //% "PIN cannot be more than %n digits." + //% "PIN cannot be more than %n characters." property string pinLengthWarning: qsTrId("settings_pin-la-pin_max_length_warning", maximumLength) property string pinShortLengthWarning //: Enter a new PIN code @@ -109,15 +103,26 @@ FocusScope { property QtObject _feedbackEffect property QtObject _voiceCallManager + property bool enableInputMethodChange + property bool digitInputOnly: true + property var validator // regexp, doesn't prevent input but shows validation warning when not matching + + //% "Disallowed characters" + property string validationWarningText: qsTrId("settings_devicelock-la-alphanumeric_validation_warning") + property bool showDigitPad: true property bool inputEnabled: true - property bool _showDigitPad: pinInput.inputMethodHints & (Qt.ImhDigitsOnly | Qt.ImhDialableCharactersOnly) + readonly property bool _digitPadEffective: showDigitPad || emergency + property bool pasteDisabled: false + // applies only if new pin is requested via requestAndConfirmNewPin() readonly property bool _pinMismatch: (enteringNewPin && pinInput.length >= minimumLength && _newPin !== "" && _newPin !== enteredPin) - readonly property bool _inputOrCancelEnabled: inputEnabled || (showCancelButton && cancelText !== "") + readonly property bool _inputOrCancelEnabled: inputEnabled || showCancelButton // Height rule an approximation without all margins exactly. Should cover currently used device set. - readonly property bool _twoColumnMode: pageStack.currentPage.isLandscape && keypad.visible - && height < (keypad.height + headingColumn.height + pinInput.height + Theme.itemSizeSmall) + readonly property bool _twoColumnMode: pageStack.currentPage.isLandscape + && keypad.visible + && height < (keypad.height * 1.5 + Theme.itemSizeSmall / 2) + readonly property int _viewHeight: pageStack.currentPage.isLandscape ? Screen.width : Screen.height signal pinConfirmed() signal pinEntryCanceled() @@ -125,7 +130,6 @@ FocusScope { function clear() { inputEnabled = true - lastChance = false suggestionsEnabled = false enteredPin = "" @@ -188,7 +192,7 @@ FocusScope { } } - function _popPinDigit() { + function _popPinCharacter() { if (suggestionVisible) { pinInput.remove(pinInput.selectionStart, pinInput.selectionEnd) } else { @@ -196,12 +200,12 @@ FocusScope { } } - function _handleNumberPress(number) { + function _handleInputKeyPress(character) { if (root.suggestionVisible && !root.emergency) { pinInput.remove(pinInput.selectionStart, pinInput.selectionEnd) } pinInput.cursorPosition = pinInput.length - pinInput.insert(pinInput.cursorPosition, number) + pinInput.insert(pinInput.cursorPosition, character) } function _handleCancelPress() { @@ -238,6 +242,13 @@ FocusScope { } } + // virtual keyboard swipe down removes the focus, click anywhere to bring it back easily + MouseArea { + anchors.fill: parent + onClicked: { + pinInput.focus = true + } + } Rectangle { // emergency background @@ -258,32 +269,87 @@ FocusScope { source: "image://theme/icon-m-device-lock?" + headingLabel.color } + // extra close button if the keypad isn't shown + IconButton { + id: closeButton + + anchors { + right: parent.right + rightMargin: Theme.horizontalPageMargin + top: parent.top + topMargin: Math.max(Theme.paddingMedium, root.headingVerticalOffset + Theme.paddingSmall) + } + enabled: !_digitPadEffective && showCancelButton + opacity: enabled ? 1 : 0 + visible: opacity > 0 + icon.source: "image://theme/icon-m-clear" + + Behavior on opacity { FadeAnimation {} } + + onClicked: { + if (_feedbackEffect) { + _feedbackEffect.play() + } + root.pinEntryCanceled() + } + } + + IconButton { + id: inputMethodSwitch + + anchors { + left: parent.left + leftMargin: Theme.horizontalPageMargin + top: parent.top + topMargin: Math.max(Theme.paddingMedium, root.headingVerticalOffset + Theme.paddingSmall) + } + enabled: root.enableInputMethodChange && !root.emergency + opacity: enabled ? 1 : 0 + visible: opacity > 0 + icon.source: root.showDigitPad ? "image://theme/icon-m-keyboard" + : "image://theme/icon-m-dialpad" + + Behavior on opacity { FadeAnimation {} } + + onClicked: { + if (_feedbackEffect) { + _feedbackEffect.play() + } + root.showDigitPad = !root.showDigitPad + if (root.showDigitPad) { + pinInput.forceTextVisible = false + } + } + } + Column { id: headingColumn - property int availableSpace: pinInput.y + property int availableSpace: pinInput.y - headingVerticalOffset + property bool tight: pageStack.currentPage.isLandscape && Screen.width < 1.5 * keypad.height y: root._inputOrCancelEnabled || root.emergency ? availableSpace/4 + headingVerticalOffset : (parent.height / 2) - headingLabel.height - subHeadingLabel.height + x: inputMethodSwitch.enabled ? (inputMethodSwitch.x + inputMethodSwitch.width + Theme.paddingMedium) + : Theme.horizontalPageMargin width: (root._twoColumnMode ? parent.width / 2 : parent.width) - - x - (root._twoColumnMode ? Theme.paddingLarge : x) - x: Theme.horizontalPageMargin - spacing: Theme.paddingMedium + - x + - (root._twoColumnMode ? Theme.paddingLarge : x) + spacing: tight ? Theme.paddingSmall : Theme.paddingMedium Label { id: headingLabel + width: parent.width horizontalAlignment: Text.AlignHCenter wrapMode: Text.Wrap color: root.emergency ? root.emergencyTextColor - : root.lastChance - ? "#ff4956" - : root.highlightTitle - ? Theme.secondaryHighlightColor - : root.titleColor - font.pixelSize: Theme.fontSizeExtraLarge + : root.highlightTitle + ? Theme.secondaryHighlightColor + : root.titleColor + font.pixelSize: headingColumn.tight ? Theme.fontSizeLarge : Theme.fontSizeExtraLarge text: root.emergency //: Shown when user has chosen emergency call mode //% "Emergency call" @@ -298,8 +364,8 @@ FocusScope { wrapMode: Text.Wrap horizontalAlignment: Text.AlignHCenter color: headingLabel.color - visible: root._inputOrCancelEnabled || root.emergency - font.pixelSize: Theme.fontSizeLarge + visible: text !== "" || !headingColumn.tight + font.pixelSize: headingColumn.tight ? Theme.fontSizeMedium : Theme.fontSizeLarge text: root.subTitleText } @@ -310,7 +376,7 @@ FocusScope { color: root.warningTextColor visible: text !== "" - font.pixelSize: root._inputOrCancelEnabled ? Theme.fontSizeSmall : Theme.fontSizeMedium + font.pixelSize: root._inputOrCancelEnabled || headingColumn.tight ? Theme.fontSizeSmall : Theme.fontSizeMedium text: { if (root.emergency) { return root._emergencyWarningText @@ -318,6 +384,8 @@ FocusScope { return root.transientWarningText } else if (root._pinValidationWarningText !== "") { return root._pinValidationWarningText + } else if (root.validator && !root.validator.test(root.enteredPin)) { + return root.validationWarningText } else { return root.warningText } @@ -329,20 +397,30 @@ FocusScope { y: headingColumn.y + headingLabel.height + ((pinInput.y - headingColumn.y - headingLabel.height - height) / 2) running: root.busy visible: running - anchors.horizontalCenter: parent.horizontalCenter + anchors.horizontalCenter: headingColumn.horizontalCenter size: BusyIndicatorSize.Medium } TextInput { id: pinInput + // special property for the virtual keyboard to handle + property var __inputMethodExtensions: { "pasteDisabled": root.pasteDisabled, 'keyboardClosingDisabled': true } + property bool forceTextVisible readonly property bool interactive: root.emergency || (root.inputEnabled && root.requirePin && !(root.suggestionsEnabled && root.suggestionsEnforced && root.suggestionVisible)) x: Theme.horizontalPageMargin - y: root._twoColumnMode ? Math.round(parent.height * 0.75) - height - : Math.min(keypad.y, root.height - Theme.itemSizeSmall) - height - (Theme.itemSizeSmall / 2) + // two column always with keypad on the right + y: root._twoColumnMode ? root._viewHeight * 0.75 - height + : Math.min(((pageStack.currentPage.isPortrait || keypad.visible) + ? keypad.y : root._viewHeight), + (root._viewHeight + - (pageStack.currentPage.isPortrait ? Qt.inputMethod.keyboardRectangle.height + : Qt.inputMethod.keyboardRectangle.width))) + - height - (pageStack.currentPage.isLandscape ? 0 : Theme.paddingLarge) + - Theme.itemSizeSmall width: backspace.x - x - Theme.paddingSmall @@ -350,10 +428,16 @@ FocusScope { focus: true // avoid virtual keyboard - readOnly: inputMethodHints & (Qt.ImhDigitsOnly | Qt.ImhDialableCharactersOnly) + readOnly: root._digitPadEffective + onReadOnlyChanged: { + if (!readOnly) { + Qt.inputMethod.show() + } + } + enabled: interactive - echoMode: root.emergency || (root.suggestionsEnabled && root.suggestionVisible) + echoMode: root.emergency || forceTextVisible || (root.suggestionsEnabled && root.suggestionVisible) ? TextInput.Normal : TextInput.Password passwordCharacter: "\u2022" @@ -366,25 +450,14 @@ FocusScope { persistentSelection: true color: root.emergency ? "white" : root.pinDisplayColor - font.pixelSize: Theme.fontSizeHuge * 1.5 - - inputMethodHints: { - var hints = Qt.ImhNoPredictiveText - | Qt.ImhSensitiveData - | Qt.ImhNoAutoUppercase - | Qt.ImhHiddenText - | Qt.ImhMultiLine // This stops the text input hiding the keyboard when enter is pressed. - if (root.emergency - || (root.inputEnabled && root.suggestionsEnabled && root.suggestionsEnforced && root.suggestionVisible) - || (!root.inputEnabled && root.showCancelButton && root.cancelText !== "")) { - hints |= Qt.ImhDigitsOnly - } else if (root.inputEnabled) { - hints |= root.inputMethodHints - } - return hints - } - - EnterKey.enabled: length >= minimumLength + font.pixelSize: Theme.fontSizeHuge + inputMethodHints: Qt.ImhNoPredictiveText + | Qt.ImhSensitiveData + | Qt.ImhNoAutoUppercase + | Qt.ImhHiddenText + | Qt.ImhMultiLine // This stops the text input hiding the keyboard when enter is pressed. + + EnterKey.enabled: root._validInput EnterKey.iconSource: "image://theme/icon-m-enter-accept" onTextChanged: root.transientWarningText = "" @@ -393,10 +466,8 @@ FocusScope { validator: RegExpValidator { regExp: { - if (pinInput.inputMethodHints & Qt.ImhDigitsOnly) { + if (root.emergency || root.digitInputOnly) { return /[0-9]*/ - } else if (pinInput.inputMethodHints & Qt.ImhLatinOnly) { - return /[ -~¡-ÿ]*/ } else { return /.*/ } @@ -406,17 +477,23 @@ FocusScope { // readOnly property disables all key handling except return for accepting. // have some explicit handling here. also disallows moving the invisible cursor which is nice. Keys.onPressed: { + if (root.pasteDisabled && event.key === Qt.Key_V && event.modifiers & Qt.ControlModifier) { + event.accepted = true + } + if (!readOnly) { return } var text = event.text - if (text.length == 1 && "0123456789".indexOf(text) >= 0) { - _handleNumberPress(text) - } else if (event.key == Qt.Key_Escape) { + if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { + // readonly fields still have acceptance handling + } else if (event.key === Qt.Key_Escape) { _handleCancelPress() - } else if (event.key == Qt.Key_Backspace) { - _popPinDigit() + } else if (event.key === Qt.Key_Backspace) { + _popPinCharacter() + } else if (text.length === 1 && (!root.digitInputOnly || "0123456789".indexOf(text) >= 0)) { + _handleInputKeyPress(text) } } @@ -439,6 +516,8 @@ FocusScope { IconButton { id: emergencyButton + visible: deviceInfo.hasCellularVoiceCallFeature + anchors { horizontalCenter: root._inputOrCancelEnabled ? option1Button.horizontalCenter @@ -456,16 +535,30 @@ FocusScope { } } } - states: State { - when: root._twoColumnMode && root._inputOrCancelEnabled - AnchorChanges { - target: emergencyButton - anchors.left: headingColumn.left - anchors.horizontalCenter: undefined + states: [ + State { + name: "twoColumn" + when: root._twoColumnMode && root._inputOrCancelEnabled + AnchorChanges { + target: emergencyButton + anchors.left: headingColumn.left + anchors.horizontalCenter: undefined + } + }, + State { + name: "landscapeTight" + when: pageStack.currentPage.isLandscape && !keypad.visible && headingColumn.tight + AnchorChanges { + target: emergencyButton + anchors.right: closeButton.right + anchors.top: closeButton.visible ? closeButton.bottom : closeButton.top + anchors.horizontalCenter: undefined + anchors.verticalCenter: undefined + } } - } + ] - enabled: showEmergencyButton && !root.emergency && pinInput.length < 5 + enabled: visible && showEmergencyButton && !root.emergency && pinInput.length < 5 opacity: enabled ? 1 : 0 icon.source: "image://theme/icon-lockscreen-emergency-call" icon.color: undefined @@ -479,24 +572,6 @@ FocusScope { } } - IconButton { - x: Theme.itemSizeSmall - anchors.verticalCenter: pinInput.verticalCenter - height: pinInput.height + pinInput.anchors.bottomMargin - enabled: !showEmergencyButton && !_showDigitPad && pinInput.length < 5 - opacity: enabled ? 1 : 0 - icon.source: "image://theme/icon-m-close" - - Behavior on opacity { FadeAnimation {} } - - onClicked: { - if (_feedbackEffect) { - _feedbackEffect.play() - } - root.pinEntryCanceled() - } - } - IconButton { id: backspace @@ -516,9 +591,12 @@ FocusScope { height: pinInput.height + Theme.paddingMedium // increase reactive area icon { - source: root._showSuggestionButton - ? "image://theme/icon-m-reload" - : "image://theme/icon-m-backspace-keypad" + source: !keypad.visible && false + ? (pinInput.forceTextVisible ? "image://theme/icon-splus-hide-password" + : "image://theme/icon-splus-show-password") + : root._showSuggestionButton + ? "image://theme/icon-m-reload" + : "image://theme/icon-m-backspace-keypad" color: { if (root.emergency) { return Theme.lightPrimaryColor @@ -533,23 +611,26 @@ FocusScope { highlightColor: root.emergency ? emergencyTextColor : Theme.highlightColor } - opacity: root.enteredPin === "" && !root._showSuggestionButton ? 0 : 1 + opacity: keypad.visible && (root.enteredPin !== "" || root._showSuggestionButton) + ? 1 : 0 enabled: opacity Behavior on opacity { FadeAnimation {} } onClicked: { - if (root._showSuggestionButton) { + if (!keypad.visible) { + pinInput.forceTextVisible = !pinInput.forceTextVisible + } else if (root._showSuggestionButton) { root.suggestionRequested() } else { - root._popPinDigit() + root._popPinCharacter() } } onPressAndHold: { - if (root._showSuggestionButton) { + if (!keypad.visible || root._showSuggestionButton) { return } - root._popPinDigit() + root._popPinCharacter() if (pinInput.length > 0) { backspaceRepeat.start() } @@ -565,6 +646,18 @@ FocusScope { } } + IconButton { + anchors.centerIn: backspace + height: backspace.height + opacity: keypad.visible ? 0 : 1 + enabled: opacity > 0 + Behavior on opacity { FadeAnimation {} } + + icon.source: pinInput.forceTextVisible ? "image://theme/icon-splus-hide-password" + : "image://theme/icon-splus-show-password" + onClicked: pinInput.forceTextVisible = !pinInput.forceTextVisible + } + Timer { id: backspaceRepeat @@ -572,7 +665,7 @@ FocusScope { repeat: true onTriggered: { - root._popPinDigit() + root._popPinCharacter() if (pinInput.length === 0) { stop() } @@ -583,15 +676,17 @@ FocusScope { id: keypad y: root.height + pageStack.panelSize - height - - (pageStack.currentPage.isPortrait ? Math.round(parent.height/20) + - (pageStack.currentPage.isPortrait ? Math.round(Screen.height / 20) : Theme.paddingLarge) anchors.right: parent.right width: root._twoColumnMode ? parent.width / 2 : parent.width - symbolsVisible: pinInput.inputMethodHints & Qt.ImhDialableCharactersOnly + symbolsVisible: false visible: opacity > 0 - opacity: root.requirePin && pinInput.inputMethodHints & (Qt.ImhDigitsOnly | Qt.ImhDialableCharactersOnly) ? 1 : 0 + opacity: root.requirePin + && root._digitPadEffective + ? 1 : 0 textColor: { if (root.emergency) { return Theme.lightPrimaryColor @@ -613,15 +708,15 @@ FocusScope { enabled: pinInput.activeFocus onPressed: { root._feedback() - _handleNumberPress(number) + _handleInputKeyPress(number) } } PinInputOptionButton { id: option1Button + visible: (keypad.visible || !root.requirePin) - && text !== "" - && (showCancelButton || root.emergency) + && (showCancelButton || root.emergency) anchors { left: keypad.left @@ -637,7 +732,7 @@ FocusScope { ? //: Cancels out of the emergency call mode and returns to the PIN input screen //% "Cancel" qsTrId("settings_pin-bt-cancel_emergency_call") - : root.cancelText + : "" icon { visible: !root.emergency @@ -653,10 +748,16 @@ FocusScope { PinInputOptionButton { id: option2Button + property bool showIcon: !root.emergency + && (!root.requirePin + || (root._validInput + && !root._pinMismatch + && (!root.enteringNewPin || root._oldPin == "" || root._oldPin !== root.enteredPin))) + primaryColor: option1Button.primaryColor visible: (keypad.visible || !root.requirePin) - && text !== "" - && ((root.showOkButton && root.inputEnabled) || root.emergency) + && (text !== "" || showIcon) + && ((root.showOkButton && root.inputEnabled) || root.emergency) anchors { right: keypad.right @@ -667,22 +768,13 @@ FocusScope { width: option1Button.width height: icon.visible ? keypad._buttonHeight : width / 2 emergency: root.emergency - text: { - if (root.emergency) { - //: Starts the phone call - //% "Call" - return qsTrId("settings_pin-bt-start_call") - } else if (root.requirePin && (pinInput.length < minimumLength - || _pinMismatch - || (root.enteringNewPin && root._oldPin !== "" && root._oldPin === root.enteredPin))) { - return "" - } else { - return root.okText - } - } + text: root.emergency ? //: Starts the phone call + //% "Call" + qsTrId("settings_pin-bt-start_call") + : "" showWhiteBackgroundByDefault: root.emergency icon { - visible: text == root.okText + visible: showIcon source: "image://theme/icon-m-accept" } @@ -761,6 +853,11 @@ FocusScope { onActiveChanged: if (Qt.application.active) delayReset.start() } + DeviceInfo { + id: deviceInfo + readonly property bool hasCellularVoiceCallFeature: hasFeature(DeviceInfo.FeatureCellularVoice) + } + Timer { id: delayReset interval: 250 diff --git a/usr/lib/qt5/qml/com/jolla/settings/system/SimActivationPullDownMenu.qml b/usr/lib/qt5/qml/com/jolla/settings/system/SimActivationPullDownMenu.qml index 1908cd52..bcd5e620 100644 --- a/usr/lib/qt5/qml/com/jolla/settings/system/SimActivationPullDownMenu.qml +++ b/usr/lib/qt5/qml/com/jolla/settings/system/SimActivationPullDownMenu.qml @@ -2,9 +2,9 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 import Sailfish.Telephony 1.0 import Sailfish.Policy 1.0 -import MeeGo.QOfono 0.2 -import MeeGo.Connman 0.2 -import org.nemomobile.dbus 2.0 +import QOfono 0.2 +import Connman 0.2 +import Nemo.DBus 2.0 import org.nemomobile.ofono 1.0 /* diff --git a/usr/lib/qt5/qml/com/jolla/settings/system/SimPinInput.qml b/usr/lib/qt5/qml/com/jolla/settings/system/SimPinInput.qml index e2a4b98e..7e2097ad 100644 --- a/usr/lib/qt5/qml/com/jolla/settings/system/SimPinInput.qml +++ b/usr/lib/qt5/qml/com/jolla/settings/system/SimPinInput.qml @@ -1,8 +1,8 @@ import QtQuick 2.0 -import org.nemomobile.configuration 1.0 +import Nemo.Configuration 1.0 import Sailfish.Silica 1.0 import Sailfish.Telephony 1.0 -import MeeGo.QOfono 0.2 +import QOfono 0.2 PinInput { id: root diff --git a/usr/lib/qt5/qml/com/jolla/settings/system/SimPinQuery.qml b/usr/lib/qt5/qml/com/jolla/settings/system/SimPinQuery.qml index 173b5b03..c17ed4d9 100644 --- a/usr/lib/qt5/qml/com/jolla/settings/system/SimPinQuery.qml +++ b/usr/lib/qt5/qml/com/jolla/settings/system/SimPinQuery.qml @@ -7,8 +7,8 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 -import org.nemomobile.notifications 1.0 -import MeeGo.QOfono 0.2 +import Nemo.Notifications 1.0 +import QOfono 0.2 Item { id: root @@ -18,7 +18,6 @@ Item { property alias multiSimManager: pinInput.multiSimManager property alias showCancelButton: pinInput.showCancelButton property alias showBackgroundGradient: pinInput.showBackgroundGradient - property alias cancelText: pinInput.cancelText property alias emergency: pinInput.emergency property int _confirmedPinType diff --git a/usr/lib/qt5/qml/com/jolla/settings/system/SimSectionPlaceholder.qml b/usr/lib/qt5/qml/com/jolla/settings/system/SimSectionPlaceholder.qml index e8c0478b..d39eea85 100644 --- a/usr/lib/qt5/qml/com/jolla/settings/system/SimSectionPlaceholder.qml +++ b/usr/lib/qt5/qml/com/jolla/settings/system/SimSectionPlaceholder.qml @@ -1,7 +1,7 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 import Sailfish.Telephony 1.0 -import org.nemomobile.dbus 2.0 +import Nemo.DBus 2.0 /* This provides a placeholder item to be shown when a SIM is inactive or not present. diff --git a/usr/lib/qt5/qml/com/jolla/settings/system/SimViewPlaceholder.qml b/usr/lib/qt5/qml/com/jolla/settings/system/SimViewPlaceholder.qml index 286b55f8..1f8ca4c2 100644 --- a/usr/lib/qt5/qml/com/jolla/settings/system/SimViewPlaceholder.qml +++ b/usr/lib/qt5/qml/com/jolla/settings/system/SimViewPlaceholder.qml @@ -1,6 +1,6 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 -import MeeGo.QOfono 0.2 +import QOfono 0.2 /* This provides a pulley menu for activating flight mode, and also for diff --git a/usr/lib/qt5/qml/com/jolla/settings/system/SoundDialog.qml b/usr/lib/qt5/qml/com/jolla/settings/system/SoundDialog.qml index ef3c0536..790ee22d 100644 --- a/usr/lib/qt5/qml/com/jolla/settings/system/SoundDialog.qml +++ b/usr/lib/qt5/qml/com/jolla/settings/system/SoundDialog.qml @@ -92,11 +92,14 @@ Dialog { width: parent.width enabled: !noSound opacity: enabled ? 1.0 : Theme.opacityLow - onClicked: pageStack.animatorPush(musicPicker, { - acceptDestination: soundDialog.acceptDestination - || pageStack.previousPage(soundDialog), - acceptDestinationAction: PageStackAction.Pop - }) + onClicked: { + previewPlayer.stop() + pageStack.animatorPush(musicPicker, { + acceptDestination: soundDialog.acceptDestination + || pageStack.previousPage(soundDialog), + acceptDestinationAction: PageStackAction.Pop + }) + } Image { id: musicIcon diff --git a/usr/lib/qt5/qml/com/jolla/settings/system/Tones.qml b/usr/lib/qt5/qml/com/jolla/settings/system/Tones.qml index 503910e4..a7152073 100644 --- a/usr/lib/qt5/qml/com/jolla/settings/system/Tones.qml +++ b/usr/lib/qt5/qml/com/jolla/settings/system/Tones.qml @@ -35,14 +35,27 @@ Item { ToneItem { visible: modemManager.availableModems.length > 0 - //% "Ringtone" - defaultText: qsTrId("settings_sound-la-ringtone") + + defaultText: modemManager.availableModems.length === 1 + ? //% "Ringtone" + qsTrId("settings_sound-la-ringtone") + : //% "Ringtone - SIM1" + qsTrId("settings_sound-la-ringtone_sim1") //% "Current ringtone" currentText: qsTrId("settings_sound-la-current_ringtone") enabledProperty: "ringerToneEnabled" fileProperty: "ringerToneFile" } + ToneItem { + visible: modemManager.availableModems.length > 1 + //% "Ringtone - SIM2" + defaultText: qsTrId("settings_sound-la-ringtone_sim2") + currentText: qsTrId("settings_sound-la-current_ringtone") + enabledProperty: "ringerTone2Enabled" + fileProperty: "ringerTone2File" + } + /* Re-enable once we have VOIP support JB#4599 ToneItem { diff --git a/usr/lib/qt5/qml/com/jolla/settings/system/Use24HourClockSettingDisplay.qml b/usr/lib/qt5/qml/com/jolla/settings/system/Use24HourClockSettingDisplay.qml index 5fc280fe..b373b18c 100644 --- a/usr/lib/qt5/qml/com/jolla/settings/system/Use24HourClockSettingDisplay.qml +++ b/usr/lib/qt5/qml/com/jolla/settings/system/Use24HourClockSettingDisplay.qml @@ -1,6 +1,6 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 -import org.nemomobile.configuration 1.0 +import Nemo.Configuration 1.0 import org.nemomobile.systemsettings 1.0 TextSwitch { diff --git a/usr/lib/qt5/qml/com/jolla/signonuiservice/qmldir b/usr/lib/qt5/qml/com/jolla/signonuiservice/qmldir index 1b1950b7..d8681336 100644 --- a/usr/lib/qt5/qml/com/jolla/signonuiservice/qmldir +++ b/usr/lib/qt5/qml/com/jolla/signonuiservice/qmldir @@ -1,2 +1,3 @@ module com.jolla.signonuiservice plugin jollasignonuiserviceplugin +typeinfo plugins.qmltypes diff --git a/usr/lib/qt5/qml/com/jolla/startupwizard/AndroidInstallationDialog.qml b/usr/lib/qt5/qml/com/jolla/startupwizard/AndroidInstallationDialog.qml index ad9063a1..a60ddf3f 100644 --- a/usr/lib/qt5/qml/com/jolla/startupwizard/AndroidInstallationDialog.qml +++ b/usr/lib/qt5/qml/com/jolla/startupwizard/AndroidInstallationDialog.qml @@ -51,8 +51,8 @@ Dialog { opacity: applicationModel.populated ? 0 : 1 Behavior on opacity { FadeAnimation {} } - //: Heading for page that allows user to install Android™ App Support. - //% "Get Android™ App Support" + //: Heading for page that allows user to install Android™ AppSupport. + //% "Get Android™ AppSupport" text: qsTrId("startupwizard-he-get_android_app_support") } @@ -73,7 +73,7 @@ Dialog { font.family: Theme.fontFamilyHeading color: Theme.highlightColor - //: Heading for page that allows user to install Android™ App Support. + //: Heading for page that allows user to install Android™ AppSupport. //% "Do you want to use Android™ apps?" text: qsTrId("startupwizard-he-do_you_want_to_use_android_apps") } @@ -87,8 +87,8 @@ Dialog { font.pixelSize: Theme.fontSizeExtraSmall visible: applicationModel.androidSupportPackageAvailable - //: Hint to user to install Android™ App Support. - //% "If you want to use Android apps on the device, select this to install Android App Support." + //: Hint to user to install Android™ AppSupport. + //% "If you want to use Android apps on the device, select this to install Android AppSupport." text: qsTrId("startupwizard-la-install_android_support") } @@ -153,10 +153,5 @@ Dialog { } } } - - ViewPlaceholder { - // Shown only if no selections are available - enabled: applicationModel.populated && applicationModel.count == 0 - } } } diff --git a/usr/lib/qt5/qml/com/jolla/startupwizard/ApplicationInstallationDialog.qml b/usr/lib/qt5/qml/com/jolla/startupwizard/ApplicationInstallationDialog.qml index 2a1630c3..443e1dbd 100644 --- a/usr/lib/qt5/qml/com/jolla/startupwizard/ApplicationInstallationDialog.qml +++ b/usr/lib/qt5/qml/com/jolla/startupwizard/ApplicationInstallationDialog.qml @@ -135,9 +135,4 @@ Dialog { y: Math.max(root.height/2 - height/2, appGrid.headerItem.height) running: !root.applicationModel.populated } - - ViewPlaceholder { - // Shown only if no selections are available - enabled: root.applicationModel.populated && root.applicationModel.count == 0 - } } diff --git a/usr/lib/qt5/qml/com/jolla/startupwizard/ApplicationList.qml b/usr/lib/qt5/qml/com/jolla/startupwizard/ApplicationList.qml index 83daa95d..50805fbd 100644 --- a/usr/lib/qt5/qml/com/jolla/startupwizard/ApplicationList.qml +++ b/usr/lib/qt5/qml/com/jolla/startupwizard/ApplicationList.qml @@ -6,7 +6,7 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 -import org.nemomobile.dbus 2.0 +import Nemo.DBus 2.0 QtObject { property int selectionCount diff --git a/usr/lib/qt5/qml/com/jolla/startupwizard/DateTimeDialog.qml b/usr/lib/qt5/qml/com/jolla/startupwizard/DateTimeDialog.qml index 28a528db..71e55785 100644 --- a/usr/lib/qt5/qml/com/jolla/startupwizard/DateTimeDialog.qml +++ b/usr/lib/qt5/qml/com/jolla/startupwizard/DateTimeDialog.qml @@ -8,7 +8,7 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 import com.jolla.startupwizard 1.0 import org.nemomobile.systemsettings 1.0 -import org.nemomobile.configuration 1.0 +import Nemo.Configuration 1.0 import com.jolla.settings.system 1.0 Dialog { diff --git a/usr/lib/qt5/qml/com/jolla/startupwizard/PersonalizedNamingSetup.qml b/usr/lib/qt5/qml/com/jolla/startupwizard/PersonalizedNamingSetup.qml index 9879494d..5d1e5086 100644 --- a/usr/lib/qt5/qml/com/jolla/startupwizard/PersonalizedNamingSetup.qml +++ b/usr/lib/qt5/qml/com/jolla/startupwizard/PersonalizedNamingSetup.qml @@ -6,14 +6,18 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 -import MeeGo.Connman 0.2 -import Nemo.Ssu 1.1 as Ssu +import Connman 0.2 +import org.nemomobile.systemsettings 1.0 Item { id: root function personalizeBroadcastNames() { - wifiTechnology.tetheringId = Ssu.DeviceInfo.displayName(Ssu.DeviceInfo.DeviceModel) + wifiTechnology.tetheringId = deviceInfo.prettyName + } + + DeviceInfo { + id: deviceInfo } NetworkManagerFactory { diff --git a/usr/lib/qt5/qml/com/jolla/startupwizard/PleaseWaitPage.qml b/usr/lib/qt5/qml/com/jolla/startupwizard/PleaseWaitPage.qml index 3806a01f..6e344561 100644 --- a/usr/lib/qt5/qml/com/jolla/startupwizard/PleaseWaitPage.qml +++ b/usr/lib/qt5/qml/com/jolla/startupwizard/PleaseWaitPage.qml @@ -1,5 +1,5 @@ /* - * Copyright (c) 2013 - 2019 Jolla Ltd. + * Copyright (c) 2013 - 2022 Jolla Ltd. * * License: Proprietary */ @@ -8,6 +8,7 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 import com.jolla.startupwizard 1.0 import org.nemomobile.systemsettings 1.0 +import Nemo.Configuration 1.0 Page { id: root @@ -73,11 +74,21 @@ Page { Image { opacity: busyIndicator.opacity + visible: osLogoSettings.showLogo anchors { bottom: parent.bottom bottomMargin: parent.height/8 horizontalCenter: parent.horizontalCenter } - source: "image://theme/icon-os-state-update?" + startupWizardManager.defaultHighlightColor() + source: osLogoSettings.logoPath + "?" + startupWizardManager.defaultHighlightColor() + } + + ConfigurationGroup { + id: osLogoSettings + + path: "/apps/jolla-startupwizard" + + property bool showLogo: true + property string logoPath: "image://theme/graphic-os-logo" } } diff --git a/usr/lib/qt5/qml/com/jolla/startupwizard/TermsOfUseDialog.qml b/usr/lib/qt5/qml/com/jolla/startupwizard/TermsOfUseDialog.qml index 3f6efdfe..623c1219 100644 --- a/usr/lib/qt5/qml/com/jolla/startupwizard/TermsOfUseDialog.qml +++ b/usr/lib/qt5/qml/com/jolla/startupwizard/TermsOfUseDialog.qml @@ -7,7 +7,6 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 import com.jolla.startupwizard 1.0 -import com.jolla.settings.accounts 1.0 import com.jolla.settings.system 1.0 Dialog { diff --git a/usr/lib/qt5/qml/com/jolla/startupwizard/WizardPostAccountCreationDialog.qml b/usr/lib/qt5/qml/com/jolla/startupwizard/WizardPostAccountCreationDialog.qml index 38846a0a..655f0651 100644 --- a/usr/lib/qt5/qml/com/jolla/startupwizard/WizardPostAccountCreationDialog.qml +++ b/usr/lib/qt5/qml/com/jolla/startupwizard/WizardPostAccountCreationDialog.qml @@ -9,7 +9,7 @@ import QtQuick 2.0 import QtQml.Models 2.1 import Sailfish.Silica 1.0 import com.jolla.startupwizard 1.0 -import org.nemomobile.configuration 1.0 +import Nemo.Configuration 1.0 import org.nemomobile.systemsettings 1.0 import Sailfish.Accounts 1.0 import Sailfish.Store 1.0 diff --git a/usr/lib/qt5/qml/org/freedesktop/contextkit/ContextProperty.qml b/usr/lib/qt5/qml/org/freedesktop/contextkit/ContextProperty.qml new file mode 100644 index 00000000..80818c9d --- /dev/null +++ b/usr/lib/qt5/qml/org/freedesktop/contextkit/ContextProperty.qml @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2020 Open Mobile Platform LLC. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 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 + * Lesser General Public License for more details. + + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301 USA + * + * http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html + */ + +import QtQuick 2.6 +import org.freedesktop.contextkit 1.0 + +Loader { + id: root + + property string key + property var value + readonly property bool subscribed: item && item.subscribed + + property string _namespace + property string _propertyName + + function subscribe() { + if (item) { + item.subscribed = true + } + } + + function unsubscribe() { + if (item) { + item.subscribed = false + } + } + + asynchronous: true + + onKeyChanged: { + var sepIndex = key.indexOf(".") + if (sepIndex < 0) { + console.log("Error: context property key does not contain a '.' namespace qualifier:", key) + return + } + var namespace = key.substring(0, sepIndex) + _propertyName = key.substring(sepIndex + 1) + if (_namespace !== namespace) { + _namespace = namespace + setSource("/usr/share/contextkit/providers/" + _namespace + ".qml", + { "propertyName": _propertyName }) + } else if (status === Loader.Ready) { + root.item.propertyName = _propertyName + } + } + + onStatusChanged: { + if (status === Loader.Error) { + console.log("Error: unable to load context object at", source, + "for namespace '" + _namespace + "' from key '" + key + "'") + } + } + + onLoaded: { + root.value = Qt.binding(function(){ + return root.item.propertyValue + }) + } +} diff --git a/usr/lib/qt5/qml/org/freedesktop/contextkit/ContextPropertyBase.qml b/usr/lib/qt5/qml/org/freedesktop/contextkit/ContextPropertyBase.qml new file mode 100644 index 00000000..fcd32b5d --- /dev/null +++ b/usr/lib/qt5/qml/org/freedesktop/contextkit/ContextPropertyBase.qml @@ -0,0 +1,7 @@ +import QtQuick 2.6 + +Item { + property string propertyName + property var propertyValue + property bool subscribed: true +} diff --git a/usr/lib/qt5/qml/org/freedesktop/contextkit/providers/battery/qmldir b/usr/lib/qt5/qml/org/freedesktop/contextkit/providers/battery/qmldir new file mode 100644 index 00000000..bcc14e8e --- /dev/null +++ b/usr/lib/qt5/qml/org/freedesktop/contextkit/providers/battery/qmldir @@ -0,0 +1,3 @@ +module org.freedesktop.contextkit.providers.battery +plugin contextkitbatteryprovider +typeinfo plugins.qmltypes diff --git a/usr/lib/qt5/qml/org/freedesktop/contextkit/qmldir b/usr/lib/qt5/qml/org/freedesktop/contextkit/qmldir new file mode 100644 index 00000000..59d4d2d3 --- /dev/null +++ b/usr/lib/qt5/qml/org/freedesktop/contextkit/qmldir @@ -0,0 +1,5 @@ +module org.freedesktop.contextkit +plugin contextkit +typeinfo plugins.qmltypes +ContextProperty 1.0 ContextProperty.qml +ContextPropertyBase 1.0 ContextPropertyBase.qml diff --git a/usr/lib/qt5/qml/org/kde/bluezqt/DevicesModel.qml b/usr/lib/qt5/qml/org/kde/bluezqt/DevicesModel.qml index 07600243..cf2293f8 100644 --- a/usr/lib/qt5/qml/org/kde/bluezqt/DevicesModel.qml +++ b/usr/lib/qt5/qml/org/kde/bluezqt/DevicesModel.qml @@ -1,21 +1,7 @@ /* - * Copyright (C) 2015 David Rosca + * SPDX-FileCopyrightText: 2015 David Rosca * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) version 3, or any - * later version 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 defined in Section 6 of version 3 of the license. - * - * 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 - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library. If not, see . + * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL */ import org.kde.bluezqt 1.0 as BluezQt diff --git a/usr/lib/qt5/qml/org/kde/bluezqt/qmldir b/usr/lib/qt5/qml/org/kde/bluezqt/qmldir index 3c9d74e5..52ba9b60 100644 --- a/usr/lib/qt5/qml/org/kde/bluezqt/qmldir +++ b/usr/lib/qt5/qml/org/kde/bluezqt/qmldir @@ -1,4 +1,5 @@ +# This file was automatically generated by ECMQmlModule and should not be modified module org.kde.bluezqt plugin bluezqtextensionplugin - +classname BluezQtExtensionPlugin DevicesModel 1.0 DevicesModel.qml diff --git a/usr/lib/qt5/qml/org/kde/calligra/qmldir b/usr/lib/qt5/qml/org/kde/calligra/qmldir new file mode 100644 index 00000000..b504281b --- /dev/null +++ b/usr/lib/qt5/qml/org/kde/calligra/qmldir @@ -0,0 +1,3 @@ +module org.kde.calligra + +plugin CalligraComponentsPlugin diff --git a/usr/lib/qt5/qml/org/nemomobile/transferengine/qmldir b/usr/lib/qt5/qml/org/nemomobile/transferengine/qmldir index c770c025..78545ff0 100644 --- a/usr/lib/qt5/qml/org/nemomobile/transferengine/qmldir +++ b/usr/lib/qt5/qml/org/nemomobile/transferengine/qmldir @@ -1,3 +1,3 @@ module org.nemomobile.transferengine plugin declarativetransferengine - +typeinfo plugins.qmltypes diff --git a/usr/lib/qt5/qml/org/sailfishos/weather/settings/qmldir b/usr/lib/qt5/qml/org/sailfishos/weather/settings/qmldir new file mode 100644 index 00000000..fe22e7a5 --- /dev/null +++ b/usr/lib/qt5/qml/org/sailfishos/weather/settings/qmldir @@ -0,0 +1,2 @@ +module org.sailfishos.weather.settings +plugin weathersettingsplugin diff --git a/usr/share/accounts/ui/EmailBusyPage.qml b/usr/share/accounts/ui/EmailBusyPage.qml new file mode 100644 index 00000000..df4815d9 --- /dev/null +++ b/usr/share/accounts/ui/EmailBusyPage.qml @@ -0,0 +1,148 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Sailfish.Accounts 1.0 +import com.jolla.settings.accounts 1.0 +import Nemo.Email 0.1 + +AccountBusyPage { + id: busyPage + + property bool hideIncomingSettings + property bool skipping + property bool errorOccured + property string currentTask + property bool settingsRetrieved + property Item settingsDialog + + //: Save account without smtp server + //% "Save" + property string saveButtonText: qsTrId("components_accounts-bt-save_without_smtp") + + busyDescription: currentTask === "settingsDiscovery" + //: Notifies user that we are trying to retrieve the account settings + //% "Discovering account settings..." + ? qsTrId("components_accounts-la-genericemail_discovering") + //: Checking account credentials + //% "Checking account credentials..." + : qsTrId("components_accounts-la-genericemail_checking_credentials") + + function _prepareForSkip() { + infoButtonText = skipButtonText + } + + function _prepareForSkipSmtpCreation() { + hideIncomingSettings = true + infoButtonText = saveButtonText + } + + function operationSucceeded() { + errorOccured = false + if (currentTask === "checkCredentials") { + pageStack.animatorReplace(settingsDialog) + } else if (currentTask === "settingsDiscovery") { + if (!settingsRetrieved) { + pageStack.pop() + } + } + } + + function operationFailed(serverType, error) { + errorOccured = true + infoButtonText = "" + state = "info" + + if (error === EmailAccount.ConnectionError || error === EmailAccount.ExternalComunicationError) { + //: Error displayed when connection to the server can't be performed due to connection error. + //% "Connection error" + infoHeading = qsTrId("components_accounts-he-genericemail_connection_error") + _prepareForSkip() + if (serverType === EmailAccount.IncomingServer) { + //: Description displayed when connection to the incoming server can't be performed due connection error. + //% "Connection to your incoming email server failed, please check your internet connection and your server connection settings. Go back to try again or skip now and add this account later." + infoExtraDescription = qsTrId("components_accounts-la-genericemail_incoming_connection_error_description") + _prepareForSkip() + } else { + //: Description displayed when connection to the outgoing can't be performed due connection error.. + //% "Connection to your outgoing email server failed, please check your internet connection and your server connection settings. Go back to try again or save this account without a outgoing email server configuration, this account won't be available for email sending." + infoExtraDescription = qsTrId("components_accounts-la-genericemail_outgoing_connection_error_description") + _prepareForSkipSmtpCreation() + } + } else if (error === EmailAccount.DiskFull) { + //: Error displayed when account can't be saved due to device disk full. + //% "No space available" + infoHeading = qsTrId("components_accounts-he-genericemail_diskfull_error") + //: Description displayed when device disk if full and account can't be saved. + //% "Your device memory is full, please free some space in order to save this account. Go back to try again or skip now and add this account later." + infoExtraDescription = qsTrId("components_accounts-la-genericemail_diskfull_description") + _prepareForSkip() + } else if (error === EmailAccount.InvalidConfiguration || error === EmailAccount.InternalError) { + //: Error displayed when the configuration is invalid. + //% "Invalid configuration" + infoHeading = qsTrId("components_accounts-he-genericemail_invalid_configuration") + if (serverType === EmailAccount.IncomingServer) { + //: Description displayed when incoming server configuration is invalid. + //% "Go back to correct your incoming email server settings or skip now and add this account later." + infoExtraDescription = qsTrId("components_accounts-la-genericemail_invalid_configuration_description") + _prepareForSkip() + } else { + //: Description displayed when outgoing server configuration is invalid. + //% "Go back to correct your outgoing email server connection settings or save this account without a outgoing email server configuration, this account won't be available for email sending." + infoExtraDescription = qsTrId("components_accounts-la-genericemail_outgoing_authentication_failed_description") + _prepareForSkipSmtpCreation() + } + } else if (error === EmailAccount.LoginFailed) { + //: Authentication failed error. + //% "Authentication failed" + infoHeading = qsTrId("components_accounts-he-genericemail_authentication_failed") + if (serverType === EmailAccount.IncomingServer) { + //: Description displayed when authentication fails for incoming server. + //% "Go back to correct your incoming email server connection settings or skip now and add this account later." + infoExtraDescription = qsTrId("components_accounts-la-genericemail_incoming_authentication_failed_description") + _prepareForSkip() + } else { + //: Description displayed when authentication fails for outgoing server. + //% "Go back to correct your outgoing email server connection settings or save this account without a outgoing email server configuration, this account won't be available for email sending." + infoExtraDescription = qsTrId("components_accounts-la-genericemail_outgoing_authentication_failed_description") + _prepareForSkipSmtpCreation() + } + } else if (error === EmailAccount.Timeout) { + //: Error displayed when connection to the server timeout. + //% "Connection timeout" + infoHeading = qsTrId("components_accounts-he-genericemail_timeout") + if (serverType === EmailAccount.IncomingServer) { + //: Description displayed when connection to the incoming server timeout. + //% "Connection to your incoming email server timeout, please check your internet connection and your server connection settings. Go back to try again or skip now and add this account later." + infoExtraDescription = qsTrId("components_accounts-la-genericemail_timeout_incoming_description") + _prepareForSkip() + } else { + //: Description displayed when connection to the outgoing server timeout. + //% "Connection to your outgoing email server timeout, please check your internet connection your server connection settings. Go back to try again or save this account without a outgoing mail server configuration, this account won't be available for email sending." + infoExtraDescription = qsTrId("components_accounts-la-genericemail_timeout_outgoing_description") + _prepareForSkipSmtpCreation() + } + } else if (error === EmailAccount.UntrustedCertificates) { + //: Error displayed when the server certificates are untrusted. + //% "Untrusted certificates" + infoHeading = qsTrId("components_accounts-he-genericemail_untrustedCertificates") + if (serverType === EmailAccount.IncomingServer) { + //: Description displayed when incoming email server certificates are untrusted or invalid. + //% "Unable to connect to your incoming email server due to untrusted certificates. If your certificates are self-signed you can go back and accept all untrusted certificates or skip now and add this account later." + infoExtraDescription = qsTrId("components_accounts-la-genericemail_untrustedCertificates_incoming_description") + _prepareForSkip() + } else { + //: Description displayed when outgoing email server certificates are untrusted. + //% "Unable to connect to your outgoing email server due to untrusted certificates. If your certificates are self-signed you can go back and accept all untrusted certificate or continue and save this account without a outgoing mail server configuration, this account won't be available for email sending." + infoExtraDescription = qsTrId("components_accounts-la-genericemail_untrustedCertificates_outgoing_description") + _prepareForSkipSmtpCreation() + } + } else { + // InvalidAccount case + //: Error displayed when account failed to be added + //% "Oops, account could not be added" + infoHeading = qsTrId("components_accounts-he-genericemail_error") + // Account is removed at this point, don't allow back navigation + backNavigation = false + _prepareForSkip() + } + } +} diff --git a/usr/share/accounts/ui/EmailCommon.qml b/usr/share/accounts/ui/EmailCommon.qml new file mode 100644 index 00000000..830df78a --- /dev/null +++ b/usr/share/accounts/ui/EmailCommon.qml @@ -0,0 +1,336 @@ +/* + * Copyright (c) 2013 - 2020 Jolla Ltd. + * Copyright (c) 2020 Open Mobile Platform LLC. + * + * License: Proprietary + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import com.jolla.settings.accounts 1.0 + +Column { + id: root + + property bool editMode + property bool hideIncoming + property bool hideOutgoing + property bool incomingUsernameEdited + property bool incomingPasswordEdited + property bool outgoingUsernameEdited + property bool outgoingPasswordEdited + property bool checkMandatoryFields + property bool accountLimited + property alias emailAddress: emailaddress.text + property alias serverTypeIndex: incomingServerType.currentIndex + property alias incomingUsername: incomingUsernameField.text + property alias incomingPassword: incomingPasswordField.text + property alias incomingServer: incomingServerField.text + property alias incomingSecureConnectionIndex: incomingSecureConnection.currentIndex + property alias incomingPort: incomingPortField.text + property alias outgoingUsername: outgoingUsernameField.text + property alias outgoingPassword: outgoingPasswordField.text + property alias outgoingServer: outgoingServerField.text + property alias outgoingSecureConnectionIndex: outgoingSecureConnection.currentIndex + property alias outgoingPort: outgoingPortField.text + property alias outgoingRequiresAuth: outgoingRequiresAuthSwitch.checked + property alias acceptUntrustedCertificates: acceptUntrustedCertificatesSwitch.checked + + spacing: Theme.paddingLarge + width: parent.width + + function defaultIncomingPort() { + if (serverTypeIndex === 0) { + if (incomingSecureConnectionIndex === 1) { + return "993" + } else { + return "143" + } + } else { + if (incomingSecureConnectionIndex === 1) { + return "995" + } else { + return "110" + } + } + } + + function defaultOutgoingPort() { + if (outgoingSecureConnectionIndex === 1) { + return "465" + } else if (outgoingSecureConnectionIndex === 2) { + return "587" + } else { + return "25" + } + } + + GeneralEmailAddressField { + id: emailaddress + visible: !accountLimited + onTextChanged: { + if (!incomingUsernameEdited && !editMode) { + incomingUsernameField.text = text + } + } + errorHighlight: !text && checkMandatoryFields + EnterKey.iconSource: "image://theme/icon-m-enter-next" + EnterKey.onClicked: incomingUsernameField.focus = true + } + + SectionHeader { + id: incomingServerSection + visible: !hideIncoming + //: Label explaining that the following fields are for the incoming mail server + //% "Incoming mail server" + text: qsTrId("components_accounts-la-genericemail_incoming_server_label") + } + + ComboBox { + id: incomingServerType + visible: !editMode && !hideIncoming && !accountLimited + width: parent.width - Theme.paddingMedium + //: Incoming server type + //% "Server type" + label: qsTrId("components_accounts-la-genericemail_incoming_server_type") + currentIndex: 0 + + menu: ContextMenu { + MenuItem { text: "IMAP4" } + MenuItem { text: "POP3" } + onClosed: incomingUsernameField.focus = true + } + } + + TextField { + id: incomingUsernameField + visible: !hideIncoming && !accountLimited + inputMethodHints: Qt.ImhNoPredictiveText | Qt.ImhNoAutoUppercase + //: Incoming server username + //% "Username" + label: qsTrId("components_accounts-la-genericemail_incoming_username") + onTextChanged: { + if (focus) { + incomingUsernameEdited = true + } + if (!outgoingUsernameEdited && !editMode) { + outgoingUsernameField.text = text + } + } + //% "Username is required" + description: errorHighlight ? qsTrId("components_accounts-la-username_required") : "" + + errorHighlight: !text && checkMandatoryFields + EnterKey.iconSource: "image://theme/icon-m-enter-next" + EnterKey.onClicked: incomingPasswordField.focus = true + } + + PasswordField { + id: incomingPasswordField + visible: !hideIncoming + onTextChanged: { + if (focus && !incomingPasswordEdited) { + incomingPasswordEdited = true + } + if (!outgoingPasswordEdited && !editMode) { + outgoingPasswordField.text = text + } + } + //% "Password is required" + description: errorHighlight ? qsTrId("components_accounts-la-password_required") : "" + + errorHighlight: !text && checkMandatoryFields + EnterKey.iconSource: "image://theme/icon-m-enter-next" + EnterKey.onClicked: incomingServerField.focus = true + } + + TextField { + id: incomingServerField + visible: !hideIncoming && !accountLimited + inputMethodHints: Qt.ImhNoPredictiveText | Qt.ImhNoAutoUppercase + //: Incoming server address + //% "Server address" + label: qsTrId("components_accounts-la-genericemail_incoming_server") + + //% "Server address is required" + description: errorHighlight ? qsTrId("components_accounts-la-server_address_required") : "" + + errorHighlight: !text && checkMandatoryFields + EnterKey.iconSource: "image://theme/icon-m-enter-next" + EnterKey.onClicked: incomingPortField.focus = true + } + + ComboBox { + id: incomingSecureConnection + visible: !hideIncoming && !accountLimited + width: parent.width - Theme.paddingMedium + //: Incoming server secure connection + //% "Secure connection" + label: qsTrId("components_accounts-la-genericemail_incoming_secure_connection") + currentIndex: 0 + + menu: ContextMenu { + MenuItem { + //% "None" + text: qsTrId("components_accounts-la-genericemail_secure_connection_none") + } + MenuItem { text: "SSL" } + MenuItem { text: "StartTLS" } + onClosed: outgoingServerField.focus = true + } + } + + TextField { + id: incomingPortField + visible: !hideIncoming && !accountLimited + inputMethodHints: Qt.ImhDigitsOnly + //: Incoming server port + //% "Port" + label: qsTrId("components_accounts-la-genericemail_incoming_port") + text: defaultIncomingPort() + errorHighlight: !text && checkMandatoryFields + + //% "Port is required" + description: errorHighlight ? qsTrId("components_accounts-la-port_required") : "" + + EnterKey.iconSource: "image://theme/icon-m-enter-next" + EnterKey.onClicked: outgoingServerField.focus = true + } + + SectionHeader { + id: outgoingServerSection + visible: !hideOutgoing && !accountLimited && outgoingPasswordField.visible + //: Label explaining that the following fields are for the outgoing mail server + //% "Outgoing mail server" + text: qsTrId("components_accounts-la-genericemail_outgoing_server_label") + } + + ComboBox { + id: outgoingServerType + visible: !editMode && !hideOutgoing && !accountLimited + width: parent.width + //: Outgoing server type + //% "Server type" + label: qsTrId("components_accounts-la-genericemail_outgoing_server_type") + currentIndex: 0 + + menu: ContextMenu { + MenuItem { text: "SMTP" } // we only support SMTP at this time + onClosed: outgoingServerField.focus = true + } + } + + TextField { + id: outgoingServerField + visible: !hideOutgoing && !accountLimited + inputMethodHints: Qt.ImhNoPredictiveText | Qt.ImhNoAutoUppercase + //: Outgoing server address + //% "Server address" + label: qsTrId("components_accounts-la-genericemail_outgoing_server") + errorHighlight: !text && checkMandatoryFields + + //% "Server address is required" + description: errorHighlight ? qsTrId("components_accounts-la-server_address_required") : "" + + EnterKey.iconSource: "image://theme/icon-m-enter-next" + EnterKey.onClicked: outgoingPortField.focus = true + } + + ComboBox { + id: outgoingSecureConnection + visible: !hideOutgoing && !accountLimited + width: parent.width - Theme.paddingMedium + //: Outgoing server secure connection + //% "Secure connection" + label: qsTrId("components_accounts-la-genericemail_outgoing_secure_connection") + currentIndex: 0 + + menu: ContextMenu { + MenuItem { + text: qsTrId("components_accounts-la-genericemail_secure_connection_none") + } + MenuItem { text: "SSL" } + MenuItem { text: "StartTLS" } + } + } + + TextField { + id: outgoingPortField + visible: !hideOutgoing && !accountLimited + inputMethodHints: Qt.ImhDigitsOnly + //: Outgoing server port + //% "Port" + label: qsTrId("components_accounts-la-genericemail_outgoing_port") + text: defaultOutgoingPort() + errorHighlight: !text && checkMandatoryFields + + //% "Port is required" + description: errorHighlight ? qsTrId("components_accounts-la-port_required") : "" + + EnterKey.iconSource: "image://theme/icon-m-enter-close" + EnterKey.onClicked: outgoingUsernameField.visible ? outgoingUsernameField.focus = true : focus = false + } + + TextSwitch { + id: outgoingRequiresAuthSwitch + visible: !hideOutgoing && !accountLimited + checked: true + //% "Requires authentication" + text: qsTrId("components_accounts-la-genericemail_outgoing_requires_auth") + } + + TextField { + id: outgoingUsernameField + visible: !hideOutgoing && outgoingRequiresAuthSwitch.checked && !accountLimited + inputMethodHints: Qt.ImhNoPredictiveText | Qt.ImhNoAutoUppercase + //% "Username" + label: qsTrId("components_accounts-la-genericemail_outgoing_username") + errorHighlight: !text && checkMandatoryFields + EnterKey.iconSource: "image://theme/icon-m-enter-next" + EnterKey.onClicked: outgoingPasswordField.focus = true + + //% "Username is required" + description: errorHighlight ? qsTrId("components_accounts-la-username_required") : "" + + // solution for faster input, since most accounts have same credentials for + // username and password + // this can go away if we get a initial page for username/password, depends on design + onTextChanged: { + if (focus) + outgoingUsernameEdited = true + } + } + + PasswordField { + id: outgoingPasswordField + visible: !hideOutgoing && outgoingRequiresAuthSwitch.checked + errorHighlight: !text && checkMandatoryFields + onTextChanged: if (focus) outgoingPasswordEdited = true + + //% "Password is required" + description: errorHighlight ? qsTrId("components_accounts-la-password_required") : "" + + EnterKey.iconSource: "image://theme/icon-m-enter-accept" + EnterKey.onClicked: root.focus = true + } + + SectionHeader { + id: certificatesSection + visible: !accountLimited + //: Label explaining that the following fields are related to server certificates + //% "Certificates" + text: qsTrId("components_accounts-la-genericemail_certificates_label") + } + + TextSwitch { + id: acceptUntrustedCertificatesSwitch + visible: !accountLimited + checked: false + //: Accept untrusted certificates + //% "Accept untrusted certificates" + text: qsTrId("components_accounts-la-genericemail_accept_certificates") + //: Description informing the user that accepting untrusted certificates can poses potential security threats + //% "Accepting untrusted certificates poses potential security threats to your data." + description: qsTrId("components_accounts-la-genericemail_accept_certificates_description") + } +} diff --git a/usr/share/accounts/ui/EmailCryptoKeySelection.qml b/usr/share/accounts/ui/EmailCryptoKeySelection.qml new file mode 100644 index 00000000..6324bc09 --- /dev/null +++ b/usr/share/accounts/ui/EmailCryptoKeySelection.qml @@ -0,0 +1,246 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Sailfish.Secrets 1.0 as Secrets +import Sailfish.Crypto 1.0 as Crypto + +Item { + id: root + property string emailAddress + property string defaultKey + property string identity + readonly property string pluginName: cryptoCombo.currentItem ? cryptoCombo.currentItem.pluginName : "" + readonly property string keyIdentifier: cryptoCombo.currentItem ? cryptoCombo.currentItem.keyIdentifier : "" + + width: parent.width + height: Math.max(cryptoCombo.visible ? cryptoCombo.height : 0, + busyIndicator.visible ? busyIndicator.height : 0, + keyPlaceholder.visible ? keyPlaceholder.height : 0) + + ComboBox { + id: cryptoCombo + readonly property bool ready: pgpKeyFinder.status === Secrets.Request.Finished + && smimeKeyFinder.status === Secrets.Request.Finished + readonly property bool isEmpty: keyListModel.count === 0 + + opacity: (ready && !isEmpty) ? 1. : 0. + visible: opacity > 0. + Behavior on opacity { FadeAnimator {} } + //% "Outgoing emails" + label: qsTrId("settings-accounts-la-outgoing_emails") + currentIndex: 0 + menu: ContextMenu { + MenuItem { + property string pluginName: "" + property string keyIdentifier: "" + //% "No signature" + text: qsTrId("settings-accounts-mi-no_signature") + } + Repeater { + model: keyListModel + delegate: MenuItem { + id: keyDelegate + property string pluginName: model.plugin + property string keyIdentifier: model.name + text: model.displayName + Component.onCompleted: { + if (keyIdentifier == defaultKey) { + cryptoCombo.currentItem = keyDelegate + } + } + } + } + } + ListModel { + id: keyListModel + } + + Secrets.SecretManager { + id: secretManager + } + Secrets.FindSecretsRequest { + id: pgpKeyFinder + manager: secretManager + collectionName: "import" // Trick because we don't know the collection name. + filter: secretManager.constructFilterData({"email": emailAddress, + "canSign": "true"}) + filterOperator: Secrets.SecretManager.OperatorAnd + storagePluginName: "org.sailfishos.crypto.plugin.gnupg.openpgp" + Component.onCompleted: startRequest() + onIdentifiersChanged: { + for (var i = 0; i < identifiers.length; i++) { + //: %1: identifier of the signing key, usually 8 hexadecimal characters + //% "PGP key %1" + var displayName = qsTrId("settings-accounts-mi-pgp_key").arg(identifiers[i].name.slice(-8)) + keyListModel.append({"name": identifiers[i].name, + "displayName": displayName, + "plugin": "libgpgme.so"}) // QMF plugin for PGP signatures. + } + } + } + Connections { + target: keyPlaceholder.item + onKeyRingChanged: pgpKeyFinder.startRequest() + } + Secrets.FindSecretsRequest { + id: smimeKeyFinder + manager: secretManager + collectionName: "import" // Trick because we don't know the collection name. + filter: secretManager.constructFilterData({"email": emailAddress, + "canSign": "true"}) + filterOperator: Secrets.SecretManager.OperatorAnd + storagePluginName: "org.sailfishos.crypto.plugin.gnupg.smime" + Component.onCompleted: startRequest() + onIdentifiersChanged: { + for (var i = 0; i < identifiers.length; i++) { + //: %1: identifier of the signing key, usually 8 hexadecimal characters + //% "S/MIME key %1" + var displayName = qsTrId("settings-accounts-mi-smime_key").arg(identifiers[i].name.slice(-8)) + keyListModel.append({"name": identifiers[i].name, + "displayName": displayName, + "plugin": "libsmime.so"}) // QMF plugin for S/MIME signatures. + } + } + } + } + + BusyIndicator { + id: busyIndicator + size: BusyIndicatorSize.Medium + anchors.horizontalCenter: parent.horizontalCenter + visible: running + running: !cryptoCombo.visible && !keyPlaceholder.visible + } + + Loader { + id: keyPlaceholder + property alias identity: root.identity + property alias emailAddress: root.emailAddress + asynchronous: true + sourceComponent: (cryptoCombo.ready && cryptoCombo.isEmpty) + ? keyPlaceholderComponent : undefined + + width: parent.width + opacity: status == Loader.Ready && !item.busy ? 1. : 0. + visible: opacity > 0. + Behavior on opacity { FadeAnimator {} } + } + Component { + id: keyPlaceholderComponent + Column { + id: keyColumn + property bool busy: keyGenerator.status === Crypto.Request.Active + || keyImporter.status === Crypto.Request.Active + signal keyRingChanged() + Label { + //% "No key stored on the device for this email" + text: qsTrId("settings-accounts-la-no_stored_key") + wrapMode: Text.Wrap + horizontalAlignment: Text.AlignHCenter + font.pixelSize: Theme.fontSizeMedium + x: Theme.horizontalPageMargin + width: parent.width - 2 * x + color: Theme.secondaryHighlightColor + } + Item { + width: parent.width + height: Theme.paddingLarge + } + ButtonLayout { + Button { + //% "Generate" + text: qsTrId("settings-accounts-bt-generate_key") + onClicked: { + keyGenerator.startRequest() + } + } + Button { + //% "Import" + text: qsTrId("settings-accounts-bt-import_key") + onClicked: { + var picker = pageStack.push("Sailfish.Pickers.FilePickerPage", { + nameFilters: [ '*.asc', '*.gpg' ] + }) + + picker.selectedContentPropertiesChanged.connect(function() { + keyImporter.data = "file://" + picker.selectedContentProperties['filePath'] + keyImporter.startRequest() + }) + } + } + } + Item { + width: parent.width + height: Theme.paddingSmall + errorLabel.height + visible: errorLabel.text.length > 0 + Label { + id: errorLabel + wrapMode: Text.Wrap + font.pixelSize: Theme.fontSizeSmall + x: Theme.horizontalPageMargin + y: Theme.paddingSmall + width: parent.width - 2 * x + color: Theme.secondaryColor + } + } + + Crypto.CryptoManager { + id: cryptoManager + } + Crypto.GenerateStoredKeyRequest { + id: keyGenerator + manager: cryptoManager + cryptoPluginName: "org.sailfishos.crypto.plugin.gnupg.openpgp" + keyPairGenerationParameters: cryptoManager.constructRsaKeygenParams( + {"name": identity, + "email": emailAddress, + "expire": "2y"}) + keyTemplate: cryptoManager.constructKey("name", + "import", "org.sailfishos.crypto.plugin.gnupg.openpgp") + onStatusChanged: { + if (status === Crypto.Request.Finished) { + if (result.code === Crypto.Result.Succeeded) { + keyColumn.keyRingChanged() + deleteKeyHelper.startRequest() + errorLabel.text = "" + } else { + console.log(result.code) + //% "Cannot generate key: %1" + errorLabel.text = qsTrId("settings-accounts-la-key_generation_error").arg(result.errorMessage) + } + } + } + } + Crypto.ImportStoredKeyRequest { + id: keyImporter + manager: cryptoManager + cryptoPluginName: "org.sailfishos.crypto.plugin.gnupg.openpgp" + keyTemplate: cryptoManager.constructKey("name", + "import", "org.sailfishos.crypto.plugin.gnupg.openpgp") + onStatusChanged: { + if (status === Crypto.Request.Finished) { + if (result.code === Crypto.Result.Succeeded) { + keyColumn.keyRingChanged() + deleteKeyHelper.startRequest() + errorLabel.text = "" + } else { + console.log(result.code) + //% "Cannot import key: %1" + errorLabel.text = qsTrId("settings-accounts-la-key_importation_error").arg(result.errorMessage) + } + } + } + } + Crypto.DeleteStoredKeyRequest { + /* This helper is a trick to delete the cached key in the fake + "import" collection, allowing to generate a new one again + later. */ + id: deleteKeyHelper + manager: cryptoManager + identifier: cryptoManager.constructIdentifier + ("name", "import", "org.sailfishos.crypto.plugin.gnupg.openpgp") + // Ensure that import is empty on start. + Component.onCompleted: startRequest() + } + } + } +} diff --git a/usr/share/accounts/ui/EmailCryptoSection.qml b/usr/share/accounts/ui/EmailCryptoSection.qml new file mode 100644 index 00000000..f57d12e2 --- /dev/null +++ b/usr/share/accounts/ui/EmailCryptoSection.qml @@ -0,0 +1,81 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Sailfish.Crypto 1.0 + +Item { + id: root + + property alias ready: pluginChecker.ready + readonly property bool available: pluginChecker.available && emailAddress.length > 0 + + property string emailAddress + property string identity + readonly property string pluginName: comboSignature.status == Loader.Ready ? comboSignature.item.pluginName : "" + readonly property string keyIdentifier: comboSignature.status == Loader.Ready ? comboSignature.item.keyIdentifier : "" + property string defaultKey + + // Whole column is shown only if the Secrets/Crypto framework is installed + opacity: ready ? 1. : 0. + visible: opacity > 0. + Behavior on opacity { FadeAnimator {} } + + width: parent.width + height: header.height + + (comboSignature.item ? comboSignature.item.height : 0) + + (pluginChecker.visible ? pluginChecker.height : 0) + + onAvailableChanged: { + comboSignature.setSource("EmailCryptoKeySelection.qml", { + "emailAddress": emailAddress, + "identity": identity, + "defaultKey": defaultKey}) + } + + SectionHeader { + id: header + //: Email cryptographic signature settings + //% "Digital signature" + text: qsTrId("settings-accounts-he-crypto_signature") + } + + Loader { + // Combobox with available keys for this email. + id: comboSignature + width: parent.width + anchors.top: header.bottom + } + // Placeholder in case of missing plugin + Label { + id: pluginChecker + property bool ready + property bool available + + //% "No plugin for signature" + text: qsTrId("settings-accounts-la-signature_not_available") + wrapMode: Text.Wrap + horizontalAlignment: Text.AlignHCenter + font.pixelSize: Theme.fontSizeMedium + anchors.top: header.bottom + x: Theme.horizontalPageMargin + width: parent.width - 2 * x + color: Theme.secondaryHighlightColor + visible: !available + + CryptoManager { + id: cryptoManager + } + PluginInfoRequest { + manager: cryptoManager + onCryptoPluginsChanged: { + for (var i = 0; i < cryptoPlugins.length && !pluginChecker.available; i++) { + if (cryptoPlugins[i].name == "org.sailfishos.crypto.plugin.gnupg.openpgp" + && (cryptoPlugins[i].statusFlags & PluginInfo.Available)) { + pluginChecker.available = true + } + } + pluginChecker.ready = true + } + Component.onCompleted: startRequest() + } + } +} diff --git a/usr/share/accounts/ui/EmailSettingsDisplay.qml b/usr/share/accounts/ui/EmailSettingsDisplay.qml new file mode 100644 index 00000000..8169968f --- /dev/null +++ b/usr/share/accounts/ui/EmailSettingsDisplay.qml @@ -0,0 +1,542 @@ +/* + * Copyright (c) 2014 - 2020 Jolla Ltd. + * Copyright (c) 2020 Open Mobile Platform LLC. + * + * License: Proprietary + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Sailfish.Accounts 1.0 +import com.jolla.settings.accounts 1.0 +import Nemo.Configuration 1.0 +import Nemo.FileManager 1.0 +import org.nemomobile.systemsettings 1.0 + +Column { + id: root + + property string _defaultServiceName: "email" + property bool _saving + property bool _syncProfileWhenAccountSaved + property alias accountEnabled: mainAccountSettings.accountEnabled + property bool autoEnableAccount + property bool skipSmtp + property bool isNewAccount + property bool pushCapable + property bool accountIsReadOnly + property bool accountIsLimited + property bool accountIsProvisioned + property string outgoingUsername + property string incomingUsername + + property Provider accountProvider + property AccountManager accountManager + property alias account: account + property int accountId + + property QtObject _emailSyncOptions + property var _emailSyncProfileIds: [] + property AccountSyncManager _syncManager: AccountSyncManager {} + property Item settings + + signal accountSaveCompleted(var success) + + function saveAccount(blockingSave, saveSettings) { + account.enabled = mainAccountSettings.accountEnabled + account.displayName = mainAccountSettings.accountDisplayName + account.enableWithService(_defaultServiceName) + _saveEmailDetails() + + if (settingsLoader.anySyncOptionsModified() || _emailSyncOptions.modified) { + _updateProfiles(_emailSyncProfileIds, {}, _emailSyncOptions) + } + + if (saveSettings) { + saveServiceSettings() + } + + _saving = true + if (blockingSave) { + account.blockingSync() + } else { + account.sync() + } + } + + function saveNewAccount() { + account.displayName = mainAccountSettings.accountDisplayName + account.enabled = mainAccountSettings.accountEnabled + account.setConfigurationValue(_defaultServiceName, "credentialsCheck", 0) + account.setConfigurationValue(_defaultServiceName, "syncemail/profile_id", _emailSyncProfileIds[0]) + if (skipSmtp) { + account.setConfigurationValue(_defaultServiceName, "canTransmit", 0) + account.setConfigurationValue(_defaultServiceName, "smtp/smtpusername", "") + account.setConfigurationValue(_defaultServiceName, "smtp/address", "") + account.setConfigurationValue(_defaultServiceName, "smtp/server", "") + account.setConfigurationValue(_defaultServiceName, "smtp/port", 0) + account.setConfigurationValue(_defaultServiceName, "smtp/servicetype", "") + account.setConfigurationValue(_defaultServiceName, "smtp/CredentialsId", 0) + + account.removeSignInCredentials("Jolla", "smtp/CredentialsId") + } + + _updateProfiles(_emailSyncProfileIds, {}, _emailSyncOptions) + _saveEmailDetails() + root._syncProfileWhenAccountSaved = true + account.sync() + } + + function saveAccountAndSync(saveSettings) { + root._syncProfileWhenAccountSaved = true + saveAccount(false, saveSettings) + } + + function _updateProfiles(profileIds, props, syncOptions) { + if (syncOptions !== null) { + for (var i=0; i 0) { + // UI is only working for the first key in a multi-sign setting. + cryptoSection.defaultKey = ids[0] + } + } + } + + function _saveEmailDetails() { + account.setConfigurationValue(_defaultServiceName, "signatureEnabled", signatureEnabledSwitch.checked) + account.setConfigurationValue(_defaultServiceName, "signature", signatureField.text) + account.setConfigurationValue(_defaultServiceName, "fullName", yourNameField.text) + + // check if is push capable and add default folder (Inbox) + if (pushCapable) { + account.setConfigurationValue(_defaultServiceName, "imap4/pushFolders", "INBOX") + } + if (cryptoSection.keyIdentifier.length > 0 + && cryptoSection.pluginName.length > 0) { + account.setConfigurationValue(_defaultServiceName, "crypto/signByDefault", true) + account.setConfigurationValue(_defaultServiceName, "crypto/pluginName", cryptoSection.pluginName) + var serviceSettings = account.configurationValues(_defaultServiceName) + var ids = serviceSettings["crypto/keyNames"] + if (ids && ids.length > 0) { + // UI is only working for the first key in a multi-sign setting. + ids[0] = cryptoSection.keyIdentifier + account.setConfigurationValue(_defaultServiceName, "crypto/keyNames", ids) + } else { + account.setConfigurationValue(_defaultServiceName, "crypto/keyNames", [cryptoSection.keyIdentifier]) + } + } else { + account.setConfigurationValue(_defaultServiceName, "crypto/signByDefault", false) + } + + if (settings && settings.serverTypeIndex === 0) { + account.setConfigurationValue("", "folderSyncPolicy", folderSyncSettings.policy) + } + } + + function populateServiceSettings() { + var serviceSettings = account.configurationValues(_defaultServiceName) + var accountGeneralSettings = account.configurationValues("") + settings.emailAddress = serviceSettings["emailaddress"] + settings.serverTypeIndex = parseInt(serviceSettings["incomingServerType"]) + if (settings.serverTypeIndex == 0) { + settings.incomingUsername = serviceSettings["imap4/username"] + settings.incomingServer = serviceSettings["imap4/server"] + settings.incomingSecureConnectionIndex = parseInt(serviceSettings["imap4/encryption"]) + settings.incomingPort = serviceSettings["imap4/port"] + settings.acceptUntrustedCertificates = serviceSettings["imap4/acceptUntrustedCertificates"] + ? serviceSettings["imap4/acceptUntrustedCertificates"] : 0 + pushCapable = parseInt(serviceSettings["imap4/pushCapable"]) + folderSyncSettings.setPolicy(accountGeneralSettings["folderSyncPolicy"]) + } else { + settings.incomingUsername = serviceSettings["pop3/username"] + settings.incomingServer = serviceSettings["pop3/server"] + settings.incomingSecureConnectionIndex = parseInt(serviceSettings["pop3/encryption"]) + settings.incomingPort = serviceSettings["pop3/port"] + settings.acceptUntrustedCertificates = serviceSettings["pop3/acceptUntrustedCertificates"] + ? serviceSettings["pop3/acceptUntrustedCertificates"] : 0 + } + + // check if we have a valid smtp server saved + // TODO: use CanTransmit flag instead, note old accounts don't have it + var smtpService = serviceSettings["smtp/servicetype"] + if (smtpService == "sink") { + settings.outgoingUsername = serviceSettings["smtp/smtpusername"] + settings.outgoingServer = serviceSettings["smtp/server"] + settings.outgoingSecureConnectionIndex = parseInt(serviceSettings["smtp/encryption"]) + settings.outgoingPort = serviceSettings["smtp/port"] + settings.outgoingRequiresAuth = serviceSettings["smtp/authentication"] || serviceSettings["smtp/authFromCapabilities"] + + // Identity Secret can't be read from db + settings.outgoingPassword = "default" + // Avoid to update crendetials if user modifies username but ends up with same as saved + outgoingUsername = settings.outgoingUsername + } else { + skipSmtp = true + settings.hideOutgoing = true + } + + // Identity Secret can't be read from db + settings.incomingPassword = "default" + // Avoid to update crendetials if user modifies username but ends up with same as saved + incomingUsername = settings.incomingUsername + } + + function saveServiceSettings() { + account.setConfigurationValue(_defaultServiceName, "emailaddress", settings.emailAddress) + account.setConfigurationValue("", "default_credentials_username", settings.incomingUsername) + + if (settings.serverTypeIndex == 0) { + //TODO: remove incomingServerType it can't be edit, just for compatibility for old accounts + account.setConfigurationValue(_defaultServiceName, "incomingServerType", 0) + account.setConfigurationValue(_defaultServiceName, "imap4/downloadAttachments", 0) + account.setConfigurationValue(_defaultServiceName, "imap4/username", settings.incomingUsername) + account.setConfigurationValue(_defaultServiceName, "imap4/server", settings.incomingServer) + account.setConfigurationValue(_defaultServiceName, "imap4/port", settings.incomingPort) + account.setConfigurationValue(_defaultServiceName, "imap4/encryption", settings.incomingSecureConnectionIndex) + account.setConfigurationValue(_defaultServiceName, "imap4/acceptUntrustedCertificates", settings.acceptUntrustedCertificates ? 1 : 0) + } else { + account.setConfigurationValue(_defaultServiceName, "incomingServerType", 1) + account.setConfigurationValue(_defaultServiceName, "customFields/showMoreMails", "false") + account.setConfigurationValue(_defaultServiceName, "pop3/username", settings.incomingUsername) + account.setConfigurationValue(_defaultServiceName, "pop3/server", settings.incomingServer) + account.setConfigurationValue(_defaultServiceName, "pop3/port", settings.incomingPort) + account.setConfigurationValue(_defaultServiceName, "pop3/encryption", settings.incomingSecureConnectionIndex) + account.setConfigurationValue(_defaultServiceName, "pop3/acceptUntrustedCertificates", settings.acceptUntrustedCertificates ? 1 : 0) + } + if (!skipSmtp) { + account.setConfigurationValue(_defaultServiceName, "smtp/smtpusername", settings.outgoingUsername) + account.setConfigurationValue(_defaultServiceName, "smtp/address", settings.emailAddress) + account.setConfigurationValue(_defaultServiceName, "smtp/server", settings.outgoingServer) + account.setConfigurationValue(_defaultServiceName, "smtp/port", settings.outgoingPort) + account.setConfigurationValue(_defaultServiceName, "smtp/encryption", settings.outgoingSecureConnectionIndex) + // If auth is required set authFromCapabilities to true, if not set to false and also set authentication to 0 + if (!settings.outgoingRequiresAuth) { + account.setConfigurationValue(_defaultServiceName, "smtp/authentication", 0) + } + account.setConfigurationValue(_defaultServiceName, "smtp/authFromCapabilities", settings.outgoingRequiresAuth ? 1 : 0) + account.setConfigurationValue(_defaultServiceName, "smtp/acceptUntrustedCertificates", settings.acceptUntrustedCertificates ? 1 : 0) + } + } + + function _updateIncomingCredentials() { + var credentialsName + var incomingPassword + if (settings.incomingPasswordEdited) { + incomingPassword = settings.incomingPassword + settings.incomingPasswordEdited = false + } else { + incomingPassword = "" + } + credentialsName = (settings.serverTypeIndex == 0) ? "imap4/CredentialsId": "pop3/CredentialsId" + if (account.hasSignInCredentials("Jolla", credentialsName)) { + account.updateSignInCredentials("Jolla", credentialsName, + account.signInParameters(_defaultServiceName, settings.incomingUsername, incomingPassword)) + } else { + // build account configuration map, to avoid another asynchronous state round trip. + var configValues = { "": account.configurationValues("") } + var serviceNames = account.supportedServiceNames + for (var si in serviceNames) { + configValues[serviceNames[si]] = account.configurationValues(serviceNames[si]) + } + accountFactory.recreateAccountCredentials(account.identifier, _defaultServiceName, + settings.incomingUsername, incomingPassword, + account.signInParameters(_defaultServiceName, settings.incomingUsername, incomingPassword), + "Jolla", "", credentialsName, configValues) + } + } + + function _updateOutgoingCredentials() { + var outgoingPassword + if (settings.outgoingPasswordEdited) { + outgoingPassword = settings.outgoingPassword + settings.outgoingPasswordEdited = false + } else { + outgoingPassword = "" + } + var credentialsName = "smtp/CredentialsId" + if (account.hasSignInCredentials("Jolla", credentialsName)) { + account.updateSignInCredentials("Jolla", credentialsName, + account.signInParameters(_defaultServiceName, settings.outgoingUsername, outgoingPassword)) + } else { + // build account configuration map, to avoid another asynchronous state round trip. + var configValues = { "": account.configurationValues("") } + var serviceNames = account.supportedServiceNames + for (var si in serviceNames) { + configValues[serviceNames[si]] = account.configurationValues(serviceNames[si]) + } + accountFactory.recreateAccountCredentials(account.identifier, _defaultServiceName, + settings.outgoingUsername, outgoingPassword, + account.signInParameters(_defaultServiceName, settings.outgoingUsername, outgoingPassword), + "Jolla", "", credentialsName, configValues) + } + } + + width: parent.width + spacing: Theme.paddingLarge + + AccountMainSettingsDisplay { + id: mainAccountSettings + accountProvider: root.accountProvider + accountUserName: account.defaultCredentialsUserName + accountDisplayName: account.displayName + accountEnabledReadOnly: root.accountIsReadOnly || root.accountIsLimited + accountIsProvisioned: root.accountIsProvisioned + } + + AccountServiceSettingsDisplay { + id: serviceSettingsDisplay + showSectionHeader: false + autoEnableServices: root.autoEnableAccount + visible: mainAccountSettings.accountEnabled + } + + Column { + id: emailOptions + width: parent.width + visible: mainAccountSettings.accountEnabled + + SectionHeader { + //: Email details + //% "Details" + text: qsTrId("settings-accounts-la-details_email") + } + + TextField { + id: yourNameField + width: parent.width + inputMethodHints: Qt.ImhNoPredictiveText + //: Placeholder text for your name + //% "Your name" + placeholderText: qsTrId("components_accounts-ph-genericemail_your_name") + //: Your name + //% "Your name" + label: qsTrId("components_accounts-la-genericemail_your_name") + } + + TextSwitch { + id: signatureEnabledSwitch + checked: true + //: Include signature in emails + //% "Include signature" + text: qsTrId("settings-accounts-la-include_email_signature") + } + + TextArea { + id: signatureField + width: parent.width + textLeftMargin: Theme.itemSizeExtraSmall + //: Placeholder text for signature text area + //% "Write signature here" + placeholderText: qsTrId("settings-accounts-ph-email_signature") + text: { + if (settingsConf.default_signature_translation_id && settingsConf.default_signature_translation_catalog) { + var translated = Format.trId(settingsConf.default_signature_translation_id, settingsConf.default_signature_translation_catalog) + if (translated && translated != settingsConf.default_signature_translation_id) + return translated + } + + //: Default signature. %1 is an operating system name without the OS suffix + //% "Sent from my %1 device" + return qsTrId("settings_email-la-email_default_signature") + .arg(aboutSettings.baseOperatingSystemName) + } + } + + SectionHeader { + //% "Synchronization" + text: qsTrId("settings-accounts-la-sync_email_section") + } + + SyncScheduleOptions { + schedule: root._emailSyncOptions ? root._emailSyncOptions.schedule : null + isAlwaysOn: root._emailSyncOptions ? root._emailSyncOptions.syncExternallyEnabled : false + showAlwaysOn: root.pushCapable + intervalModel: EmailIntervalListModel {} + + onAlwaysOnChanged: { + root._emailSyncOptions.syncExternallyEnabled = state + } + } + + Loader { + width: parent.width + height: item ? item.height : 0 + sourceComponent: (root._emailSyncOptions + && root._emailSyncOptions.schedule.enabled + && root._emailSyncOptions.schedule.peakScheduleEnabled) ? emailPeakOptions : null + Component { + id: emailPeakOptions + PeakSyncOptions { + schedule: root._emailSyncOptions.schedule + showAlwaysOn: root.pushCapable + intervalModel: EmailIntervalListModel {} + offPeakIntervalModel: EmailOffPeakIntervalListModel {} + } + } + } + + FolderSyncSettings { + id: folderSyncSettings + width: parent.width + accountId: root.accountId + active: !root.isNewAccount && settings.serverTypeIndex === 0 // This is IMAP + } + + Loader { + id: cryptoSection + + property string defaultKey + property string emailAddress + property string identity: yourNameField.text + readonly property string pluginName: item ? item.pluginName : "" + readonly property string keyIdentifier: item ? item.keyIdentifier : "" + width: parent.width + height: item ? item.height : 0 + + onDefaultKeyChanged: if (item) {item.defaultKey = defaultKey} + onEmailAddressChanged: if (item) {item.emailAddress = emailAddress} + onIdentityChanged: if (item) {item.identity = identity} + + // This is put behind a loader in case the EmailCryptoSection.qml is not installed. + source: emailCryptoFile.exists ? emailCryptoFile.url : "" + } + + FileInfo { + id: emailCryptoFile + + url: Qt.resolvedUrl("EmailCryptoSection.qml") + } + } + + StandardAccountSettingsLoader { + id: settingsLoader + + account: root.account + accountProvider: root.accountProvider + accountManager: root.accountManager + accountSyncManager: root._syncManager + autoEnableServices: root.autoEnableAccount + + onSettingsLoaded: { + root._emailSyncProfileIds = serviceSyncProfiles["email"] + var emailOptions = allSyncOptionsForService("email") + for (var profileId in emailOptions) { + root._emailSyncOptions = emailOptions[profileId] + break + } + } + } + + AccountSyncAdapter { + id: syncAdapter + accountManager: root.accountManager + } + + Account { + id: account + + identifier: root.accountId + property bool needToUpdateIncoming + property bool needToUpdateOutgoing + + onStatusChanged: { + if (status === Account.Initialized) { + mainAccountSettings.accountEnabled = root.isNewAccount || account.enabled + _populateEmailDetails() + if (!root.isNewAccount) { + populateServiceSettings() + } + } else if (status === Account.Synced) { + // success + if (!root.isNewAccount && settings) { + if (incomingUsername != settings.incomingUsername || settings.incomingPasswordEdited) { + needToUpdateIncoming = true + } + if (!skipSmtp) { + if (outgoingUsername != settings.outgoingUsername || settings.outgoingPasswordEdited) { + needToUpdateOutgoing = true + } + } + if (needToUpdateIncoming || needToUpdateOutgoing) { + updateCredentials() + } else if (root._syncProfileWhenAccountSaved) { + root._syncProfileWhenAccountSaved = false + syncAdapter.triggerSync(account) + } + } else if (root._syncProfileWhenAccountSaved) { + root._syncProfileWhenAccountSaved = false + syncAdapter.triggerSync(account) + } + } else if (status === Account.Error) { + // display "error" dialog + } else if (status === Account.Invalid) { + // successfully deleted + } + if (root._saving && status != Account.SyncInProgress) { + root._saving = false + root.accountSaveCompleted(status == Account.Synced) + } + } + + function updateCredentials() { + if (needToUpdateIncoming) { + needToUpdateIncoming = false + incomingUsername = settings.incomingUsername + _updateIncomingCredentials() + } else if (needToUpdateOutgoing) { + needToUpdateOutgoing = false + outgoingUsername = settings.outgoingUsername + _updateOutgoingCredentials() + } + } + + onSignInCredentialsUpdated: { + // Check if we need to perform a sync after update all credentials + if (!root.isNewAccount && !needToUpdateIncoming && !needToUpdateOutgoing + && root._syncProfileWhenAccountSaved) { + root._syncProfileWhenAccountSaved = false + syncAdapter.triggerSync(account) + } + } + + onSignInError: { + console.log("Generic email provider account error:", message) + //What should be done here ????? + } + } + + ConfigurationGroup { + id: settingsConf + + path: "/apps/jolla-settings" + + property string default_signature_translation_id + property string default_signature_translation_catalog + } + + AboutSettings { + id: aboutSettings + } +} diff --git a/usr/share/accounts/ui/VkSettingsDisplay.qml b/usr/share/accounts/ui/FacebookSettingsDisplay.qml similarity index 67% rename from usr/share/accounts/ui/VkSettingsDisplay.qml rename to usr/share/accounts/ui/FacebookSettingsDisplay.qml index 9f43e37a..6e316e06 100644 --- a/usr/share/accounts/ui/VkSettingsDisplay.qml +++ b/usr/share/accounts/ui/FacebookSettingsDisplay.qml @@ -7,18 +7,30 @@ StandardAccountSettingsDisplay { id: root function _prepareForSave() { - // nothing to do. Normally we'd set up sync schedules here. + // The UI only sets the 'download new content' schedule for the calendar profile, + // so copy this schedule to other relevant non-microblog profiles. + var calendarSchedule = otherContentSchedule.schedule + var serviceNames = ["facebook-images"] + for (var i=0; i 0 + visible: text.length > 0 && model.serviceName !== "facebook-contacts" && model.serviceName !== "facebook-microblog" onCheckedChanged: { if (checked) { root.account.enableWithService(model.serviceName) @@ -87,31 +94,6 @@ StandardAccountSettingsDisplay { text: qsTrId("settings-accounts-la-download_details") } - SyncScheduleOptions { - id: feedSchedule - - property QtObject syncOptions - - //: Click to show options on how often content feed updates should be fetched from the server - //% "Download feed updates" - label: qsTrId("settings-accounts-la-download_feed_updates") - schedule: syncOptions ? syncOptions.schedule : null - } - - Loader { - width: parent.width - height: item ? item.height : 0 - sourceComponent: (feedSchedule.syncOptions - && feedSchedule.syncOptions.schedule.enabled - && feedSchedule.syncOptions.schedule.peakScheduleEnabled) ? microblogPeakOptions : null - Component { - id: microblogPeakOptions - PeakSyncOptions { - schedule: feedSchedule.syncOptions.schedule - } - } - } - SyncScheduleOptions { id: otherContentSchedule diff --git a/usr/share/accounts/ui/FolderListView.qml b/usr/share/accounts/ui/FolderListView.qml new file mode 100644 index 00000000..652be932 --- /dev/null +++ b/usr/share/accounts/ui/FolderListView.qml @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2020 Jolla Ltd. + * Copyright (c) 2020 Open Mobile Platform LLC. + * + * License: Proprietary + */ + +import QtQuick 2.6 +import Sailfish.Silica 1.0 +import Nemo.Email 0.1 + +Column { + id: root + x: Theme.horizontalPageMargin + width: parent ? parent.width - 2 * x : implicitWidth + + property alias accountId: folderModel.accountKey + property alias typeFilter: folderModel.typeFilter + readonly property alias folderCount: folderModel.count + readonly property alias syncFolderList: folderModel.syncFolderList + + FolderListFilterTypeModel { + id: folderModel + typeFilter: [ + EmailFolder.NormalFolder, + EmailFolder.InboxFolder, + EmailFolder.OutboxFolder, + EmailFolder.SentFolder, + EmailFolder.DraftsFolder, + EmailFolder.TrashFolder, + EmailFolder.JunkFolder + ] + } + + Repeater { + anchors { + left: parent.left + leftMargin: Theme.paddingLarge + right: parent.right + } + model: folderModel + delegate: Item { + width: parent.width + height: Theme.itemSizeExtraSmall + TextSwitch { + text: folderName + anchors { + left: parent.left + leftMargin: Theme.paddingLarge * folderNestingLevel + right: parent.right + verticalCenter: parent.verticalCenter + } + automaticCheck: false + checked: syncEnabled + onClicked: syncEnabled = !checked + } + } + } + + Label { + visible: folderModel.count === 0 + //% "No folders, account has not been synced yet." + text: qsTrId("settings-accounts-la-no_folder") + width: parent.width + wrapMode: Text.Wrap + color: Theme.secondaryHighlightColor + } +} diff --git a/usr/share/accounts/ui/FolderSyncPage.qml b/usr/share/accounts/ui/FolderSyncPage.qml new file mode 100644 index 00000000..d9d152e3 --- /dev/null +++ b/usr/share/accounts/ui/FolderSyncPage.qml @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2020 Jolla Ltd. + * Copyright (c) 2020 Open Mobile Platform LLC. + * + * License: Proprietary + */ + +import QtQuick 2.6 +import Sailfish.Silica 1.0 + +Page { + property alias accountId: folderListView.accountId + + SilicaFlickable { + anchors.fill: parent + contentHeight: content.height + + VerticalScrollDecorator {} + + Column { + id: content + width: parent.width + bottomPadding: Theme.paddingLarge + + PageHeader { + //% "Folders to sync" + title: qsTrId("settings_accounts-he-page_folder_sync") + } + + FolderListView { + id: folderListView + } + } + } +} diff --git a/usr/share/accounts/ui/FolderSyncSettings.qml b/usr/share/accounts/ui/FolderSyncSettings.qml new file mode 100644 index 00000000..dbd78743 --- /dev/null +++ b/usr/share/accounts/ui/FolderSyncSettings.qml @@ -0,0 +1,144 @@ +/* + * Copyright (c) 2020 Jolla Ltd. + * Copyright (c) 2020 Open Mobile Platform LLC. + * + * License: Proprietary + */ + +import QtQuick 2.6 +import Sailfish.Silica 1.0 +import com.jolla.settings.accounts 1.0 +import Nemo.Email 0.1 + +Column { + property int accountId + property bool active + property int maxFolders: 6 + readonly property string policy: comboBox.currentItem.policy + property bool _autoScroll + opacity: enabled ? 1.0 : Theme.opacityLow + + id: root + + function setPolicy(newPolicy) { + var newIndex = 0 + if (newPolicy) { + for (var i = 0; i < comboBox.menu.children.length; i++) { + if (comboBox.menu.children[i].policy === newPolicy) { + newIndex = i + break + } + } + } + + if (newIndex !== comboBox.currentIndex) { + _autoScroll = false + comboBox.currentIndex = newIndex + } + } + + ComboBox { + id: comboBox + readonly property bool isManualPolicy: currentIndex == 2 + + visible: active + //: Combobox title for syncing email folders + //% "Synced folders" + label: qsTrId("settings-accounts-la-sync_folders") + menu: ContextMenu { + MenuItem { + readonly property string policy: "inbox" + //: Syncing email folders option + //% "Inbox only" + text: qsTrId("settings-accounts-me-inbox_only") + onClicked: _autoScroll = true + } + MenuItem { + readonly property string policy: "inbox-and-subfolders" + //: Syncing email folders option + //% "Inbox and subfolders" + text: qsTrId("settings-accounts-me-inbox_all") + onClicked: _autoScroll = true + } + MenuItem { + readonly property string policy: "follow-flags" + //: Syncing email folders option (equivalent to "select folders to sync") + //% "Custom" + text: qsTrId("settings-accounts-me-custom_folders") + onClicked: _autoScroll = true + } + } + } + + Item { + width: parent.width + height: _manualVisible ? loader.height : 0 + opacity: _manualVisible ? 1.0 : 0.0 + VerticalAutoScroll.keepVisible: animation.running && _autoScroll + clip: animation.running + + readonly property bool _manualVisible: comboBox.isManualPolicy && root.active + + Behavior on height { NumberAnimation { id: animation; duration: 200; easing.type: Easing.InOutQuad } } + Behavior on opacity { FadeAnimator {} } + + Loader { + id: loader + active: parent._manualVisible || animation.running + width: parent.width + sourceComponent: Column { + readonly property bool showFolders: maxFolders < 0 || folderListView.folderCount <= maxFolders + + FolderListView { + id: folderListView + accountId: root.accountId + visible: showFolders + } + + BackgroundItem { + id: configureFolders + visible: !showFolders + height: Theme.itemSizeMedium + onClicked: pageStack.animatorPush('FolderSyncPage.qml', { accountId: accountId }) + + Icon { + anchors { + right: parent.right + rightMargin: Theme.horizontalPageMargin + verticalCenter: parent.verticalCenter + } + source: "image://theme/icon-m-add" + } + + Column { + id: configureFoldersColumn + x: Theme.horizontalPageMargin + y: Theme.paddingMedium + width: parent.width - x + + Label { + width: parent.width - Theme.itemSizeSmall - 2 * Theme.horizontalPageMargin + wrapMode: Text.Wrap + //: Please simplify to just "Custom folders" for longer translations + //% "Custom folders to sync" + text: qsTrId("settings_accounts-bu-custom_folders") + } + Label { + readonly property bool foldersSelected : folderListView.syncFolderList.length > 0 + width: parent.width - Theme.itemSizeSmall - 2 * Theme.horizontalPageMargin + font.pixelSize: Theme.fontSizeExtraSmall + color: configureFolders.highlighted ? Theme.secondaryHighlightColor : Theme.secondaryColor + truncationMode: foldersSelected ? TruncationMode.Fade : TruncationMode.None + wrapMode: foldersSelected ? Text.NoWrap : Text.Wrap + text: foldersSelected + ? folderListView.syncFolderList.join(Format.listSeparator) + //: Shown instead of the folder list in case no folders are selected + //% "No folders selected" + : qsTrId("settings_accounts-la-none_selected") + } + } + } + } + } + } +} diff --git a/usr/share/accounts/ui/GeneralEmailAddressField.qml b/usr/share/accounts/ui/GeneralEmailAddressField.qml new file mode 100644 index 00000000..fc1138e5 --- /dev/null +++ b/usr/share/accounts/ui/GeneralEmailAddressField.qml @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2020 Open Mobile Platform LLC. + * + * License: Proprietary + */ + +import QtQuick 2.6 +import Sailfish.Silica 1.0 +import com.jolla.settings.accounts 1.0 + +Column { + id: root + + property alias text: emailAddress.text + property alias errorHighlight: emailAddress.errorHighlight + + width: parent.width + + TextField { + id: emailAddress + + inputMethodHints: Qt.ImhNoPredictiveText | Qt.ImhNoAutoUppercase | Qt.ImhEmailCharactersOnly + + //% "Email address" + label: qsTrId("components_accounts-la-genericemail_email_address") + + //% "Email address is required" + description: errorHighlight ? qsTrId("components_accounts-la-email_address_required") : "" + } + + ValidatedTextInput { + textField: emailAddress + autoValidate: true + autoValidationTimeout: 100 + + onValidationRequested: { + if (emailAddress.text.toLowerCase().indexOf("@gmail.") > 0) { + //: Describes how Google users need to update security settings to allow accounts + //: to be created on Sailfish OS + //% "Gmail login requires that your Google account's security settings are set to " + //% "allow 'Less secure app access'. Note that for improved security, it is " + //% "recommended to create a 'Google' account instead of an email-only account." + progressText = qsTrId("settings_accounts-generic_google_account_creation_warning") + } else if (emailAddress.text.toLowerCase().indexOf("@yahoo.") > 0) { + //: Describes how Yahoo! users need to update security settings to allow accounts + //: to be created on Sailfish OS + //% "Yahoo! Mail login requires that your Yahoo! account's security settings are " + //% "set to allow 'Less secure app access'." + progressText = qsTrId("settings_accounts-generic_yahoo_account_creation_warning") + } else { + progressText = "" + } + } + } +} diff --git a/usr/share/accounts/ui/GoogleSettingsDisplay.qml b/usr/share/accounts/ui/GoogleSettingsDisplay.qml index b65cd985..03772f2d 100644 --- a/usr/share/accounts/ui/GoogleSettingsDisplay.qml +++ b/usr/share/accounts/ui/GoogleSettingsDisplay.qml @@ -9,7 +9,7 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 import Sailfish.Accounts 1.0 import com.jolla.settings.accounts 1.0 -import org.nemomobile.configuration 1.0 +import Nemo.Configuration 1.0 import org.nemomobile.systemsettings 1.0 StandardAccountSettingsDisplay { diff --git a/usr/share/accounts/ui/OnlineSyncAccountCreationAgent.qml b/usr/share/accounts/ui/OnlineSyncAccountCreationAgent.qml index 570e50cb..32677217 100644 --- a/usr/share/accounts/ui/OnlineSyncAccountCreationAgent.qml +++ b/usr/share/accounts/ui/OnlineSyncAccountCreationAgent.qml @@ -8,6 +8,7 @@ AccountCreationAgent { property alias provider: authDialog.accountProvider property alias services: authDialog.services + property var sharedScheduleServices: services property alias usernameLabel: authDialog.usernameLabel property alias username: authDialog.username @@ -124,6 +125,7 @@ AccountCreationAgent { accountProvider: root.accountProvider autoEnableAccount: true services: root.services + sharedScheduleServices: root.sharedScheduleServices allowCalendarRefresh: false onAccountSaveCompleted: { diff --git a/usr/share/accounts/ui/SIPCommon.qml b/usr/share/accounts/ui/SIPCommon.qml new file mode 100644 index 00000000..6f363ac1 --- /dev/null +++ b/usr/share/accounts/ui/SIPCommon.qml @@ -0,0 +1,436 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import com.jolla.settings.accounts 1.0 + +Column { + property alias account: accountField.text + property alias password: passwordField.text + + property bool editMode + property bool acceptAttempted + + property bool acceptableInput: account != "" && password != "" + + width: parent.width + + TextField { + id: accountField + + property string _tpType: 's' + property string _tpParam: "param-account" + + width: parent.width + visible: !editMode + + inputMethodHints: Qt.ImhNoPredictiveText | Qt.ImhNoAutoUppercase + errorHighlight: !text && acceptAttempted + + placeholderText: "username@sip.example.com" + + //: SIP account + //% "Account" + label: qsTrId("components_accounts-la-sip_account") + + EnterKey.iconSource: "image://theme/icon-m-enter-next" + EnterKey.onClicked: passwordField.focus = true + } + + TextField { + id: passwordField + + width: parent.width + visible: !editMode + + echoMode: TextInput.Password + errorHighlight: !text && acceptAttempted + + //: Placeholder text for password + //% "Enter password" + placeholderText: qsTrId("components_accounts-ph-sip_password_placeholder") + + //: SIP password + //% "Password" + label: qsTrId("components_accounts-la-sip_password") + + EnterKey.iconSource: "image://theme/icon-m-enter-close" + EnterKey.onClicked: focus = false + } + + TextField { + id: nicknameField + + property string _tpType: 's' + property string _tpParam: "param-alias" + + width: parent.width + + //: Placeholder text for account alias + //% "Enter nickname (optional)" + placeholderText: qsTrId("components_accounts-ph-sip_alias") + + //: User Alias + //% "Nickname" + label: qsTrId("components_accounts-la-sip_alias") + } + + SectionHeader { + //% "Advanced settings" + text: qsTrId("components_accounts-la-sip_advanced_settings-header") + } + + ComboBox { + id: transportField + + property string _tpType: "e" + property string _tpParam: "param-transport" + property string _tpDefault: 'auto' + + width:parent.width + + //% "Transport" + label: qsTrId("components_accounts-la-sip_transport") + + menu: ContextMenu { + MenuItem { + property string _tpValue: "auto" + + //% "Automatic" + text: qsTrId("components_accounts-la-sip_transport_auto") + } + MenuItem { + property string _tpValue: "udp" + + text: "UDP" + } + MenuItem { + property string _tpValue: "tcp" + + text: "TCP" + } + MenuItem { + property string _tpValue: "tls" + + text: "TLS" + } + } + } + + TextField { + id: usernameField + + property string _tpType: 's' + property string _tpParam: "param-auth-user" + + width: parent.width + + inputMethodHints: Qt.ImhNoPredictiveText | Qt.ImhNoAutoUppercase + + //: Placeholder text for username override + //% "Username" + placeholderText: qsTrId("components_accounts-ph-sip_username") + + //: Username + //% "Username" + label: qsTrId("components_accounts-la-sip_username") + } + + TextField { + id: hostField + + property string _tpType: 's' + property string _tpParam: "param-proxy-host" + + width: parent.width + + inputMethodHints: Qt.ImhNoPredictiveText | Qt.ImhNoAutoUppercase + + //: Placeholder text for account server + //% "Server" + placeholderText: qsTrId("components_accounts-ph-sip_server") + + //: Server + //% "Server" + label: qsTrId("components_accounts-la-sip_server") + } + + TextField { + id: portField + + property string _tpType: 's' + property string _tpParam: "param-port" + + width: parent.width + + inputMethodHints: Qt.ImhDigitsOnly + + //: Placeholder text for account port + //% "Port" + placeholderText: qsTrId("components_accounts-ph-sip_port") + + //: Port + //% "Port" + label: qsTrId("components_accounts-la-sip_port") + } + + //TODO: VISIBLE ONLY IF TLS IS ENABLED? + SectionHeader { + //% "Security" + text: qsTrId("components_accounts-la-sip_security-header") + } + + TextSwitch { + id:ignoreTlsErrorsField + + property string _tpType: 'b' + property string _tpParam: "param-ignore-tls-errors" + property bool _tpDefault: false + + //: Switch to ignore TLS errors + //% "Ignore TLS errors" + text: qsTrId("components_accounts-la-sip_ignore_tls_errors") + } + + TextSwitch { + id: immutableStreamsField + + property string _tpType: 'b' + property string _tpParam: "param-immutable-streams" + property bool _tpDefault: false + + //% "Enable immutable streams" + text: qsTrId("components_accounts-la-sip_immutable_streams") + } + + TextSwitch { + id: looseRoutingField + + property string _tpType: 'b' + property string _tpParam: "param-loose-routing" + property bool _tpDefault: false + + //: Switch to enable loose routing + //% "Enable loose routing" + text: qsTrId("components_accounts-la-sip_loose-routing") + } + + TextSwitch { + id: discoverBindingField + + property string _tpType: 'b' + property string _tpParam: "param-discover-binding" + property bool _tpDefault: true + + checked: _tpDefault + + //: Switch to enable binding discovery + //% "Enable binding discovery" + text: qsTrId("components_accounts-la-sip_discover_binding") + } + + TextSwitch { + id: discoverStunField + + property string _tpType: 'b' + property string _tpParam: "param-discover-stun" + property bool _tpDefault: true + + checked: _tpDefault + + //: Switch to enable STUN server discovery + //% "Discover STUN" + text: qsTrId("components_accounts-la-sip_discover_stun") + } + + SectionHeader { + visible: !discoverStunField.checked + + //% "STUN server settings" + text: qsTrId("components_accounts-la-sip_stun_server-header") + } + + TextField { + id: stunServerField + + property string _tpType: 's' + property string _tpParam: "param-stun-server" + + width: parent.width + visible: !discoverStunField.checked + + inputMethodHints: Qt.ImhNoPredictiveText | Qt.ImhNoAutoUppercase + + //% "STUN server" + placeholderText: qsTrId("components_accounts-ph-sip_stun_server") + + //% "STUN server" + label: qsTrId("components_accounts-la-sip_stun_server") + } + + TextField { + id: stunPortField + + property string _tpType: 's' + property string _tpParam: "param-stun-port" + + width: parent.width + visible: !discoverStunField.checked + + inputMethodHints: Qt.ImhDigitsOnly + + //% "Port" + placeholderText: qsTrId("components_accounts-ph-sip_stun_port") + + //% "Port" + label: qsTrId("components_accounts-la-sip_stun_port") + } + + SectionHeader { + //% "Keepalive" + text: qsTrId("components_accounts-la-sip_keepalive-header") + } + + ComboBox { + id: keepaliveMechanismField + + property string _tpType: "e" + property string _tpParam: "param-keepalive-mechanism" + property string _tpDefault: "auto" + + width:parent.width + + //% "Keep-Alive mechanism" + label: qsTrId("components_accounts-la-sip_keepalive_mechanism") + + menu: ContextMenu { + MenuItem { + property string _tpValue: "auto" + + //% "Automatic" + text: qsTrId("components_accounts-la-sip_keepalive_mechanism_auto") + } + MenuItem { + property string _tpValue: "register" + + //% "Register" + text: qsTrId("components_accounts-la-sip_keepalive_mechanism_register") + } + MenuItem { + property string _tpValue: "options" + + //% "Options" + text: qsTrId("components_accounts-la-sip_keepalive_mechanism_options") + } + MenuItem { + property string _tpValue: "stun" + + //% "STUN" + text: qsTrId("components_accounts-la-sip_keepalive_mechanism_stun") + } + MenuItem { + property string _tpValue: "off" + + //% "Disabled" + text: qsTrId("components_accounts-la-sip_keepalive_mechanism_disabled") + } + } + } + + TextField { + id: keepaliveIntervalField + + property string _tpType: 's' + property string _tpParam: "param-keepalive-interval" + property string _tpDefault: '0' + + width: parent.width + + inputMethodHints: Qt.ImhDigitsOnly + + //% "0" + placeholderText: qsTrId("components_accounts-ph-sip_keepalive_interval") + + //% "Keepalive interval" + label: qsTrId("components_accounts-la-sip_keepalive_interval") + + text: _tpDefault + } + + SectionHeader { + //% "Local Address" + text: qsTrId("components_accounts-la-sip_local_address-header") + } + + TextField { + id: localIpField + + property string _tpType: 's' + property string _tpParam: "param-local-ip-address" + + width: parent.width + + inputMethodHints: Qt.ImhDigitsOnly + + //% "Local IP" + placeholderText: qsTrId("components_accounts-ph-sip_local_ip") + + //% "Local IP" + label: qsTrId("components_accounts-la-sip_local_ip") + } + + TextField { + id: localPortField + + property string _tpType: 's' + property string _tpParam: "param-local-port" + + width: parent.width + + inputMethodHints: Qt.ImhDigitsOnly + + //% "Local port" + placeholderText: qsTrId("components_accounts-ph-sip_local_port") + + //% "Local port" + label: qsTrId("components_accounts-la-sip_local_port") + } + + SectionHeader { + //% "Extra Auth" + text: qsTrId("components_accounts-la-sip_extra_auth-header") + } + + TextField { + id: extraAuthUserField + + property string _tpType: 's' + property string _tpParam: "param-extra-auth-user" + + width: parent.width + + inputMethodHints: Qt.ImhNoPredictiveText | Qt.ImhNoAutoUppercase + + //% "Extra auth user" + placeholderText: qsTrId("components_accounts-ph-sip_extra_auth_user") + + //% "Extra auth user" + label: qsTrId("components_accounts-la-sip_extra_auth_user") + } + + TextField { + id: extraAuthPasswordField + + property string _tpType: 's' + property string _tpParam: "param-extra-auth-passowrd" + + width: parent.width + + inputMethodHints: Qt.ImhNoPredictiveText | Qt.ImhNoAutoUppercase + echoMode: TextInput.Password + + //% "Extra auth password" + placeholderText: qsTrId("components_accounts-ph-sip_extra_auth_password") + + //% "Extra auth password" + label: qsTrId("components_accounts-la-sip_extra_auth_password") + } +} diff --git a/usr/share/accounts/ui/SIPSettingsDisplay.qml b/usr/share/accounts/ui/SIPSettingsDisplay.qml new file mode 100644 index 00000000..3d8e44a9 --- /dev/null +++ b/usr/share/accounts/ui/SIPSettingsDisplay.qml @@ -0,0 +1,136 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Sailfish.Accounts 1.0 +import com.jolla.settings.accounts 1.0 + +Column { + id: root + + property bool autoEnableAccount + property Provider accountProvider + property int accountId + property alias acceptableInput: settings.acceptableInput + + property string _defaultServiceName: "sip" + property bool _saving + + signal accountSaveCompleted(var success) + + function saveAccount(blockingSave) { + account.enabled = mainAccountSettings.accountEnabled + account.displayName = mainAccountSettings.accountDisplayName + account.enableWithService(_defaultServiceName) + + _saveServiceSettings(blockingSave) + } + + function _populateServiceSettings() { + var accountSettings = account.configurationValues(_defaultServiceName) + + for (var i = 0; i < settings.children.length; i++) { + var item = settings.children[i] + + if (!item._tpType) continue + + var tpValue = accountSettings['telepathy/' + item._tpParam] + + if (!tpValue) continue + + if (item._tpType === 's') { + item.text = tpValue; + + } else if (item._tpType === 'b') { + item.checked = tpValue + + } else if (item._tpType === 'e') { + for (var j = 0; j < item.menu.children.length; j++) { + var mi = item.menu.children[j] + + if (mi._tpValue == tpValue) { + item.currentIndex = j + break + } + } + } + } + } + + function _saveServiceSettings(blockingSave) { + account.setConfigurationValue("", "default_credentials_username", settings.account) + + for (var i = 0; i < settings.children.length; i++) { + var item = settings.children[i] + var value + + if (!item._tpType) continue + + if (item._tpType == 's') + value = item.text === '' ? null : item.text + else if (item._tpType == 'b') + value = item.checked == item._tpDefault ? null : item.checked + else if (item._tpType == 'e') + value = item.currentItem._tpValue == item._tpDefault ? null : item.currentItem._tpValue + + var tpParam = 'telepathy/' + item._tpParam + + if (value !== null) { + console.log(tpParam + ' = ' + value) + account.setConfigurationValue(_defaultServiceName, tpParam, value) + } else { + console.log(tpParam + ' (removed)') + account.removeConfigurationValue(_defaultServiceName, tpParam) + } + } + + _saving = true + if (blockingSave) { + account.blockingSync() + } else { + account.sync() + } + } + + width: parent.width + spacing: Theme.paddingLarge + + AccountMainSettingsDisplay { + id: mainAccountSettings + accountProvider: root.accountProvider + accountUserName: account.defaultCredentialsUserName + accountDisplayName: account.displayName + } + + SIPCommon { + id: settings + enabled: mainAccountSettings.accountEnabled + opacity: enabled ? 1 : 0 + editMode: true + + Behavior on opacity { FadeAnimation { } } + } + + Account { + id: account + + identifier: root.accountId + property bool needToUpdate + + onStatusChanged: { + if (status === Account.Initialized) { + mainAccountSettings.accountEnabled = root.autoEnableAccount || account.enabled + if (root.autoEnableAccount) { + enableWithService(_defaultServiceName) + } + root._populateServiceSettings() + } else if (status === Account.Error) { + // display "error" dialog + } else if (status === Account.Invalid) { + // successfully deleted + } + if (root._saving && status != Account.SyncInProgress) { + root._saving = false + root.accountSaveCompleted(status == Account.Synced) + } + } + } +} diff --git a/usr/share/accounts/ui/SailfishEasCommon.qml b/usr/share/accounts/ui/SailfishEasCommon.qml index b1d707fe..41772f08 100644 --- a/usr/share/accounts/ui/SailfishEasCommon.qml +++ b/usr/share/accounts/ui/SailfishEasCommon.qml @@ -2,7 +2,7 @@ import QtQuick 2.6 import Sailfish.Silica 1.0 import com.jolla.sailfisheas 1.0 import com.jolla.settings.accounts 1.0 -import org.nemomobile.configuration 1.0 +import Nemo.Configuration 1.0 import org.nemomobile.systemsettings 1.0 Column { diff --git a/usr/share/accounts/ui/SailfishEasConnectionSettings.qml b/usr/share/accounts/ui/SailfishEasConnectionSettings.qml index d18d0511..bba58529 100644 --- a/usr/share/accounts/ui/SailfishEasConnectionSettings.qml +++ b/usr/share/accounts/ui/SailfishEasConnectionSettings.qml @@ -22,6 +22,7 @@ Column { property alias sslCertificatePath: certificateHelper.certificatePath property alias sslCertificatePassword: certificateHelper.certificatePassphrase property alias sslCredentialsId: certificateHelper.credentialsId + property bool oauthEnabled signal certificateDataSaved(int credentialsId) signal certificateDataSaveError(string errorMessage) @@ -48,12 +49,20 @@ Column { //% "Email address is required" description: errorHighlight ? qsTrId("components_accounts-la-activesync_emailaddress_required") : "" - EnterKey.iconSource: "image://theme/icon-m-enter-next" + EnterKey.iconSource: usernameField.visible || passwordField.visible || domainField.visible || editMode + ? "image://theme/icon-m-enter-next" + : "image://theme/icon-m-enter-close" EnterKey.onClicked: { if (usernameField.visible) { usernameField.focus = true - } else { + } else if (passwordField.visible) { passwordField.focus = true + } else if (domainField.visible) { + domainField.focus = true + } else if (editMode) { + serverField.focus = true + } else { + emailaddressField.focus = false } } } @@ -71,7 +80,7 @@ Column { EnterKey.iconSource: "image://theme/icon-m-enter-next" EnterKey.onClicked: passwordField.focus = true - visible: !limitedMode + visible: !limitedMode && !oauthEnabled } PasswordField { @@ -87,6 +96,8 @@ Column { //% "Password is required" description: errorHighlight ? qsTrId("components_accounts-la-activesync_password_required") : "" + visible: !oauthEnabled + EnterKey.iconSource: domainField.visible ? "image://theme/icon-m-enter-next" : "image://theme/icon-m-enter-close" EnterKey.onClicked: { @@ -119,7 +130,7 @@ Column { domainField.focus = false } } - visible: !limitedMode + visible: !limitedMode && !oauthEnabled } Column { diff --git a/usr/share/accounts/ui/SailfishEasOofSettingsDialog.qml b/usr/share/accounts/ui/SailfishEasOofSettingsDialog.qml index ee060081..9963ceec 100644 --- a/usr/share/accounts/ui/SailfishEasOofSettingsDialog.qml +++ b/usr/share/accounts/ui/SailfishEasOofSettingsDialog.qml @@ -1,7 +1,7 @@ import QtQuick 2.6 import Sailfish.Silica 1.0 import Sailfish.Calendar 1.0 -import org.nemomobile.notifications 1.0 as SystemNotifications +import Nemo.Notifications 1.0 as SystemNotifications import com.jolla.sailfisheas 1.0 Dialog { diff --git a/usr/share/accounts/ui/SailfishEasSettings.js b/usr/share/accounts/ui/SailfishEasSettings.js index 18085ada..f9dbc0bb 100644 --- a/usr/share/accounts/ui/SailfishEasSettings.js +++ b/usr/share/accounts/ui/SailfishEasSettings.js @@ -1,26 +1,32 @@ -function saveSettings(settings) { +function saveSettings(settings, prefix) { + if (prefix === undefined) { + prefix = "sailfisheas" + } // General account.setConfigurationValue("", "conflict_policy", settings.conflictsIndex === 0 ? 1 : 0) account.setConfigurationValue("", "disable_provision", !settings.provision) account.setConfigurationValue("", "folderSyncPolicy", settings.syncPolicy) // Mail - account.setConfigurationValue("sailfisheas-email", "signature", settings.signature) - account.setConfigurationValue("sailfisheas-email", "signatureEnabled", settings.signatureEnabled) - account.setConfigurationValue("sailfisheas-email", "enabled", settings.mail) - account.setConfigurationValue("sailfisheas-email", "sync_past_time", pastTimeValueFromIndex(settings.pastTimeEmailIndex, 2, true)) + account.setConfigurationValue(prefix + "-email", "signature", settings.signature) + account.setConfigurationValue(prefix + "-email", "signatureEnabled", settings.signatureEnabled) + account.setConfigurationValue(prefix + "-email", "enabled", settings.mail) + account.setConfigurationValue(prefix + "-email", "sync_past_time", pastTimeValueFromIndex(settings.pastTimeEmailIndex, 2, true)) // Calendar - account.setConfigurationValue("sailfisheas-calendars", "name", account.displayName) - account.setConfigurationValue("sailfisheas-calendars", "enabled", settings.calendar) - account.setConfigurationValue("sailfisheas-calendars", "sync_past_time", pastTimeValueFromIndex(settings.pastTimeCalendarIndex, 4, false)) + account.setConfigurationValue(prefix + "-calendars", "name", account.displayName) + account.setConfigurationValue(prefix + "-calendars", "enabled", settings.calendar) + account.setConfigurationValue(prefix + "-calendars", "sync_past_time", pastTimeValueFromIndex(settings.pastTimeCalendarIndex, 4, false)) // Contacts - account.setConfigurationValue("sailfisheas-contacts", "enabled", settings.contacts) - account.setConfigurationValue("sailfisheas-contacts", "sync_local", settings.contacts2WaySync) + account.setConfigurationValue(prefix + "-contacts", "enabled", settings.contacts) + account.setConfigurationValue(prefix + "-contacts", "sync_local", settings.contacts2WaySync) } -function saveConnectionSettings(connectionSettings) { +function saveConnectionSettings(connectionSettings, prefix) { + if (prefix === undefined) { + prefix = "sailfisheas" + } if (connectionSettings !== null) { account.setConfigurationValue("", "connection/accept_all_certificates", connectionSettings.acceptSSLCertificates) account.setConfigurationValue("", "connection/domain", connectionSettings.domain) @@ -31,8 +37,9 @@ function saveConnectionSettings(connectionSettings) { account.setConfigurationValue("", "connection/username", connectionSettings.username) //Email address is also need in the email service, to be used as from address - account.setConfigurationValue("sailfisheas-email", "emailaddress", connectionSettings.emailaddress) + account.setConfigurationValue(prefix + "-email", "emailaddress", connectionSettings.emailaddress) + account.setConfigurationValue("", "default_credentials_username", settings.username || settings.emailaddress) account.setConfigurationValue("", "SslCertCredentialsId", connectionSettings.sslCredentialsId) account.setConfigurationValue("", "connection/ssl_certificate_path", (connectionSettings.hasSslCertificate && connectionSettings.sslCredentialsId > 0) diff --git a/usr/share/accounts/ui/SailfishEasSettingsDialog.qml b/usr/share/accounts/ui/SailfishEasSettingsDialog.qml index cf6bfd9b..11bfecfb 100644 --- a/usr/share/accounts/ui/SailfishEasSettingsDialog.qml +++ b/usr/share/accounts/ui/SailfishEasSettingsDialog.qml @@ -6,6 +6,7 @@ Dialog { property alias accountId: settingsDisplay.accountId property Item connectionSettings + property bool oauthEnabled acceptDestination: accountCreationAgent.busyPageInstance acceptDestinationAction: PageStackAction.Push @@ -30,6 +31,7 @@ Dialog { SailfishEasSettingsDisplay { id: settingsDisplay + oauthEnabled: root.oauthEnabled anchors.top: header.bottom isNewAccount: true accountManager: accountCreationAgent.accountManager diff --git a/usr/share/accounts/ui/SailfishEasSettingsDisplay.qml b/usr/share/accounts/ui/SailfishEasSettingsDisplay.qml index 7c6facfe..a30baf18 100644 --- a/usr/share/accounts/ui/SailfishEasSettingsDisplay.qml +++ b/usr/share/accounts/ui/SailfishEasSettingsDisplay.qml @@ -26,6 +26,8 @@ Column { property int _credentialsUpdateCounter property bool _saving property bool _triggerSyncWhenAccountSaved + property bool oauthEnabled + readonly property string _easPrefix: "sailfisheas" + (oauthEnabled ? "-oauth" : "") signal accountSaveCompleted(var success) @@ -37,8 +39,8 @@ Column { easSettings.setSyncPolicy(accountGlobalSettings["folderSyncPolicy"]) // Email - var accountEmailSettings = account.configurationValues("sailfisheas-email") - easSettings.mail = accountEmailSettings["enabled"] + var accountEmailSettings = account.configurationValues(root._easPrefix + "-email") + easSettings.mail = accountEmailSettings["enabled"] || root.autoEnableAccount easSettings.pastTimeEmailIndex = parseInt(accountEmailSettings["sync_past_time"]) - 1 // Email details @@ -51,14 +53,14 @@ Column { easSettings.isNewAccount = root.isNewAccount // Calendar - var accountCalendarSettings = account.configurationValues("sailfisheas-calendars") - easSettings.calendar = accountCalendarSettings["enabled"] + var accountCalendarSettings = account.configurationValues(root._easPrefix + "-calendars") + easSettings.calendar = accountCalendarSettings["enabled"] || root.autoEnableAccount var pastTimeCal = parseInt(accountCalendarSettings["sync_past_time"]) easSettings.pastTimeCalendarIndex = pastTimeCal > 3 ? pastTimeCal - 4 : 4 // Contacts - var accountContatcsSettings = account.configurationValues("sailfisheas-contacts") - easSettings.contacts = accountContatcsSettings["enabled"] + var accountContatcsSettings = account.configurationValues(root._easPrefix + "-contacts") + easSettings.contacts = accountContatcsSettings["enabled"] || root.autoEnableAccount easSettings.contacts2WaySync = accountContatcsSettings["sync_local"] // Avoid to update credentials if user modifies username but ends up with same as saved @@ -95,7 +97,7 @@ Column { account.displayName = mainAccountSettings.accountDisplayName if (saveConnectionSettings) { - ServiceSettings.saveConnectionSettings(connectionSettings) + ServiceSettings.saveConnectionSettings(connectionSettings, root._easPrefix) } if (!root.isNewAccount) { @@ -132,25 +134,25 @@ Column { function saveSettings() { console.log("[jsa-eas] Saving account settings") _saveScheduleProfile() - ServiceSettings.saveSettings(easSettings) + ServiceSettings.saveSettings(easSettings, root._easPrefix) if (root.isNewAccount) { // Save service here, since settings loader can overwrite those for new accounts if (easSettings.mail) { - account.enableWithService("sailfisheas-email") + account.enableWithService(root._easPrefix + "-email") } else { - account.disableWithService("sailfisheas-email") + account.disableWithService(root._easPrefix + "-email") } if (easSettings.calendar) { - account.enableWithService("sailfisheas-calendars") + account.enableWithService(root._easPrefix + "-calendars") } else { - account.disableWithService("sailfisheas-calendars") + account.disableWithService(root._easPrefix + "-calendars") } if (easSettings.contacts) { - account.enableWithService("sailfisheas-contacts") + account.enableWithService(root._easPrefix + "-contacts") } else { - account.disableWithService("sailfisheas-contacts") + account.disableWithService(root._easPrefix + "-contacts") } } } @@ -197,7 +199,7 @@ Column { function increaseCredentialsCounter() { _credentialsUpdateCounter++ // Save a string since double is not supported in c++ side: 'Account::setConfigurationValues(): variant type QVariant::double' - account.setConfigurationValue("sailfisheas-email", "credentials_update_counter", _credentialsUpdateCounter.toString()) + account.setConfigurationValue(root._easPrefix + "-email", "credentials_update_counter", _credentialsUpdateCounter.toString()) } CheckProvision { @@ -274,26 +276,26 @@ Column { // Load the initial settings. Each of these services only have one sync profile. var profileId = 0 // Email profile contains the main sync settings - var syncOptions = allSyncOptionsForService("sailfisheas-email") + var syncOptions = allSyncOptionsForService(root._easPrefix + "-email") for (profileId in syncOptions) { easSettings.syncScheduleOptions = syncOptions[profileId] break } // Getting email sync profile var emailProfileIds = accountSyncManager.profileIds(account.identifier, - "sailfisheas-email") + root._easPrefix + "-email") if (emailProfileIds.length > 0 && emailProfileIds[0] !== "") { root._emailProfileId = emailProfileIds[0] } // Getting calendars sync profile var calendarsProfileIds = accountSyncManager.profileIds(account.identifier, - "sailfisheas-calendars") + root._easPrefix + "-calendars") if (calendarsProfileIds.length > 0 && calendarsProfileIds[0] !== "") { root._calendarProfileId = calendarsProfileIds[0] } // Getting contacts sync profile var _contactsProfileIds = accountSyncManager.profileIds(account.identifier, - "sailfisheas-contacts") + root._easPrefix + "-contacts") if (_contactsProfileIds.length > 0 && _contactsProfileIds[0] !== "") { root._contactsProfileId = _contactsProfileIds[0] } diff --git a/usr/share/accounts/ui/TwitterSettingsDisplay.qml b/usr/share/accounts/ui/TwitterSettingsDisplay.qml index 0df2367b..44057798 100644 --- a/usr/share/accounts/ui/TwitterSettingsDisplay.qml +++ b/usr/share/accounts/ui/TwitterSettingsDisplay.qml @@ -2,7 +2,7 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 import Sailfish.Accounts 1.0 import com.jolla.settings.accounts 1.0 -import org.nemomobile.configuration 1.0 +import Nemo.Configuration 1.0 StandardAccountSettingsDisplay { id: root diff --git a/usr/share/accounts/ui/email-settings.qml b/usr/share/accounts/ui/email-settings.qml new file mode 100644 index 00000000..a3c1e52e --- /dev/null +++ b/usr/share/accounts/ui/email-settings.qml @@ -0,0 +1,156 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Sailfish.Accounts 1.0 +import com.jolla.settings.accounts 1.0 +import com.jolla.settings.system 1.0 + +AccountSettingsAgent { + id: root + + property Item settings + property bool serverSettingsActive + property bool saveServerSettings + + Component.onCompleted: { + if (settings === null) { + settings = settingsComponent.createObject(root) + } + } + + initialPage: Page { + id: settingsPage + + onPageContainerChanged: { + if (pageContainer == null) { + root.delayDeletion = true + settingsDisplay.saveAccount(false, saveServerSettings) + } + } + + Component.onDestruction: { + if (status == PageStatus.Active || root.serverSettingsActive) { + // app closed while settings are open, so save settings synchronously + settingsDisplay.saveAccount(true, saveServerSettings) + } + } + + SilicaFlickable { + anchors.fill: parent + contentHeight: header.height + settingsDisplay.height + Theme.paddingLarge + + StandardAccountSettingsPullDownMenu { + allowCredentialsUpdate: false + allowDelete: !root.accountIsReadOnly + allowDeleteLimited: !root.accountIsLimited + + onAccountDeletionRequested: { + root.accountDeletionRequested() + pageStack.pop() + } + onSyncRequested: { + settingsDisplay.saveAccountAndSync(saveServerSettings) + saveServerSettings = false + } + + MenuItem { + enabled: settingsDisplay.accountEnabled + //: Opens server settings page + //% "Server settings" + text: qsTrId("accounts-me-server_settings") + onClicked: { + root.serverSettingsActive = true + root.saveServerSettings = true + pageStack.animatorPush(root.settings) + } + } + } + + PageHeader { + id: header + title: root.accountsHeaderText + } + + EmailSettingsDisplay { + id: settingsDisplay + anchors.top: header.bottom + accountManager: root.accountManager + accountProvider: root.accountProvider + accountId: root.accountId + settings: root.settings.settings + accountIsReadOnly: root.accountIsReadOnly + accountIsProvisioned: root.accountIsProvisioned + + onAccountSaveCompleted: { + root.delayDeletion = false + } + } + VerticalScrollDecorator {} + } + } + + Component { + id: settingsComponent + Page { + property bool incomingUsernameEdited: serverConnectionSettings.incomingUsernameEdited + property bool incomingPasswordEdited: serverConnectionSettings.incomingPasswordEdited + property bool outgoingUsernameEdited: serverConnectionSettings.outgoingUsernameEdited + property bool outgoingPasswordEdited: serverConnectionSettings.outgoingPasswordEdited + property bool checkMandatoryFields: serverConnectionSettings.checkMandatoryFields + property alias emailAddress: serverConnectionSettings.emailAddress + property alias serverTypeIndex: serverConnectionSettings.serverTypeIndex + property alias incomingUsername: serverConnectionSettings.incomingUsername + property alias incomingPassword: serverConnectionSettings.incomingPassword + property alias incomingServer: serverConnectionSettings.incomingServer + property alias incomingSecureConnectionIndex: serverConnectionSettings.incomingSecureConnectionIndex + property alias incomingPort: serverConnectionSettings.incomingPort + property alias outgoingUsername: serverConnectionSettings.outgoingUsername + property alias outgoingPassword: serverConnectionSettings.outgoingPassword + property alias outgoingServer: serverConnectionSettings.outgoingServer + property alias outgoingSecureConnectionIndex: serverConnectionSettings.outgoingSecureConnectionIndex + property alias outgoingPort: serverConnectionSettings.outgoingPort + property alias outgoingRequiresAuth: serverConnectionSettings.outgoingRequiresAuth + property alias acceptUntrustedCertificates: serverConnectionSettings.acceptUntrustedCertificates + + property alias settings: serverConnectionSettings + + onPageContainerChanged: { + if (pageContainer == null) { + root.serverSettingsActive = false + } + } + + SilicaFlickable { + anchors.fill: parent + contentHeight: contentColumn.height + Column { + id: contentColumn + width: parent.width + enabled: !accountIsReadOnly + spacing: Theme.paddingLarge + + PageHeader { + id: header + //: Server settings page + //% "Server settings" + title: qsTrId("accounts-he-server_settings") + } + + DisabledByMdmBanner { + id: disabledByMdmBanner + active: root.accountIsReadOnly || root.accountIsLimited + limited: root.accountIsLimited && !root.accountIsReadOnly + } + + EmailCommon { + id: serverConnectionSettings + editMode: true + checkMandatoryFields: true + opacity: accountIsReadOnly ? Theme.opacityLow : 1.0 + accountLimited: root.accountIsLimited + } + } + VerticalScrollDecorator {} + } + } + } +} diff --git a/usr/share/accounts/ui/email-update.qml b/usr/share/accounts/ui/email-update.qml new file mode 100644 index 00000000..ddd18ea0 --- /dev/null +++ b/usr/share/accounts/ui/email-update.qml @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2020 Open Mobile Platform LLC. + * + * License: Proprietary + */ + +import QtQuick 2.0 +import com.jolla.settings.accounts 1.0 +import Sailfish.Accounts 1.0 + +AccountCredentialsAgent { + id: root + + canCancelUpdate: true + + initialPage: CredentialsUpdateDialog { + id: update + serviceName: accountProvider.serviceNames[0] + applicationName: "Jolla" + credentialsName: pop3 ? "pop3/CredentialsId" : "imap4/CredentialsId" + account.identifier: root.accountId + providerIcon: root.accountProvider.iconName + providerName: root.accountProvider.displayName + property bool pop3 + + onCredentialsUpdated: { + root.credentialsUpdated(identifier) + root.goToEndDestination() + } + + onCredentialsUpdateError: root.credentialsUpdateError(message) + + Connections { + target: update.account + onStatusChanged: { + if (update.account.status === Account.Initialized) { + // Type 0 is imap; type 1 is pop3 + // Default to imap + update.pop3 = (update.account.configurationValue(update.serviceName, "incomingServerType") === 1) + } + } + } + } +} diff --git a/usr/share/accounts/ui/email.qml b/usr/share/accounts/ui/email.qml new file mode 100644 index 00000000..c671e332 --- /dev/null +++ b/usr/share/accounts/ui/email.qml @@ -0,0 +1,501 @@ +/* + * Copyright (c) 2013 - 2020 Jolla Ltd. + * Copyright (c) 2020 Open Mobile Platform LLC. + * + * License: Proprietary + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Sailfish.Accounts 1.0 +import com.jolla.settings.accounts 1.0 +import Nemo.Email 0.1 + +AccountCreationAgent { + id: accountCreationAgent + + property Item settingsDialog + property Item busyPageInstance + property QtObject emailAccountInstance + + initialPage: Dialog { + id: accountCreationDialog + + property string name: accountProvider.displayName + property string iconSource: accountProvider.iconName + property bool credentialsCreated + property string defaultServiceName: accountProvider.serviceNames[0] + property bool requiresAuthFields: settings.outgoingRequiresAuth ? settings.outgoingUsername != "" + && settings.outgoingPassword != "" : true + property bool requiredFields: settings.emailAddress != "" && settings.incomingUsername != "" && + settings.incomingServer != "" && settings.incomingPort != "" && + requiresAuthFields && settings.outgoingServer != "" && settings.outgoingPort != "" + property bool allFieldsEmpty: settings.emailAddress == "" && settings.incomingUsername == "" && + settings.incomingServer == "" && settings.incomingPort == "" && + settings.outgoingUsername == "" && settings.outgoingServer == "" && + settings.outgoingPort == "" && autoDiscoverySettings.emailAddress == "" && + autoDiscoverySettings.password == "" + property bool initialSetup: true + property bool initialSetupRequiredFields: autoDiscoverySettings.emailAddress != "" && autoDiscoverySettings.password != "" + property bool showSettings + property bool checkCredentials + property bool checkMandatoryFields + property bool showSettingsDiscoveryError + + acceptDestinationAction: PageStackAction.Push + canAccept: initialSetup ? initialSetupRequiredFields : requiredFields + onAccepted: initialSetup ? busyPageInstance : _saveSettings() + onRejected: _discard() + + function acceptInitialSetup() { + _setInitialSetupSettings() + emailAccountInstance.retrieveSettings(autoDiscoverySettings.emailAddress) + } + + function _discard() { + if (account.status < Account.Error) { + account.remove() + } + } + + function _saveSettings() { + account.displayName = settings.incomingUsername + + account.setConfigurationValue("", "default_credentials_username", settings.incomingUsername) + + //change to username depending on the design + account.setConfigurationValue(defaultServiceName, "emailaddress", settings.emailAddress) + + //this should go to the service file + account.setConfigurationValue(defaultServiceName, "type", "8") + + account.setConfigurationValue(defaultServiceName, "credentialsCheck", 1) + + if (settings.serverTypeIndex == 0) { + account.setConfigurationValue(defaultServiceName, "incomingServerType", 0) + account.setConfigurationValue(defaultServiceName, "imap4/username", settings.incomingUsername) + account.setConfigurationValue(defaultServiceName, "imap4/server", settings.incomingServer) + account.setConfigurationValue(defaultServiceName, "imap4/port", settings.incomingPort) + account.setConfigurationValue(defaultServiceName, "imap4/encryption", settings.incomingSecureConnectionIndex) + account.setConfigurationValue(defaultServiceName, "imap4/pushCapable", 0) + account.setConfigurationValue(defaultServiceName, "imap4/checkInterval", 0) + account.setConfigurationValue(defaultServiceName, "imap4/downloadAttachments", 0) + account.setConfigurationValue(defaultServiceName, "imap4/servicetype", "source") + account.setConfigurationValue(defaultServiceName, "imap4/acceptUntrustedCertificates", settings.acceptUntrustedCertificates ? 1 : 0) + } else { + account.setConfigurationValue(defaultServiceName, "incomingServerType", 1) + account.setConfigurationValue(defaultServiceName, "customFields/showMoreMails", "false") + account.setConfigurationValue(defaultServiceName, "pop3/username", settings.incomingUsername) + account.setConfigurationValue(defaultServiceName, "pop3/server", settings.incomingServer) + account.setConfigurationValue(defaultServiceName, "pop3/port", settings.incomingPort) + account.setConfigurationValue(defaultServiceName, "pop3/encryption", settings.incomingSecureConnectionIndex) + account.setConfigurationValue(defaultServiceName, "pop3/servicetype", "source") + account.setConfigurationValue(defaultServiceName, "pop3/autoDownload", 1) + account.setConfigurationValue(defaultServiceName, "pop3/acceptUntrustedCertificates", settings.acceptUntrustedCertificates ? 1 : 0) + } + account.setConfigurationValue(defaultServiceName, "smtp/smtpusername", settings.outgoingUsername) + //change to username depending on the design + account.setConfigurationValue(defaultServiceName, "smtp/address", settings.emailAddress) + account.setConfigurationValue(defaultServiceName, "smtp/server", settings.outgoingServer) + account.setConfigurationValue(defaultServiceName, "smtp/port", settings.outgoingPort) + account.setConfigurationValue(defaultServiceName, "smtp/encryption", settings.outgoingSecureConnectionIndex) + // If auth is required set authFromCapabilities to true, if not set to false and also set authentication to 0 + if (!settings.outgoingRequiresAuth) { + account.setConfigurationValue(defaultServiceName, "smtp/authentication", 0) + } + account.setConfigurationValue(defaultServiceName, "smtp/authFromCapabilities", settings.outgoingRequiresAuth ? 1 : 0) + account.setConfigurationValue(defaultServiceName, "smtp/servicetype", "sink") + account.setConfigurationValue(defaultServiceName, "smtp/acceptUntrustedCertificates", settings.acceptUntrustedCertificates ? 1 : 0) + + //required to test configuration + checkCredentials = true + account.enableWithService(defaultServiceName) + account.sync() + + accountCreationDialog.acceptDestination = accountCreationAgent.busyPageInstance + accountCreationDialog.acceptDestinationInstance.currentTask = "checkCredentials" + } + + function _setInitialSetupSettings() { + settings.emailAddress = autoDiscoverySettings.emailAddress + settings.incomingUsername = autoDiscoverySettings.emailAddress + settings.outgoingUsername = autoDiscoverySettings.emailAddress + settings.incomingPassword = autoDiscoverySettings.password + settings.outgoingPassword = autoDiscoverySettings.password + } + + function _taskSucceeded() { + if (accountCreationAgent.busyPageInstance !== null) { + accountCreationAgent.busyPageInstance.operationSucceeded() + } + } + + function _taskFailed(serverType, error) { + if (accountCreationAgent.busyPageInstance !== null) { + accountCreationAgent.busyPageInstance.operationFailed(serverType, error) + } + } + + onAcceptPendingChanged: { + if (acceptPending === true) { + checkMandatoryFields = true + root.focus = true + } + } + + onStatusChanged: { + if (status === PageStatus.Active && account.identifier === 0) { + accountManager.createAccount(accountProvider.name) + accountCreationAgent.busyPageInstance = busyPageComponent.createObject(accountCreationAgent) + accountCreationDialog.acceptDestination = accountCreationAgent.busyPageInstance + accountCreationDialog.acceptDestinationInstance.currentTask = "settingsDiscovery" + emailAccountInstance = emailAccountComponent.createObject(accountCreationAgent) + } + } + + SilicaFlickable { + id: flickable + + anchors.fill: parent + contentHeight: Math.max(contentColumn.height + (manualSetupButton.visible ? manualSetupButton.height : 0), parent.height) + + Column { + id: contentColumn + + width: parent.width + + DialogHeader { + dialog: accountCreationDialog + + // Ensure checkMandatoryFields is set if 'accept' is tapped and some fields + // are not valid + Item { + id: headerChild + Connections { + target: headerChild.parent + onClicked: accountCreationDialog.checkMandatoryFields = true + } + } + } + + Item { + x: Theme.horizontalPageMargin + width: parent.width - x*2 + height: icon.height + Theme.paddingLarge + + Image { + id: icon + width: Theme.iconSizeLarge + height: width + anchors.top: parent.top + source: accountCreationDialog.iconSource + } + Label { + anchors { + left: icon.right + leftMargin: Theme.paddingLarge + right: parent.right + verticalCenter: icon.verticalCenter + } + text: accountCreationDialog.name + color: Theme.highlightColor + font.pixelSize: Theme.fontSizeLarge + truncationMode: TruncationMode.Fade + } + } + + Label { + id: settingsDiscoveryFailedLabel + x: Theme.horizontalPageMargin + visible: accountCreationDialog.showSettingsDiscoveryError + width: parent.width - x*2 + wrapMode: Text.Wrap + font.pixelSize: Theme.fontSizeExtraSmall + color: Theme.highlightColor + //: Information label displayed when settings for this account could not be discovered + //% "Couldn't find the settings for your account. Please complete the settings in the fields below." + text: qsTrId("components_accounts-la-genericemail_settings_discovery_failed") + } + + Column { + id: autoDiscoverySettings + visible: !accountCreationDialog.showSettings + property alias emailAddress: emailAddress.text + property alias password: password.text + + width: parent.width + + GeneralEmailAddressField { + id: emailAddress + width: parent.width + errorHighlight: !text && accountCreationDialog.checkMandatoryFields + EnterKey.iconSource: "image://theme/icon-m-enter-next" + EnterKey.onClicked: password.focus = true + } + + PasswordField { + id: password + errorHighlight: !text && accountCreationDialog.checkMandatoryFields + + //% "Password is required" + description: errorHighlight ? qsTrId("components_accounts-la-password_required") : "" + + EnterKey.iconSource: "image://theme/icon-m-enter-accept" + EnterKey.onClicked: autoDiscoverySettings.focus = true + } + } + + EmailCommon { + id: settings + visible: accountCreationDialog.showSettings + checkMandatoryFields: accountCreationDialog.checkMandatoryFields + // Fade for manual setup transition + opacity: accountCreationDialog.initialSetup ? 0 : 1 + + Behavior on opacity { FadeAnimation {} } + } + } + + Button { + id: manualSetupButton + anchors.horizontalCenter: parent.horizontalCenter + y: Math.max(contentColumn.height + Theme.paddingLarge, flickable.height - height - Theme.paddingLarge) + + pageStack.panelSize // don't move button when vkb is open + visible: !accountCreationDialog.showSettings + + //: Manual configuration button + //% "Manual setup" + text: qsTrId("components_accounts-la-manual_setup") + onClicked: { + // Required to close vkb + accountCreationDialog.focus = true + accountCreationDialog._setInitialSetupSettings() + accountCreationDialog.showSettings = true + accountCreationDialog.initialSetup = false + } + } + + VerticalScrollDecorator {} + } + } + + Connections { + target: accountCreationAgent.accountManager + + // Trigger account transition to 'Initialized' status + onAccountCreated: { + account.identifier = accountId + } + } + + Component { + id: busyPageComponent + EmailBusyPage { + settingsDialog: accountCreationAgent.settingsDialog + + onStatusChanged: { + if (status === PageStatus.Active) { + if (currentTask == "settingsDiscovery") { + accountCreationDialog.acceptInitialSetup() + } + } + } + + onCurrentTaskChanged: state = "busy" + + onInfoButtonClicked: { + skipping = true + if (hideIncomingSettings) { + // we are in saved mode, skip smtp creation only + settingsDialog.skipSmtp = true + pageStack.animatorReplace(settingsDialog) + } else { + // we are in skip mode, so remove the account + account.remove() + accountCreationAgent.goToEndDestination() + } + } + + onPageContainerChanged: { + if (pageContainer == null && !skipping) { + accountCreationDialog.focus = true + + if (currentTask == "checkCredentials" && errorOccured) { + if (hideIncomingSettings) { + settings.hideIncoming = true + } + // Reset everything + emailAccountInstance.cancelTest() + account.remove() + accountCreationDialog.credentialsCreated = false + account.incomingCredentialsCreated = false + account.outgoingCredentialsCreated = false + } + } + } + + Component.onDestruction: { + if (status == PageStatus.Active) { + // app closed while setup is in progress, remove account + account.remove() + } + } + } + } + + Account { + id: account + + property bool incomingCredentialsCreated + property bool outgoingCredentialsCreated + + onStatusChanged: { + if (status === Account.Synced) { + if (!incomingCredentialsCreated) { + incomingCredentialsCreated = true + var credentialsName = (settings.serverTypeIndex == 0) ? "imap4/CredentialsId": "pop3/CredentialsId" + account.createSignInCredentials( "Jolla", credentialsName, + account.signInParameters(accountCreationDialog.defaultServiceName, settings.incomingUsername, settings.incomingPassword)) + } + // set the accountId for the settings page + if (accountCreationDialog.credentialsCreated) { + accountCreationAgent.accountCreated(identifier) + if (accountCreationDialog.checkCredentials) { + accountCreationAgent.accountCreated(identifier) + accountSyncManager.createProfile("syncemail", identifier, "email") + emailAccountInstance.accountId = identifier + // Create settings page + accountCreationAgent.settingsDialog = settingsComponent.createObject(accountCreationAgent, {"accountId": identifier, "isNewAccount": true}) + // 120 seconds timeout + emailAccountInstance.test(120) + accountCreationDialog.showSettingsDiscoveryError = false + accountCreationDialog.checkCredentials = false + } + } + } else if (status === Account.Error) { + console.log("Generic email provider account error:", errorMessage) + accountCreationAgent.accountCreationError(errorMessage) + } + } + + onSignInCredentialsCreated: { + if (!outgoingCredentialsCreated) { + outgoingCredentialsCreated = true + account.createSignInCredentials( "Jolla", "smtp/CredentialsId", + account.signInParameters(accountCreationDialog.defaultServiceName, settings.outgoingUsername, settings.outgoingPassword)) + } else { + accountCreationDialog.credentialsCreated = true + var serviceSettings = account.configurationValues("") + account.setConfigurationValue(accountCreationDialog.defaultServiceName, "smtp/CredentialsId", serviceSettings["Jolla/segregated_credentials/smtp/CredentialsId"]) + var credentialsName = (settings.serverTypeIndex == 0) ? "imap4/CredentialsId": "pop3/CredentialsId" + account.setConfigurationValue(accountCreationDialog.defaultServiceName, credentialsName, serviceSettings["Jolla/segregated_credentials/" + credentialsName]) + // Enabling account here for credentials checking + account.enabled = true + account.sync() + } + } + + onSignInError: { + console.log("Generic email provider account error:", message) + accountCreationAgent.accountCreationError(message) + account.remove() + } + } + + Component { + id: emailAccountComponent + EmailAccount { + id: emailAccount + + onTestSucceeded: { + settingsDialog.pushCapable = emailAccount.pushCapable + accountCreationDialog._taskSucceeded() + } + + onTestFailed: { + accountCreationDialog._taskFailed(serverType, error) + } + + onSettingsRetrieved: { + settings.serverTypeIndex = emailAccount.recvType == "imap4" ? 0 : 1 + settings.incomingServer = emailAccount.recvServer + settings.incomingPort = emailAccount.recvPort + settings.incomingSecureConnectionIndex = parseInt(emailAccount.recvSecurity) + + settings.outgoingServer = emailAccount.sendServer + settings.outgoingPort = emailAccount.sendPort + settings.outgoingSecureConnectionIndex = parseInt(emailAccount.sendSecurity) + // 0 means no auth + settings.outgoingRequiresAuth = parseInt(emailAccount.sendAuth) + + accountCreationAgent.busyPageInstance.settingsRetrieved = true + accountCreationDialog.showSettings = true + accountCreationDialog.initialSetup = false + accountCreationDialog._saveSettings() + } + + onSettingsRetrievalFailed: { + accountCreationDialog.showSettingsDiscoveryError = true + accountCreationDialog.showSettings = true + // Don't emit error here, just show manual config page + accountCreationDialog._taskSucceeded() + accountCreationDialog.initialSetup = false + } + } + } + + AccountSyncManager { + id: accountSyncManager + } + + Component { + id: settingsComponent + Dialog { + property alias isNewAccount: settingsDisplay.isNewAccount + property alias accountId: settingsDisplay.accountId + property alias skipSmtp: settingsDisplay.skipSmtp + property alias pushCapable: settingsDisplay.pushCapable + + acceptDestination: accountCreationAgent.endDestination + acceptDestinationAction: accountCreationAgent.endDestinationAction + acceptDestinationProperties: accountCreationAgent.endDestinationProperties + acceptDestinationReplaceTarget: accountCreationAgent.endDestinationReplaceTarget + backNavigation: false + + onAccepted: { + accountCreationAgent.delayDeletion = true + settingsDisplay.saveNewAccount() + } + + Component.onDestruction: { + if (status == PageStatus.Active) { + // app closed while setup is in progress, remove account + account.remove() + } + } + + SilicaFlickable { + anchors.fill: parent + contentHeight: header.height + settingsDisplay.height + Theme.paddingLarge + + DialogHeader { + id: header + } + + EmailSettingsDisplay { + id: settingsDisplay + anchors.top: header.bottom + accountManager: accountCreationAgent.accountManager + accountProvider: accountCreationAgent.accountProvider + autoEnableAccount: true + settings: settings + + onAccountSaveCompleted: { + accountCreationAgent.delayDeletion = false + } + } + VerticalScrollDecorator {} + } + } + } +} diff --git a/usr/share/accounts/ui/vk-settings.qml b/usr/share/accounts/ui/facebook-settings.qml similarity index 98% rename from usr/share/accounts/ui/vk-settings.qml rename to usr/share/accounts/ui/facebook-settings.qml index 1f1ec27b..1402ade5 100644 --- a/usr/share/accounts/ui/vk-settings.qml +++ b/usr/share/accounts/ui/facebook-settings.qml @@ -48,7 +48,7 @@ AccountSettingsAgent { title: root.accountsHeaderText } - VkSettingsDisplay { + FacebookSettingsDisplay { id: settingsDisplay anchors.top: header.bottom accountManager: root.accountManager diff --git a/usr/share/accounts/ui/vk-update.qml b/usr/share/accounts/ui/facebook-update.qml similarity index 75% rename from usr/share/accounts/ui/vk-update.qml rename to usr/share/accounts/ui/facebook-update.qml index 66c51843..b6e2a0e4 100644 --- a/usr/share/accounts/ui/vk-update.qml +++ b/usr/share/accounts/ui/facebook-update.qml @@ -11,13 +11,10 @@ AccountCredentialsAgent { return } var sessionData = { - "Display": "touch", - "V": "5.21", - "ClientId": keyProvider.storedKey("vk", "vk-sync", "client_id"), - "ClientSecret": keyProvider.storedKey("vk", "vk-sync", "client_secret"), - "ResponseType": "token" + "Display": "popup", + "ClientId": keyProvider.storedKey("facebook", "facebook-sync", "client_id") } - initialPage.prepareAccountCredentialsUpdate(account, root.accountProvider, "vk-sync", sessionData) + initialPage.prepareAccountCredentialsUpdate(account, root.accountProvider, "facebook-sync", sessionData) } Account { @@ -46,5 +43,11 @@ AccountCredentialsAgent { onAccountCredentialsUpdateError: { root.credentialsUpdateError(errorMessage) } + + onPageContainerChanged: { + if (pageContainer == null) { // page was popped + cancelSignIn() + } + } } } diff --git a/usr/share/accounts/ui/vk.qml b/usr/share/accounts/ui/facebook.qml similarity index 51% rename from usr/share/accounts/ui/vk.qml rename to usr/share/accounts/ui/facebook.qml index ee2ab122..80a34179 100644 --- a/usr/share/accounts/ui/vk.qml +++ b/usr/share/accounts/ui/facebook.qml @@ -23,9 +23,9 @@ AccountCreationAgent { _goToSettings(accountId) }) _accountSetup.error.connect(function() { - //: Error which is displayed when the user attempts to create a duplicate VK account - //% "You have already added a VK account for user %1." - var duplicateAccountError = qsTrId("jolla_settings_accounts_extensions-la-vk_duplicate_account").arg(root._existingUserName) + //: Error which is displayed when the user attempts to create a duplicate Facebook account + //% "You have already added a Facebook account for user %1." + var duplicateAccountError = qsTrId("jolla_settings_accounts_extensions-la-facebook_duplicate_account").arg(root._existingUserName) accountCreationError(duplicateAccountError) _oauthPage.done(false, AccountFactory.BadParametersError, duplicateAccountError) }) @@ -40,27 +40,37 @@ AccountCreationAgent { } initialPage: AccountCreationLegaleseDialog { - //: The text explaining how user's VK data will be used on the device - //% "When you add a VK account, information from this account will be added to the phone to provide a faster and better experience:

- Friends will be added to the People app and linked with existing contacts

- VK groups will be added to the Calendar app

- VK posts and notifications will be added to the Feeds view.

- Photos from VK will be available from the Gallery app

Some of this data will be cached to make it available when offline. This cache can be cleared by deleting the account.

Adding a VK account on your device means that you agree to VK's Terms of Service." - legaleseText: qsTrId("jolla_settings_accounts_extensions-la-vk_consent_text") - - //: Button which the user presses to view VK Terms Of Service webpage - //% "VKontakte Terms of Service" - externalUrlText: qsTrId("jolla_settings_accounts_extensions-bt-vk_terms") - externalUrlLink: "http://vk.com/terms" + //: The text explaining how user's Facebook data will be used on the device + //% "When you add a Facebook account, information from this account will be added " + //% "to the device to provide a faster and better experience:

" + //% " - Facebook events will be added to the Calendar app

" + //% " - Facebook photos will be available from the Gallery app, " + //% "and you will be able to share photos on Facebook.

" + //% "Some of this data will be cached to make it available when offline. " + //% "This cache can be cleared at any time by disabling the relevant services " + //% "on the account settings page.

" + //% "Adding a Facebook account on your device means that you agree to " + //% "Facebook's Terms of Service." + legaleseText: qsTrId("jolla_settings_accounts_extensions-la-facebook_consent_text") + + //: Button which the user presses to view Facebook Terms Of Service webpage + //% "Facebook Terms of Service" + externalUrlText: qsTrId("jolla_settings_accounts_extensions-bt-facebook_terms") + externalUrlLink: "https://m.facebook.com/legal/terms" onStatusChanged: { - if (status == PageStatus.Active && !_oauthPage) { + if ((_oauthPage != null && status == PageStatus.Active) + || (_oauthPage != null && status == PageStatus.Deactivating && result == DialogResult.Rejected)) { + // OAuth pages can't be reused as user must be taken back to initial sign-in page. + _oauthPage.cancelSignIn() + _oauthPage.destroy() + _oauthPage = null + } + if (status == PageStatus.Active) { _oauthPage = oAuthComponent.createObject(root) acceptDestination = _oauthPage } } - - onPageContainerChanged: { - if (pageContainer == null && _oauthPage) { - _oauthPage.cancelSignIn() - } - } } AccountFactory { @@ -72,13 +82,10 @@ AccountCreationAgent { OAuthAccountSetupPage { Component.onCompleted: { var sessionData = { - "Display": "touch", - "V": "5.21", - "ClientId": keyProvider.storedKey("vk", "vk-sync", "client_id"), - "ClientSecret": keyProvider.storedKey("vk", "vk-sync", "client_secret"), - "ResponseType": "token" + "Display": "popup", + "ClientId": keyProvider.storedKey("facebook", "facebook-sync", "client_id") } - prepareAccountCreation(root.accountProvider, "vk-sync", sessionData) + prepareAccountCreation(root.accountProvider, "facebook-sync", sessionData) } onAccountCreated: { root._handleAccountCreated(accountId, responseData) @@ -98,8 +105,8 @@ AccountCreationAgent { QtObject { id: accountSetup property string accessToken - property int accountId property bool hasSetName + property int accountId signal done() signal error() @@ -109,7 +116,8 @@ AccountCreationAgent { onStatusChanged: { if (status == Account.Initialized || status == Account.Synced) { if (!accountSetup.hasSetName) { - getProfileInfo() + var queryItems = {"access_token": accountSetup.accessToken} + sni.arbitraryRequest(SocialNetwork.Get, "https://graph.facebook.com/v2.2/me?fields=name,id", queryItems) } else { accountSetup.done() } @@ -118,56 +126,35 @@ AccountCreationAgent { } } } - - function getProfileInfo() { - var doc = new XMLHttpRequest() - doc.onreadystatechange = function() { - if (doc.readyState === XMLHttpRequest.DONE) { - if (doc.status === 200) { - var users = JSON.parse(doc.responseText) - if (users.response.length > 0) { - var name = users.response[0].first_name - var lastName = users.response[0].last_name - if (name !== "" && lastName !== "") { - name += " " - } - name += lastName - - var screenName = users.response[0].screen_name - if (accountFactory.findAccount( - "vk", - "", - "default_credentials_screen_name", - screenName) !== 0) { - // this account already exists. show error dialog. - hasSetName = true - root._existingUserName = name - newAccount.remove() - accountSetup.error() - return - } - - newAccount.setConfigurationValue("", "default_credentials_username", name) - newAccount.setConfigurationValue("", "default_credentials_screen_name", screenName) - newAccount.displayName = name + property SocialNetwork sni: SocialNetwork { + onArbitraryRequestResponseReceived: { + var name = data["name"] + var fbid = data["id"] + if ((name == undefined || name == "") && (fbid == undefined || fbid == "")) { + accountSetup.done() + } else { + if (name != undefined && name != "") { + newAccount.setConfigurationValue("", "default_credentials_username", name) + newAccount.displayName = name + root._existingUserName = name + } + if (fbid != undefined && fbid != "") { + if (accountFactory.findAccount( + "facebook", + "", + "facebook_id", + fbid) !== 0) { + // this account already exists. display error dialog. accountSetup.hasSetName = true - newAccount.sync() - } else { - console.log("Empty VK user query response") - accountSetup.done() + newAccount.remove() + return } - } else { - console.log("Failed to query VK users, error: " + doc.status) - accountSetup.done() + newAccount.setConfigurationValue("", "facebook_id", fbid) } + accountSetup.hasSetName = true + newAccount.sync() } } - - var postData = "access_token=" + accessToken - var url = "https://api.vk.com/method/users.get?access_token="+accessToken+"&v=5.21&fields=screen_name" - doc.open("GET", url) - doc.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded') - doc.send() } } } @@ -196,7 +183,7 @@ AccountCreationAgent { id: header } - VkSettingsDisplay { + FacebookSettingsDisplay { id: settingsDisplay anchors.top: header.bottom accountManager: root.accountManager diff --git a/usr/share/accounts/ui/jolla-settings.qml b/usr/share/accounts/ui/jolla-settings.qml index 1c34b553..7295d921 100644 --- a/usr/share/accounts/ui/jolla-settings.qml +++ b/usr/share/accounts/ui/jolla-settings.qml @@ -1,6 +1,7 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 import Sailfish.Accounts 1.0 +import Sailfish.Store 1.0 import com.jolla.settings.accounts 1.0 AccountSettingsAgent { @@ -116,6 +117,17 @@ AccountSettingsAgent { text: qsTrId("settings_accounts-he-account_jolla_com_webpage") onClicked: Qt.openUrlExternally("https://account.jolla.com/") } + + Button { + anchors.horizontalCenter: parent.horizontalCenter + preferredWidth: Theme.buttonWidthLarge + visible: StoreClient.isAvailable + + //% "My Add-Ons" + text: qsTrId("settings_accounts-he-add_ons") + enabled: settingsDisplay.accountValid + onClicked: pageStack.animatorPush("com.jolla.settings.accounts.JollaAccountAddOnsPage") + } } } diff --git a/usr/share/accounts/ui/jolla.qml b/usr/share/accounts/ui/jolla.qml index e3f256ae..d4706f53 100644 --- a/usr/share/accounts/ui/jolla.qml +++ b/usr/share/accounts/ui/jolla.qml @@ -1,7 +1,7 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 import com.jolla.settings.accounts 1.0 -import org.nemomobile.configuration 1.0 +import Nemo.Configuration 1.0 AccountCreationAgent { id: root diff --git a/usr/share/accounts/ui/nextcloud.qml b/usr/share/accounts/ui/nextcloud.qml index 1f2e712c..98bd5f76 100644 --- a/usr/share/accounts/ui/nextcloud.qml +++ b/usr/share/accounts/ui/nextcloud.qml @@ -23,6 +23,13 @@ OnlineSyncAccountCreationAgent { accountManager.service("nextcloud-sharing") ] + sharedScheduleServices: [ + accountManager.service("nextcloud-carddav"), + accountManager.service("nextcloud-caldav"), + accountManager.service("nextcloud-images"), + accountManager.service("nextcloud-posts"), + ] + webdavPath: AccountsUtil.joinServerPathInAddress(serverAddress, "/remote.php/dav/files/" + username) imagesPath: AccountsUtil.joinServerPathInAddress(serverAddress, "/remote.php/dav/files/" + username + "/Photos") backupsPath: AccountsUtil.joinServerPathInAddress(serverAddress, "/remote.php/dav/files/" + username + "/Sailfish OS/Backups") diff --git a/usr/share/accounts/ui/sailfisheas-oauth-settings.qml b/usr/share/accounts/ui/sailfisheas-oauth-settings.qml new file mode 100644 index 00000000..3857e11a --- /dev/null +++ b/usr/share/accounts/ui/sailfisheas-oauth-settings.qml @@ -0,0 +1,155 @@ +/* + * Copyright (C) 2022 Jolla Ltd. + * + * License: Proprietary + */ +import QtQuick 2.6 +import Sailfish.Silica 1.0 +import Sailfish.Accounts 1.0 +import com.jolla.settings.accounts 1.0 +import com.jolla.sailfisheas 1.0 +import com.jolla.settings.system 1.0 +import "SailfishEasSettings.js" as ServiceSettings + +AccountSettingsAgent { + id: root + + property Item connectionSettingsPage + property bool serverSettingsActive + property bool saveConnectionSettings + + Component.onCompleted: { + if (!connectionSettingsPage) { + connectionSettingsPage = connectionSettingsComponent.createObject(root) + } + } + + initialPage: Page { + onPageContainerChanged: { + if (pageContainer == null && !credentialsUpdater.running) { + root.delayDeletion = true + settingsDisplay.saveAccount(false, saveConnectionSettings) + } + } + + Component.onDestruction: { + if (status == PageStatus.Active || root.serverSettingsActive) { + // app closed while settings are open, so save settings synchronously + settingsDisplay.saveAccount(true, saveConnectionSettings) + } + } + + AccountCredentialsUpdater { + id: credentialsUpdater + } + + SilicaFlickable { + anchors.fill: parent + contentHeight: header.height + settingsDisplay.height + Theme.paddingLarge + + StandardAccountSettingsPullDownMenu { + allowCredentialsUpdate: root.accountNotSignedIn + allowDelete: !root.accountIsReadOnly + allowDeleteLimited: !root.accountIsLimited + + onCredentialsUpdateRequested: credentialsUpdater.replaceWithCredentialsUpdatePage(root.accountId) + onAccountDeletionRequested: { + root.accountDeletionRequested() + pageStack.pop() + } + onSyncRequested: { + settingsDisplay.saveAccountAndTriggerSync(saveConnectionSettings) + saveConnectionSettings = false + } + + MenuItem { + visible: !root.accountIsReadOnly + enabled: settingsDisplay.accountEnabled + //: Opens server settings page + //% "Edit server settings" + text: qsTrId("accounts-me-edit_server_settings") + onClicked: { + root.serverSettingsActive = true + root.saveConnectionSettings = true + pageStack.animatorPush(root.connectionSettingsPage) + } + } + } + + PageHeader { + id: header + title: root.accountsHeaderText + } + + SailfishEasSettingsDisplay { + id: settingsDisplay + oauthEnabled: true + anchors.top: header.bottom + accountManager: root.accountManager + accountProvider: root.accountProvider + accountId: root.accountId + connectionSettings: root.connectionSettingsPage.settings + + onAccountSaveCompleted: { + root.delayDeletion = false + } + } + VerticalScrollDecorator {} + } + } + + Component { + id: connectionSettingsComponent + Page { + property alias settings: activesyncConnectionSettings + + onPageContainerChanged: { + if (pageContainer == null) { + root.serverSettingsActive = false + if (!credentialsUpdater.running) { + root.delayDeletion = true + settingsDisplay.saveAccount(false, saveConnectionSettings) + } + } + } + + SilicaFlickable { + anchors.fill: parent + contentHeight: contentColumn.height + Column { + id: contentColumn + width: parent.width + bottomPadding: Theme.paddingMedium + + PageHeader { + id: header + //: Server settings page + //% "Server settings" + title: qsTrId("accounts-he-server_settings") + } + + DisabledByMdmBanner { + id: disabledByMdmBanner + active: root.accountIsLimited + limited: true + } + + SailfishEasConnectionSettings { + id: activesyncConnectionSettings + oauthEnabled: true + checkMandatoryFields: true + editMode: true + limitedMode: root.accountIsLimited + onCertificateDataSaved: { + // increase credentials counter so daemon side knows to reload. + // would be saved later, but let's already avoid different settings getting out of sync + settingsDisplay.increaseCredentialsCounter() + settingsDisplay.saveAccount(false, true) + } + } + } + VerticalScrollDecorator {} + } + } + } +} diff --git a/usr/share/accounts/ui/sailfisheas-oauth-update.qml b/usr/share/accounts/ui/sailfisheas-oauth-update.qml new file mode 100644 index 00000000..0915f664 --- /dev/null +++ b/usr/share/accounts/ui/sailfisheas-oauth-update.qml @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2022 Jolla Ltd. + * + * License: Proprietary + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Sailfish.Accounts 1.0 +import com.jolla.settings.accounts 1.0 + +AccountCredentialsAgent { + id: accountCreationAgent + + function _start() { + if (initialPage.status != PageStatus.Active || account.status != Account.Initialized) { + return + } + var sessionData = { + "ClientId": keyProvider.clientId(), + "ExtraParams": { + // Require email address to match the previous address + "login_hint": account.configurationValue("", "connection/emailaddress"), + "hsu": "1" // Prevent selecting another account instead + } + } + initialPage.prepareAccountCredentialsUpdate(account, accountCreationAgent.accountProvider, + "sailfisheas-oauth-email", sessionData) + } + + Account { + id: account + identifier: accountCreationAgent.accountId + + onStatusChanged: { + accountCreationAgent._start() + } + } + + StoredKeyProvider { + id: keyProvider + + function clientId() { + return keyProvider.storedKey("sailfisheas", "", "client_id") + } + } + + initialPage: OAuthAccountSetupPage { + onStatusChanged: { + accountCreationAgent._start() + } + + onAccountCredentialsUpdated: { + accountCreationAgent.credentialsUpdated(accountCreationAgent.accountId) + accountCreationAgent.goToEndDestination() + } + + onAccountCredentialsUpdateError: { + accountCreationAgent.credentialsUpdateError(errorMessage) + } + } +} + diff --git a/usr/share/accounts/ui/sailfisheas-oauth.qml b/usr/share/accounts/ui/sailfisheas-oauth.qml new file mode 100644 index 00000000..ecda550a --- /dev/null +++ b/usr/share/accounts/ui/sailfisheas-oauth.qml @@ -0,0 +1,500 @@ +/* + * Copyright (C) 2022 Jolla Ltd. + * + * License: Proprietary + */ +import QtQuick 2.0 +import QtQml 2.2 +import Sailfish.Silica 1.0 +import Sailfish.Accounts 1.0 +import com.jolla.settings.accounts 1.0 +import com.jolla.sailfisheas 1.0 +import Nemo.Connectivity 1.0 +import "SailfishEasSettings.js" as ServiceSettings + +AccountCreationAgent { + id: accountCreationAgent + + property Item _oauthPage + property Item busyPageInstance + property Item settingsDialog + + StoredKeyProvider { + id: keyProvider + + function clientId() { + return keyProvider.storedKey("sailfisheas", "", "client_id") + } + } + + Component { + id: oAuthComponent + OAuthAccountSetupPage { + function getEmailFromIdToken(token) { + // Note that this does not attempt to check the signature + // Split payload + var sep1 = token.indexOf('.') + var sep2 = token.indexOf('.', sep1 + 1) + var payload = token.substring(sep1, sep2) + // Replace a few characters to convert from base64url to normal base64 + // See also https://stackoverflow.com/a/51838635 + payload = payload.replace(/-/g, '+').replace(/_/g, '/') + // and also determine padding to add + var padding = payload.length % 4 + if (padding > 1) { + // If padding length was 1, the encoded string would be invalid + payload += new Array(5 - padding).join('=') + } + // Now decode with regular base64 decoder + payload = Qt.atob(payload) + var content = JSON.parse(payload) + return content["email"] || "" + } + + Component.onCompleted: { + var sessionData = { + "ClientId": keyProvider.clientId(), + "Scope": ["https://outlook.office.com/.default", "offline_access", "openid", "email"], + "ExtraParams": { + "prompt": "select_account" // Ask to select account + } + } + prepareAccountCreation(accountCreationAgent.accountProvider, "sailfisheas-oauth-email", sessionData) + } + onAccountCreated: { + var idToken = responseData["IdToken"] + if (idToken !== undefined) { + settings.emailaddress = getEmailFromIdToken(idToken) + } + var accessToken = responseData["AccessToken"] + account.identifier = accountId + pageStack.replace(accountCreationDialog, { "accessToken": accessToken }) + } + onAccountCreationError: { + accountCreationAgent.accountCreationError(errorMessage) + } + } + } + + initialPage: AccountCreationLegaleseDialog { + //% "Adding a Microsoft 365 account on your device means that you agree to Microsoft 365's Terms of Service." + legaleseText: qsTrId("components_accounts-la-microsoft_365_consent_text") + + //: Button which the user presses to view Microsoft 365 Terms Of Service webpage + //% "Microsoft 365 Terms of Service" + externalUrlText: qsTrId("components_accounts-bt-microsoft_365_terms") + externalUrlLink: "https://www.microsoft.com/en-us/servicesagreement/" + + acceptDestinationAction: PageStackAction.Push + onStatusChanged: { + if (status == PageStatus.Active) { + if (_oauthPage != null) { + _oauthPage.destroy() + } + _oauthPage = oAuthComponent.createObject(root) + acceptDestination = _oauthPage + } + } + } + + ConnectionHelper { + id: connectionHelper + + onOnlineChanged: { + if (online && accountCreationDialog.delayTask) { + delayedTask() + } + } + + function delayedTask() { + accountCreationDialog.delayTask = false + if (connectionHelper.online) { + accountCreationAgent.busyPageInstance.runAccountCreation() + } else { + if (accountCreationAgent.busyPageInstance.currentTask === "checkCredentials") { + ServiceSettings.saveConnectionSettings(settings, "sailfisheas-oauth") + account.displayName = settings.emailaddress + account.sync() + } else { + accountCreationAgent.busyPageInstance.cancelAccountCreation() + } + } + } + + Component.onCompleted: { + connectionHelper.requestNetwork() + } + } + + Dialog { + id: accountCreationDialog + + property string accessToken // For autodiscovery + property string defaultServiceName: accountProvider.serviceNames[0] + property bool _serverAddressRequired: showManualSettings ? settings.server != "" : true + property bool _knownCredentials: accDbCheckService.knownEmail(settings.emailaddress) || + accDbCheckService.alreadyCreated(settings.username, settings.server, settings.domain) + property bool showManualSettings + property bool showSettingsDiscoveryError + property bool delayTask + + acceptDestinationAction: PageStackAction.Push + canAccept: settings.emailaddress != "" && _serverAddressRequired && !_knownCredentials + onRejected: { + if (account.status < Account.Error) { + account.remove() + } + } + + onAcceptBlocked: settings.checkMandatoryFields = true + + onShowManualSettingsChanged: { + if (status === PageStatus.Active) { + accountCreationAgent.busyPageInstance.currentTask = showManualSettings ? "checkCredentials" : "autodiscovery" + } + } + + function acceptInitialSetup() { + autoDiscoveryService.startAutoDiscoveryOAuth(settings.emailaddress, accessToken) + } + + function save() { + ServiceSettings.saveConnectionSettings(settings, "sailfisheas-oauth") + account.displayName = settings.emailaddress + account.sync() + } + + function taskFailed(error) { + // force main page to show all connection settings + accountCreationDialog.showManualSettings = true + if (accountCreationAgent.busyPageInstance !== null) { + accountCreationAgent.busyPageInstance.operationFailed(error) + } + } + + onStatusChanged: { + if (status === PageStatus.Active) { + accountCreationAgent.busyPageInstance = busyPageComponent.createObject(accountCreationAgent) + accountCreationDialog.acceptDestination = accountCreationAgent.busyPageInstance + accountCreationAgent.busyPageInstance.currentTask = showManualSettings ? "checkCredentials" : "autodiscovery" + } + } + + SilicaFlickable { + anchors.fill: parent + contentHeight: contentColumn.height + detailsButton.height + 2*Theme.paddingLarge + + Column { + id: contentColumn + width: parent.width + + DialogHeader { + dialog: accountCreationDialog + } + + Item { + x: Theme.paddingLarge + width: parent.width - x*2 + height: icon.height + Theme.paddingLarge + + Image { + id: icon + width: Theme.iconSizeLarge + height: width + anchors.top: parent.top + source: accountProvider.iconName + } + Label { + anchors { + left: icon.right + leftMargin: Theme.paddingLarge + right: parent.right + verticalCenter: icon.verticalCenter + } + text: accountProvider.displayName + color: Theme.highlightColor + font.pixelSize: Theme.fontSizeLarge + truncationMode: TruncationMode.Fade + } + } + + Label { + x: Theme.paddingLarge + visible: opacity > 0.0 + opacity: accountCreationDialog._knownCredentials ? 1.0 : 0.0 + height: opacity * implicitHeight + Behavior on opacity { FadeAnimation{} } + width: parent.width - x*2 + wrapMode: Text.Wrap + font.pixelSize: Theme.fontSizeExtraSmall + color: Theme.highlightColor + //: Information label displayed when settings entered for this account are used in another configured one + //% "Account for entered credentials already exists." + text: qsTrId("components_accounts-la-activesync_settings_already_exists") + } + + Label { + x: Theme.paddingLarge + visible: opacity > 0.0 + opacity: (accountCreationDialog.showSettingsDiscoveryError + && !accountCreationDialog._knownCredentials) ? 1.0 : 0.0 + height: opacity * implicitHeight + Behavior on opacity { FadeAnimation{} } + width: parent.width - x*2 + wrapMode: Text.Wrap + font.pixelSize: Theme.fontSizeExtraSmall + color: Theme.highlightColor + //: Information label displayed when settings for this account could not be discovered + //% "Couldn't find the settings for your account. Please complete the settings in the fields below." + text: qsTrId("components_accounts-la-activesync_settings_discovery_failed") + } + + SailfishEasConnectionSettings { + id: settings + oauthEnabled: true + editMode: accountCreationDialog.showManualSettings + onCertificateDataSaved: { + ServiceSettings.saveConnectionSettings(settings, "sailfisheas-oauth") + console.log("certificate data saved with id", sslCredentialsId) + account.finishCheckCredentials = true + account.sync() + } + } + } + + Button { + id: detailsButton + anchors { + horizontalCenter: parent.horizontalCenter + top: contentColumn.bottom + topMargin: Theme.paddingLarge + } + //% "Less" + text: accountCreationDialog.showManualSettings ? qsTrId("components_accounts-bt-activesync_less") + //% "More" + : qsTrId("components_accounts-bt-activesync_more") + onClicked: { + accountCreationDialog.showManualSettings = !accountCreationDialog.showManualSettings + accountCreationDialog.focus = true + } + } + + VerticalScrollDecorator {} + } + } + + CheckExistence { + id: accDbCheckService + } + + AutoDiscovery { + id: autoDiscoveryService + onAutoDiscoveryDone: { + console.log("[jsa-eas] AutoDiscovery DONE: server == " + autoDiscoveryService.server) + settings.server = autoDiscoveryService.server + settings.port = autoDiscoveryService.port + settings.secureConnection = autoDiscoveryService.secureConnection + settings.username = autoDiscoveryService.userName + settings.domain = autoDiscoveryService.domain + accountCreationAgent.busyPageInstance.settingsRetrieved = true + accountCreationAgent.busyPageInstance.operationSucceeded() + accountCreationDialog.save() + } + onAutoDiscoveryFailed: { + console.log("[jsa-eas] AutoDiscovery FAILED: error == " + error) + accountCreationAgent.busyPageInstance.settingsRetrieved = false + accountCreationDialog.showSettingsDiscoveryError = true + accountCreationDialog.showManualSettings = true + // Don't emit error here, just show manual config page + accountCreationAgent.busyPageInstance.operationSucceeded() + } + } + + CheckCredentials { + id: checkCredentialsService + + function finishCredentials() { + var component = Qt.createComponent(Qt.resolvedUrl("SailfishEasSettingsDialog.qml")) + if (component.status === Component.Ready) { + accountCreationAgent.settingsDialog = component.createObject(accountCreationAgent, + { + "accountId": account.identifier, + "connectionSettings": settings, + "oauthEnabled": true + }) + accountCreationAgent.busyPageInstance.operationSucceeded() + } else { + console.log(component.errorString()) + } + } + + onCheckCredentialsDone: { + console.log("[jsa-eas] Credentials OK!") + // create the settings dialog after certificate data is handled too + if (settings.hasSslCertificate) { + settings.storeCertificateData() + } else { + finishCredentials() + } + account.credentialsChecked = true + } + onCheckCredentialsFailed: { + console.log("[jsa-eas] Credentials check FAILED: error == " + error) + if (error === CheckCredentials.CHECKCREDENTIALS_ERROR_SLL_HANDSHAKE) { + accountCreationDialog.taskFailed("SSL failed") + } else if (error !== CheckCredentials.CHECKCREDENTIALS_ERROR_CANCELED) { + accountCreationDialog.taskFailed("CC failed") + } + } + } + + Component { + id: busyPageComponent + SailfishEasBusyPage { + property bool _skipping + property bool settingsRetrieved + backNavigation: (state == "info") || accountCreationDialog.delayTask + + function operationSucceeded() { + _errorOccured = false + if (currentTask === "checkCredentials") { + pageStack.animatorReplace(settingsDialog) + currentTask = "creatingAccount" + } else if (currentTask === "autodiscovery") { + if (settingsRetrieved) { + currentTask = "checkCredentials" + } else { + pageStack.pop() + } + } else if (currentTask === "checkProvisioning") { + accountCreationAgent.goToEndDestination() + } + } + + function runAccountCreation() { + if (currentTask === "autodiscovery") { + accountCreationDialog.acceptInitialSetup() + } else if (currentTask === "checkCredentials") { + accountCreationDialog.save() + } else if (currentTask === "checkProvisioning") { + settingsDialog.accountSaveSync() + } else if (currentTask === "savingAccount") { + settingsDialog.accountSaveSync() + accountCreationAgent.goToEndDestination() + } + } + + function cancelAccountCreation() { + accountCreationAgent.busyPageInstance.settingsRetrieved = false + accountCreationDialog.showSettingsDiscoveryError = true + // Don't emit error here, just show manual config page + accountCreationAgent.busyPageInstance.operationSucceeded() + } + + onStatusChanged: { + if (status === PageStatus.Active) { + if (connectionHelper.online) { + runAccountCreation() + } else { + accountCreationDialog.delayTask = true + connectionHelper.attemptToConnectNetwork() + } + } else if (status === PageStatus.Inactive) { + accountCreationDialog.delayTask = false + } + } + + onInfoButtonClicked: { + _skipping = true + // we are in skip mode, so remove the account + account.remove() + accountCreationAgent.goToEndDestination() + } + + onPageContainerChanged: { + if (pageContainer == null && !_skipping) { + accountCreationDialog.focus = true + + if (currentTask == "checkCredentials" && _errorOccured) { + // We are coming back from check credentials error + // Reset everything + accountCreationDialog.showSettingsDiscoveryError = false + accountCreationDialog.showManualSettings = true + } else if (currentTask == "checkProvisioning" && _errorOccured) { + state = "busy" + } + } + } + + Component.onDestruction: { + if (status == PageStatus.Active) { + // app closed while setup is in progress, remove account + account.remove() + } + } + } + } + + Account { + id: account + + property bool finishCheckCredentials + property bool incomingCredentialsCreated + property bool credentialsChecked + property string _accessToken // For repeated credentials check + + onStatusChanged: { + if (status === Account.Synced) { + if (finishCheckCredentials) { + console.log("finishing credentials from account") + finishCheckCredentials = false + checkCredentialsService.finishCredentials() + } else if (!incomingCredentialsCreated) { + console.log("creating sign in credentials for account") + incomingCredentialsCreated = true + var params = account.signInParameters(accountCreationDialog.defaultServiceName) + params.setParameter("ClientId", keyProvider.clientId()); + account.createSignInCredentials("Jolla", "ActiveSync", params) + } else if (!credentialsChecked && _accessToken !== "") { + console.log("checking credentials for account") + checkCredentials() + } else { + console.log("create profiles for account") + accountSyncManager.createProfile("sailfisheas.Email", identifier, "sailfisheas-oauth-email") + accountSyncManager.createProfile("sailfisheas.Calendars", identifier, "sailfisheas-oauth-calendars") + accountSyncManager.createProfile("sailfisheas.Contacts", identifier, "sailfisheas-oauth-contacts") + } + } else if (status === Account.Error) { + console.log("ActiveSync provider account error:", errorMessage) + accountCreationAgent.accountCreationError(errorMessage) + } + } + + function checkCredentials() { + if (settings.hasSslCertificate) { + checkCredentialsService.checkOAuthCredentials(_accessToken, settings.server, settings.port, + settings.secureConnection, settings.acceptSSLCertificates, + settings.sslCertificatePath, settings.sslCertificatePassword) + } else { + checkCredentialsService.checkOAuthCredentials(_accessToken, settings.server, settings.port, + settings.secureConnection, settings.acceptSSLCertificates) + } + } + + onSignInCredentialsCreated: { + accountCreationAgent.accountCreated(account.identifier) + _accessToken = data["AccessToken"] + checkCredentials() + } + + onSignInError: { + console.log("ActiveSync provider account error:", message) + accountCreationAgent.accountCreationError(message) + account.remove() + } + } + + AccountSyncManager { + id: accountSyncManager + } +} diff --git a/usr/share/accounts/ui/sip-settings.qml b/usr/share/accounts/ui/sip-settings.qml new file mode 100644 index 00000000..e192be00 --- /dev/null +++ b/usr/share/accounts/ui/sip-settings.qml @@ -0,0 +1,62 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Sailfish.Accounts 1.0 +import com.jolla.settings.accounts 1.0 + +AccountSettingsAgent { + id: root + + initialPage: Page { + onPageContainerChanged: { + if (pageContainer == null) { + root.delayDeletion = true + settingsDisplay.saveAccount() + } + } + + Component.onDestruction: { + if (status == PageStatus.Active && !credentialsUpdater.running) { + // app closed while settings are open, so save settings synchronously + settingsDisplay.saveAccount(true) + } + } + + SilicaFlickable { + anchors.fill: parent + contentHeight: header.height + settingsDisplay.height + Theme.paddingLarge + + StandardAccountSettingsPullDownMenu { + allowSync: false + allowCredentialsUpdate: root.accountNotSignedIn + + onCredentialsUpdateRequested: credentialsUpdater.replaceWithCredentialsUpdatePage(root.accountId) + onAccountDeletionRequested: { + root.accountDeletionRequested() + pageStack.pop() + } + } + + PageHeader { + id: header + title: root.accountsHeaderText + } + + SIPSettingsDisplay { + id: settingsDisplay + anchors.top: header.bottom + accountProvider: root.accountProvider + accountId: root.accountId + + onAccountSaveCompleted: { + root.delayDeletion = false + } + } + + VerticalScrollDecorator {} + } + + AccountCredentialsUpdater { + id: credentialsUpdater + } + } +} diff --git a/usr/share/accounts/ui/sip-update.qml b/usr/share/accounts/ui/sip-update.qml new file mode 100644 index 00000000..131f9db3 --- /dev/null +++ b/usr/share/accounts/ui/sip-update.qml @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2015 - 2020 Jolla Ltd. + * Copyright (c) 2020 Open Mobile Platform LLC. + * + * License: Proprietary + */ + +import QtQuick 2.0 +import com.jolla.settings.accounts 1.0 + +AccountCredentialsAgent { + id: root + + canCancelUpdate: true + + initialPage: CredentialsUpdateDialog { + serviceName: "sip" + applicationName: "Jolla" + credentialsName: "Jolla" + account.identifier: root.accountId + providerIcon: root.accountProvider.iconName + providerName: root.accountProvider.displayName + + onCredentialsUpdated: { + root.credentialsUpdated(identifier) + root.goToEndDestination() + } + + onCredentialsUpdateError: root.credentialsUpdateError(message) + } +} diff --git a/usr/share/accounts/ui/sip.qml b/usr/share/accounts/ui/sip.qml new file mode 100644 index 00000000..00ad55e7 --- /dev/null +++ b/usr/share/accounts/ui/sip.qml @@ -0,0 +1,162 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Sailfish.Accounts 1.0 +import com.jolla.settings.accounts 1.0 + +AccountCreationAgent { + id: root + + property Item _settingsDialog + + initialPage: Dialog { + canAccept: settings.acceptableInput + acceptDestination: busyComponent + + SilicaFlickable { + anchors.fill: parent + contentHeight: contentColumn.height + Theme.paddingLarge + + Column { + id: contentColumn + width: parent.width + + DialogHeader { + dialog: initialPage + } + + Item { + x: Theme.horizontalPageMargin + width: parent.width - x*2 + height: icon.height + Theme.paddingLarge + + Image { + id: icon + width: Theme.iconSizeLarge + height: width + anchors.top: parent.top + source: root.accountProvider.iconName + } + Label { + anchors { + left: icon.right + leftMargin: Theme.paddingLarge + right: parent.right + verticalCenter: icon.verticalCenter + } + text: root.accountProvider.displayName + color: Theme.highlightColor + font.pixelSize: Theme.fontSizeLarge + truncationMode: TruncationMode.Fade + } + } + + SIPCommon { + id: settings + } + } + + VerticalScrollDecorator {} + } + } + + Component { + id: busyComponent + AccountBusyPage { + onStatusChanged: { + if (status == PageStatus.Active) { + accountFactory.beginCreation() + } + } + } + } + + AccountFactory { + id: accountFactory + function beginCreation() { + var configuration = {} + + for (var i = 0; i < settings.children.length; i++) { + var item = settings.children[i] + var value + + if (!item._tpType) continue + + if (item._tpType == 's') + value = item.text == item._tpDefault || item.text === '' ? null : item.text + else if (item._tpType == 'b') + value = item.checked == item._tpDefault ? null : item.checked + else if (item._tpType == 'e') + value = item.currentItem._tpValue == item._tpDefault ? null : item.currentItem._tpValue + + if (value !== null) { + var tpParam = 'telepathy/' + item._tpParam + + console.log(tpParam + ' = ' + value) + configuration[tpParam] = value + } + } + + createAccount(root.accountProvider.name, + root.accountProvider.serviceNames[0], + settings.account, settings.password, + settings.account, + { "sip": configuration }, // configuration map + "Jolla", // applicationName + "", // symmetricKey + "Jolla") // credentialsName + } + + onError: { + console.log("SIP creation error:", message) + initialPage.acceptDestinationInstance.state = "info" + initialPage.acceptDestinationInstance.infoExtraDescription = message + root.accountCreationError(message) + } + + onSuccess: { + root._settingsDialog = settingsComponent.createObject(root, {"accountId": newAccountId}) + pageStack.animatorPush(root._settingsDialog) + root.accountCreated(newAccountId) + } + } + + Component { + id: settingsComponent + Dialog { + property alias accountId: settingsDisplay.accountId + + acceptDestination: root.endDestination + acceptDestinationAction: root.endDestinationAction + acceptDestinationProperties: root.endDestinationProperties + acceptDestinationReplaceTarget: root.endDestinationReplaceTarget + backNavigation: false + + onAccepted: { + root.delayDeletion = true + settingsDisplay.saveAccount() + } + + SilicaFlickable { + anchors.fill: parent + contentHeight: header.height + settingsDisplay.height + Theme.paddingLarge + + DialogHeader { + id: header + } + + SIPSettingsDisplay { + id: settingsDisplay + anchors.top: header.bottom + accountProvider: root.accountProvider + autoEnableAccount: true + + onAccountSaveCompleted: { + root.delayDeletion = false + } + } + + VerticalScrollDecorator {} + } + } + } +} diff --git a/usr/share/booster-silica-media/preload.qml b/usr/share/booster-silica-media/preload.qml index 2e00c179..a39faa81 100644 --- a/usr/share/booster-silica-media/preload.qml +++ b/usr/share/booster-silica-media/preload.qml @@ -3,10 +3,10 @@ import Sailfish.Silica 1.0 import Sailfish.Gallery 1.0 import QtMultimedia 5.4 import QtDocGallery 5.0 -import org.nemomobile.ngf 1.0 -import org.nemomobile.dbus 2.0 -import org.nemomobile.policy 1.0 -import org.nemomobile.thumbnailer 1.0 +import Nemo.Ngf 1.0 +import Nemo.DBus 2.0 +import Nemo.Policy 1.0 +import Nemo.Thumbnailer 1.0 ApplicationWindow { cover: null // don't create a cover - the switcher will try to show it diff --git a/usr/share/cameragallery/qml/cameragallery.qml b/usr/share/cameragallery/qml/cameragallery.qml deleted file mode 100644 index 31bb0b56..00000000 --- a/usr/share/cameragallery/qml/cameragallery.qml +++ /dev/null @@ -1,211 +0,0 @@ -/* -Copyright (c) 2021 Jolla Ltd. -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in - the documentation and/or other materials provided with the - distribution. - * Neither the name of the Jolla Ltd. nor the names of - its contributors may be used to endorse or promote products - derived from this software without specific prior written - permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL JOLLA LTD OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -*/ - -import QtQuick 2.0 -import Sailfish.Silica 1.0 -import QtMultimedia 5.6 -import CameraGallery 1.0 -import Nemo.KeepAlive 1.2 -import "pages" - -ApplicationWindow { - id: mainWindow - - background.color: "black" - cover: Qt.resolvedUrl("cover/CameraTestCover.qml") - allowedOrientations: Orientation.All - _defaultPageOrientations: Orientation.All - - initialPage: Component { - Page { - id: mainPage - - property bool pushAttached: status === PageStatus.Active - onPushAttachedChanged: { - pageStack.pushAttached(Qt.resolvedUrl("pages/SettingsPage.qml"), { 'camera': camera }) - pushAttached = false - } - - DisplayBlanking { - preventBlanking: camera.videoRecorder.recorderState == CameraRecorder.RecordingState - } - - Binding { - target: CameraConfigs - property: "camera" - value: camera - } - - VideoOutput { - id: videoOutput - - z: -1 - width: parent.width - height: parent.height - fillMode: VideoOutput.PreserveAspectFit - source: Camera { - id: camera - - imageCapture.onImageSaved: preview.source = path - videoRecorder { - frameRate: 30 - audioChannels: 2 - audioSampleRate: 48000 - audioCodec: "audio/mpeg, mpegversion=(int)4" - audioEncodingMode: CameraRecorder.AverageBitRateEncoding - videoCodec: "video/x-h264" - mediaContainer: "video/quicktime, variant=(string)iso" - resolution: "1280x720" - videoBitRate: 12000000 - } - } - - // When another camera app is opened - // the camera here goes to unloaded state. - // Make sure the camera becomes active again - Connections { - target: Qt.application - onActiveChanged: { - if (Qt.application.active) { - camera.cameraState = previousState - } else { - previousState = camera.cameraState - } - } - property int previousState: camera.cameraState - } - - MouseArea { - anchors.fill: parent - onClicked: { - if (videoOutput.state == "miniature") { - pageStack.navigateBack() - } else { - pageStack.navigateForward() - } - } - } - - states: State { - name: "miniature" - when: mainPage.status === PageStatus.Inactive || mainPage.status === PageStatus.Activating - PropertyChanges { - target: videoOutput - parent: pageStack - z: 1000 - width: Theme.itemSizeExtraLarge - height: width - x: parent.width - width - Theme.paddingLarge - y: parent.height - height - Theme.paddingLarge - } - } - } - - PageHeader { - z: 1 - title: "Camera settings" - interactive: true - } - - MouseArea { - width: Theme.itemSizeExtraLarge - height: Theme.itemSizeExtraLarge - - anchors { - horizontalCenter: parent.horizontalCenter - bottom: parent.bottom - bottomMargin: Theme.paddingLarge - } - - onPressed: camera.searchAndLock() - onReleased: { - if (camera.captureMode === Camera.CaptureVideo) { - if (camera.videoRecorder.recorderState == CameraRecorder.RecordingState) { - camera.videoRecorder.stop() - } else { - camera.videoRecorder.record() - } - } else { - if (containsMouse) { - camera.imageCapture.capture() - } else { - camera.unlock() - } - } - } - onCanceled: camera.unlockAutoFocus() - - Rectangle { - id: backgroundCircle - - radius: width / 2 - width: image.width - height: width - - anchors.centerIn: parent - - color: Theme.secondaryHighlightColor - } - - Image { - id: image - anchors.centerIn: parent - source: camera.videoRecorder.recorderState == CameraRecorder.RecordingState - ? "image://theme/icon-camera-video-shutter-off" - : (camera.captureMode == Camera.CaptureVideo - ? "image://theme/icon-camera-video-shutter-on" - : "image://theme/icon-camera-shutter") - } - } - - MouseArea { - - onClicked: Qt.openUrlExternally(preview.source) - - anchors { - left: parent.left - bottom: parent.bottom - margins: Theme.paddingLarge - } - - width: Theme.itemSizeExtraLarge - height: Theme.itemSizeExtraLarge - opacity: containsMouse && pressed ? 0.6 : 1.0 - - Image { - id: preview - z: -1 - anchors.fill: parent - fillMode: Image.PreserveAspectFit - } - } - } - } -} diff --git a/usr/share/cameragallery/qml/cover/CameraTestCover.qml b/usr/share/cameragallery/qml/cover/CameraTestCover.qml deleted file mode 100644 index 0a75d900..00000000 --- a/usr/share/cameragallery/qml/cover/CameraTestCover.qml +++ /dev/null @@ -1,39 +0,0 @@ -/* -Copyright (c) 2021 Jolla Ltd. -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in - the documentation and/or other materials provided with the - distribution. - * Neither the name of the Jolla Ltd. nor the names of - its contributors may be used to endorse or promote products - derived from this software without specific prior written - permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL JOLLA LTD OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -*/ - -import QtQuick 2.0 -import Sailfish.Silica 1.0 - -CoverBackground { - CoverPlaceholder { - text: "Camera Gallery" - icon.source: "image://theme/icon-launcher-camera" - } -} diff --git a/usr/share/cameragallery/qml/pages/SettingsPage.qml b/usr/share/cameragallery/qml/pages/SettingsPage.qml deleted file mode 100644 index bd9c718d..00000000 --- a/usr/share/cameragallery/qml/pages/SettingsPage.qml +++ /dev/null @@ -1,1119 +0,0 @@ -/* -Copyright (c) 2021 Jolla Ltd. -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in - the documentation and/or other materials provided with the - distribution. - * Neither the name of the Jolla Ltd. nor the names of - its contributors may be used to endorse or promote products - derived from this software without specific prior written - permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL JOLLA LTD OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -*/ - -import QtQuick 2.6 -import Sailfish.Silica 1.0 -import QtMultimedia 5.4 -import CameraGallery 1.0 - -Page { - function formatResolution(size) { - if (size.width == -1) { - return "Unselected" - } else { - return size.width + "x" + size.height - } - } - - property Camera camera - - backgroundColor: "black" - - SilicaFlickable { - anchors.fill: parent - contentHeight: column.height - - Column { - id: column - - width: parent.width - bottomPadding: Theme.paddingLarge - - PageHeader { - title: "Camera settings" - } - - Label { - x: Theme.horizontalPageMargin - width: parent.width - 2*x - text: "Note not all the image processing modes exposed by QtMultimedia are supported." - font.pixelSize: Theme.fontSizeSmall - color: Theme.secondaryHighlightColor - wrapMode: Text.Wrap - } - - ComboBox { - id: cameraMenu - label: "Camera" - menu: ContextMenu { - Repeater { - model: QtMultimedia.availableCameras.length - MenuItem { - text: QtMultimedia.availableCameras[model.index].displayName - onDelayedClick: { - camera.imageCapture.resolution = "-1x-1" - camera.videoRecorder.resolution = "-1x-1" - cameraMenu.currentIndex = model.index - cameraMenu.value = text - camera.deviceId = QtMultimedia.availableCameras[model.index].deviceId - } - } - } - } - } - - ComboBox { - id: cameraState - - function name(state) { - switch (state) { - case Camera.UnloadedState: - return "Unloaded" - case Camera.LoadedState: - return "Loaded" - case Camera.ActiveState: - return "Active" - default: - return "Unknown" - } - } - label: "State" - value: name(camera.cameraState) - menu: ContextMenu { - Repeater { - model: [Camera.UnloadedState, Camera.LoadedState, Camera.ActiveState] - - MenuItem { - text: cameraState.name(modelData) - onDelayedClick: camera.cameraState = modelData - } - } - } - } - - DetailItem { - label: "Status" - value: { - switch (camera.cameraStatus) { - case Camera.ActiveStatus: - return "Active" - case Camera.StartingStatus: - return "Starting" - case Camera.StoppingStatus: - return "Stopping" - case Camera.StandbyStatus: - return "Stand-by" - case Camera.LoadedStatus: - return "Loaded" - case Camera.LoadingStatus: - return "Loading" - case Camera.UnloadingStatus: - return "Unloading" - case Camera.UnloadedStatus: - return "Unloaded" - case Camera.UnavailableStatus: - return "Unavailable" - default: - return "Unknown" - } - } - } - - DetailItem { - label: "Lock status" - value: { - switch (camera.lockStatus) { - case Camera.Unlocked: - return "Unlocked" - case Camera.Searching: - return "Searching" - case Camera.Locked: - return "Locked" - default: - return "Unknown" - } - } - } - - DetailItem { - id: filePathItem - label: "Availability" - value: { - switch (camera.availability) { - case Camera.Available: - return "Available" - case Camera.Unavailable: - return "Unavailable" - case Camera.ResourceMissing: - return "Resource missing" - } - } - } - - DetailItem { - label: "Position" - value: { - switch (camera.position) { - case Camera.UnspecifiedPosition: - return "Unspecified" - case Camera.BackFace: - return "Back-facing" - case Camera.FrontFace: - return "Front-facing" - default: - return "Unknown" - } - } - } - - DetailItem { - label: "Orientation" - value: camera.orientation + "°" - } - - ComboBox { - label: "Resolution" - value: formatResolution(camera.viewfinder.resolution) - - menu: ContextMenu { - Repeater { - model: CameraConfigs.supportedViewfinderResolutions - MenuItem { - text: formatResolution(modelData) - onClicked: camera.viewfinder.resolution = modelData - } - } - } - } - - DetailItem { - label: "Last error code" - value: camera.errorCode - } - - DetailItem { - label: "Error description" - value: camera.errorString == "" ? "No error" - : camera.errorString - } - - SectionHeader { - text: "Zoom" - } - - Slider { - minimumValue: 1.0 - maximumValue: camera.maximumDigitalZoom - value: camera.digitalZoom - label: "Zoom" - width: parent.width - onValueChanged: camera.digitalZoom = value - } - - SectionHeader { - text: "Flash" - } - - DetailItem { - label: "Ready" - value: camera.flash.isFlashReady ? "True" : "False" - } - - ComboBox { - id: flashMode - - function name(mode) { - switch (mode) { - case Camera.FlashAuto: - return "Auto" - case Camera.FlashOff: - return "Off" - case Camera.FlashOn: - return "On" - case Camera.FlashRedEyeReduction: - return "Red-eye reduction" - case Camera.FlashFill: - return "Fill" - case Camera.FlashTorch: - return "Torch" - case Camera.FlashVideoLight: - return "Video light" - case Camera.FlashSlowSyncFrontCurtain: - return "Slow sync front curtain" - case Camera.FlashSlowSyncRearCurtain: - return "Slow sync rear curtain" - case Camera.FlashManual: - return "Manual" - default: - return "Unknown" - } - } - - label: "Mode" - enabled: CameraConfigs.supportedFlashModes.length > 0 - value: name(camera.flash.mode) - - menu: ContextMenu { - Repeater { - model: CameraConfigs.supportedFlashModes - - MenuItem { - text: flashMode.name(modelData) - onClicked: camera.flash.mode = modelData - } - } - } - } - - SectionHeader { - text: "Capture mode" - } - - ComboBox { - id: captureMenu - label: "Capture mode" - currentIndex: camera.captureMode - menu: ContextMenu { - MenuItem { - text: "Viewfinder" - onDelayedClick: camera.captureMode = Camera.CaptureViewfinder - } - MenuItem { - text: "Still Image" - onDelayedClick: camera.captureMode = Camera.CaptureStillImage - } - MenuItem { - text: "Video" - onDelayedClick: camera.captureMode = Camera.CaptureVideo - } - } - } - - Column { - width: parent.width - visible: camera.captureMode === Camera.CaptureStillImage - - DetailItem { - label: "Ready" - value: camera.imageCapture.ready ? "True" : "False" - } - - ComboBox { - label: "Resolution" - value: formatResolution(camera.imageCapture.resolution) - - menu: ContextMenu { - Repeater { - model: CameraConfigs.supportedImageResolutions - MenuItem { - text: formatResolution(modelData) - onClicked: camera.imageCapture.resolution = modelData - } - } - } - } - - DetailItem { - label: "Last image path" - value: camera.imageCapture.capturedImagePath - } - - DetailItem { - label: "Description" - value: camera.imageCapture.errorString == "" ? "No error" - : camera.errorString - } - } - - Column { - width: parent.width - visible: camera.captureMode === Camera.CaptureVideo - - DetailItem { - label: "State" - value: { - switch (camera.videoRecorder.recorderState) { - case CameraRecorder.StoppedState: - return "Stopped" - case CameraRecorder.RecordingState: - return "Recording" - default: - return "Unknown" - } - } - } - - DetailItem { - label: "Status" - value: { - switch (camera.videoRecorder.recorderStatus) { - case CameraRecorder.ActiveStatus: - return "Active" - case CameraRecorder.StartingStatus: - return "Starting" - case CameraRecorder.RecordingStatus: - return "Recording" - case CameraRecorder.PausedStatus: - return "Paused" - case CameraRecorder.FinalizingStatus: - return "Finalizing" - case CameraRecorder.LoadedStatus: - return "Loaded" - case CameraRecorder.LoadingStatus: - return "Loading" - case CameraRecorder.UnloadedStatus: - return "Unloaded" - case CameraRecorder.UnavailableStatus: - return "Unavailable" - default: - return "Unknown" - } - } - } - - ComboBox { - label: "Resolution" - value: formatResolution(camera.videoRecorder.resolution) - - menu: ContextMenu { - Repeater { - model: CameraConfigs.supportedVideoResolutions - MenuItem { - text: formatResolution(modelData) - onClicked: camera.videoRecorder.resolution = modelData - } - } - } - } - - DetailItem { - label: "Actual location" - value: camera.videoRecorder.actualLocation - } - - DetailItem { - label: "Output location" - value: camera.videoRecorder.outputLocation - } - - DetailItem { - label: "Video bit rate" - value: camera.videoRecorder.videoBitRate + "b/s" - } - - DetailItem { - label: "Video codec" - value: camera.videoRecorder.videoCodec - } - - DetailItem { - label: "Video encoding mode" - value: { - switch (camera.videoRecorder.videoEncodingMode) { - case CameraRecorder.ConstantQualityEncoding: - return "Constant quality" - case CameraRecorder.ConstantBitRateEncoding: - return "Constant bit rate" - case CameraRecorder.AverageBitRateEncoding: - return "Average bit rate" - default: - return "Unknown" - } - } - } - - DetailItem { - label: "Audio bit rate" - value: camera.videoRecorder.audioBitRate + "b/s" - } - - DetailItem { - label: "Audio channels" - value: camera.videoRecorder.audioChannels - } - - DetailItem { - label: "Audio codec" - value: camera.videoRecorder.audioCodec - } - - DetailItem { - label: "Audio encoding mode" - value: { - switch (camera.videoRecorder.audioEncodingMode) { - case CameraRecorder.ConstantQualityEncoding: - return "Constant quality" - case CameraRecorder.ConstantBitRateEncoding: - return "Constant bit rate" - case CameraRecorder.AverageBitRateEncoding: - return "Average bit rate" - default: - return "Unknown" - } - } - } - - DetailItem { - label: "Audio sample rate" - value: camera.videoRecorder.audioSampleRate - } - - DetailItem { - label: "Duration" - value: camera.videoRecorder.duration - } - - DetailItem { - label: "Framerate" - value: camera.videoRecorder.frameRate.toFixed(1) - } - - DetailItem { - label: "Media container" - value: camera.videoRecorder.mediaContainer - } - - DetailItem { - label: "Muted" - value: camera.videoRecorder.muted ? "True" : "False" - } - DetailItem { - label: "Last error code" - value: camera.videoRecorder.errorCode - } - - DetailItem { - label: "Error description" - value: camera.videoRecorder.errorString == "" ? "No error" - : camera.errorString - } - } - - SectionHeader { - text: "Focus" - } - - ComboBox { - id: focusMode - - function name(mode) { - switch (mode) { - case Camera.FocusManual: - return "Manual" - case Camera.FocusHyperfocal: - return "Hyperfocal" - case Camera.FocusInfinity: - return "Infinity" - case Camera.FocusAuto: - return "Auto" - case Camera.FocusContinuous: - return "Continuous" - case Camera.FocusMacro: - return "Macro" - default: - return "Unknown" - } - } - label: "Mode" - enabled: CameraConfigs.supportedFocusModes.length > 0 - value: name(camera.focus.focusMode) - - menu: ContextMenu { - Repeater { - model: CameraConfigs.supportedFocusModes - - MenuItem { - text: focusMode.name(modelData) - onClicked: camera.focus.focusMode = modelData - } - } - } - } - - ComboBox { - id: pointMode - - function name(mode) { - switch (mode) { - case Camera.FocusPointAuto: - return "Auto" - case Camera.FocusPointCenter: - return "Center" - case Camera.FocusPointFaceDetection: - return "Face detection" - case Camera.FocusPointCustom: - return "Custom" - default: - return "Unknown" - } - } - label: "Point mode" - enabled: CameraConfigs.supportedFocusPointModes.length > 0 - value: name(camera.focus.focusPointMode) - menu: ContextMenu { - Repeater { - model: CameraConfigs.supportedFocusPointModes - MenuItem { - text: pointMode.name(modelData) - onClicked: camera.focus.focusPointMode = modelData - } - } - } - } - - DetailItem { - label: "Custom focus point" - value: camera.focus.customFocusPoint.x + ", " + camera.focus.customFocusPoint.y - } - - SectionHeader { - text: "Exposure" - } - - ComboBox { - id: exposureMode - - function name(mode) { - switch (mode) { - case Camera.ExposureManual: - return "Manual" - case Camera.ExposureAuto: - return "Auto" - case Camera.ExposureNight: - return "Night" - case Camera.ExposureBacklight: - return "Backlight" - case Camera.ExposureSpotlight: - return "Spotlight" - case Camera.ExposureSports: - return "Sports" - case Camera.ExposureSnow: - return "Snow" - case Camera.ExposureBeach: - return "Beach" - case Camera.ExposureLargeAperture: - return "Large aperture" - case Camera.ExposureSmallAperture: - return "Small aperture" - case Camera.ExposurePortrait: - return "Portrait" - case Camera.ExposureAction: - return "Action" - case Camera.ExposureLandscape: - return "Landscape" - case Camera.ExposureNightPortrait: - return "Night portrait" - case Camera.ExposureTheatre: - return "Theatre" - case Camera.ExposureSunset: - return "Sunset" - case Camera.ExposureSteadyPhoto: - return "Steady photo" - case Camera.ExposureFireworks: - return "Fireworks" - case Camera.ExposureParty: - return "Party" - case Camera.ExposureCandlelight: - return "Candlelight" - case Camera.ExposureBarcode: - return "Barcode" - case Camera.ExposureFlowers: - return "Flowers" - case Camera.ExposureAR: - return "AR" - case Camera.ExposureCloseup: - return "Closeup" - case Camera.ExposureHDR: - return "HDR" - case Camera.ExposureModeVendor: - return "Mode vendor" - default: - return "Unknown" - } - } - label: "Mode" - enabled: CameraConfigs.supportedExposureModes.length > 0 - value: name(camera.exposure.exposureMode) - menu: ContextMenu { - Repeater { - model: CameraConfigs.supportedExposureModes - MenuItem { - text: exposureMode.name(modelData) - onClicked: camera.exposure.exposureMode = modelData - } - } - } - } - - - /* - Uncomment when works - DetailItem { - label: "Shutter speed" - value: camera.exposure.shutterSpeed.toFixed(2) - } - - TextField { - label: "Manual shutter speed" - inputMethodHints: Qt.ImhFormattedNumbersOnly - text: camera.exposure.manualShutterSpeed.toFixed(2) - onTextChanged: camera.exposure.manualShutterSpeed = parseFloat(text) - } - */ - - Slider { - minimumValue: -4.0 - maximumValue: 4.0 - stepSize: 1.0 - value: camera.exposure.exposureCompensation - label: "Compensation" - valueText: camera.exposure.exposureCompensation.toFixed(1) - width: parent.width - onValueChanged: camera.exposure.exposureCompensation = value - } - - DetailItem { - label: "ISO" - value: camera.exposure.iso - } - - ComboBox { - label: "Manual ISO" - enabled: CameraConfigs.supportedIsoSensitivities.length > 0 - value: camera.exposure.manualIso.toString() - - menu: ContextMenu { - Repeater { - model: CameraConfigs.supportedIsoSensitivities - - MenuItem { - text: modelData.toString() - onClicked: camera.exposure.manualIso = modelData - } - } - } - } - - DetailItem { - label: "Aperture" - value: camera.exposure.aperture.toFixed(2) - } - - ComboBox { - id: meteringMode - - function name(mode) { - switch (mode) { - case Camera.MeteringMatrix: - return "Matrix" - case Camera.MeteringAverage: - return "Average" - case Camera.MeteringSpot: - return "Spot" - default: - return "Unknown" - } - } - label: "Metering mode" - enabled: CameraConfigs.supportedMeteringModes.length > 0 - value: name(camera.exposure.meteringMode) - menu: ContextMenu { - Repeater { - model: CameraConfigs.supportedMeteringModes - - MenuItem { - text: meteringMode.name(modelData) - onClicked: camera.exposure.meteringMode = modelData - } - } - } - } - - DetailItem { - label: "Spot metering point" - value: camera.exposure.spotMeteringPoint.x + ", " + camera.exposure.spotMeteringPoint.y - } - - SectionHeader { - text: "Image processing" - } - - ComboBox { - id: whiteBalanceMode - - function name(mode) { - switch (mode) { - case CameraImageProcessing.WhiteBalanceManual: - return "Manual" - case CameraImageProcessing.WhiteBalanceAuto: - return "Auto" - case CameraImageProcessing.WhiteBalanceSunlight: - return "Sunlight" - case CameraImageProcessing.WhiteBalanceCloudy: - return "Cloudy" - case CameraImageProcessing.WhiteBalanceShade: - return "Shade" - case CameraImageProcessing.WhiteBalanceTungsten: - return "Tungsten" - case CameraImageProcessing.WhiteBalanceFluorescent: - return "Fluorescent" - case CameraImageProcessing.WhiteBalanceFlash: - return "Flash" - case CameraImageProcessing.WhiteBalanceSunset: - return "Sunset" - case CameraImageProcessing.WhiteBalanceWarmFluorescent: - return "Warm fluorescent" - case CameraImageProcessing.WhiteBalanceVendor: - return "Vendor" - default: - return "Unknown" - } - } - - label: "White balance mode" - enabled: CameraConfigs.supportedWhiteBalanceModes.length > 0 - value: name(camera.imageProcessing.whiteBalanceMode) - - menu: ContextMenu { - Repeater { - model: CameraConfigs.supportedWhiteBalanceModes - - MenuItem { - - text: whiteBalanceMode.name(modelData) - onClicked: camera.imageProcessing.whiteBalanceMode = modelData - } - } - } - } - - Slider { - visible: camera.imageProcessing.whiteBalanceMode === CameraImageProcessing.WhiteBalanceManual - minimumValue: 2000 - maximumValue: 9000 - value: camera.imageProcessing.manualWhiteBalance - label: "Manual white balance" - valueText: camera.imageProcessing.manualWhiteBalance.toFixed(0) - width: parent.width - onValueChanged: camera.imageProcessing.manualWhiteBalance = value - } - - ComboBox { - id: colorFilter - - function name(filter) { - switch (filter) { - case CameraImageProcessing.ColorFilterNone: - return "None" - case CameraImageProcessing.ColorFilterGrayscale: - return "Grayscale" - case CameraImageProcessing.ColorFilterNegative: - return "Negative" - case CameraImageProcessing.ColorFilterSolarize: - return "Solarize" - case CameraImageProcessing.ColorFilterSepia: - return "Sepia" - case CameraImageProcessing.ColorFilterPosterize: - return "Posterize" - case CameraImageProcessing.ColorFilterWhiteboard: - return "Whiteboard" - case CameraImageProcessing.ColorFilterBlackboard: - return "Blackboard" - case CameraImageProcessing.ColorFilterAqua: - return "Aqua" - case CameraImageProcessing.ColorFilterEmboss: - return "Emboss" - case CameraImageProcessing.ColorFilterSketch: - return "Sketch" - case CameraImageProcessing.ColorFilterNeon: - return "Neon" - case CameraImageProcessing.ColorFilterVendor: - return "Vendor" - default: - return "Unknown" - } - } - label: "Color filter" - value: name(camera.imageProcessing.colorFilter) - menu: ContextMenu { - Repeater { - model: [CameraImageProcessing.ColorFilterNone, CameraImageProcessing.ColorFilterGrayscale, CameraImageProcessing.ColorFilterNegative, CameraImageProcessing.ColorFilterSolarize, CameraImageProcessing.ColorFilterSepia, CameraImageProcessing.ColorFilterPosterize, CameraImageProcessing.ColorFilterWhiteboard, CameraImageProcessing.ColorFilterBlackboard, CameraImageProcessing.ColorFilterAqua, CameraImageProcessing.ColorFilterEmboss, CameraImageProcessing.ColorFilterSketch, CameraImageProcessing.ColorFilterNeon, CameraImageProcessing.ColorFilterVendor] - - MenuItem { - text: colorFilter.name(modelData) - onClicked: camera.imageProcessing.colorFilter = modelData - } - } - } - } - - /* - Uncomment when works - Slider { - minimumValue: -1.0 - maximumValue: 1.0 - value: camera.imageProcessing.contrast - label: "Contrast" - valueText: camera.imageProcessing.contrast.toFixed(2) - width: parent.width - onValueChanged: camera.imageProcessing.contrast = value - } - - Slider { - minimumValue: -1.0 - maximumValue: 1.0 - value: camera.imageProcessing.denoisingLevel - label: "Denoising level" - valueText: camera.imageProcessing.denoisingLevel.toFixed(2) - width: parent.width - onValueChanged: camera.imageProcessing.denoisingLevel = value - } - - Slider { - minimumValue: -1.0 - maximumValue: 1.0 - value: camera.imageProcessing.saturation - label: "Saturation" - valueText: camera.imageProcessing.saturation.toFixed(2) - width: parent.width - onValueChanged: camera.imageProcessing.saturation = value - } - - Slider { - minimumValue: -1.0 - maximumValue: 1.0 - value: camera.imageProcessing.sharpeningLevel - label: "Sharpening level" - valueText: camera.imageProcessing.sharpeningLevel.toFixed(2) - width: parent.width - onValueChanged: camera.imageProcessing.sharpeningLevel = value - }*/ - - SectionHeader { - text: "Metadata" - } - - Label { - x: Theme.horizontalPageMargin - width: parent.width - 2*x - text: "Metadata is write-only API to define metadata baked into the captured photos and videos" - font.pixelSize: Theme.fontSizeSmall - color: Theme.secondaryHighlightColor - wrapMode: Text.Wrap - } - - Item { - width: 1 - height: Theme.paddingMedium - } - - TextField { - label: "Manufacturer" - onTextChanged: camera.metaData.cameraManufacturer = text - EnterKey.iconSource: "image://theme/icon-m-enter-next" - EnterKey.onClicked: modelField.focus = true - } - - TextField { - id: modelField - - label: "Model" - onTextChanged: camera.metaData.cameraModel = text - - EnterKey.iconSource: "image://theme/icon-m-enter-next" - EnterKey.onClicked: eventField.focus = true - } - - TextField { - id: eventField - - label: "Event" - onTextChanged: camera.metaData.event = text - - EnterKey.iconSource: "image://theme/icon-m-enter-next" - EnterKey.onClicked: subjectField.focus = true - } - - TextField { - id: subjectField - - label: "Subject" - onTextChanged: camera.metaData.subject = text - - EnterKey.iconSource: "image://theme/icon-m-enter-next" - EnterKey.onClicked: latitudeField.focus = true - } - - ComboBox { - id: orientationComboBox - label: "Orientation" - value: "Not defined" - menu: ContextMenu { - Repeater { - model: [0, 90, 180, 270] - - MenuItem { - text: modelData.toString() - onClicked: { - camera.metaData.orientation = modelData - orientationComboBox.value = modelData.toString() - } - } - } - } - } - - DetailItem { - id: dateTimeOriginal - label: "Date time original" - value: "Not defined" - } - - Item { - width: 1 - height: Theme.paddingMedium - } - - Button { - text: "Update" - anchors.horizontalCenter: parent.horizontalCenter - onClicked: { - var now = new Date() - camera.metaData.dateTimeOriginal = now - dateTimeOriginal.value = now - } - } - - SectionHeader { - text: "Location metadata" - } - - TextField { - id: latitudeField - - label: "Latitude" - inputMethodHints: Qt.ImhFormattedNumbersOnly - text: camera.metaData.gpsLatitude ? camera.metaData.gpsLatitude : "" - onTextChanged: camera.metaData.gpsLatitude = parseFloat(text) - - EnterKey.iconSource: "image://theme/icon-m-enter-next" - EnterKey.onClicked: longitudeField.focus = true - } - - TextField { - id: longitudeField - - label: "Longitude" - inputMethodHints: Qt.ImhFormattedNumbersOnly - onTextChanged: camera.metaData.gpsLongitude = parseFloat(text) - - EnterKey.iconSource: "image://theme/icon-m-enter-next" - EnterKey.onClicked: altitudeField.focus = true - } - - TextField { - id: altitudeField - - label: "Altitude" - inputMethodHints: Qt.ImhFormattedNumbersOnly - onTextChanged: camera.metaData.gpsAltitude = parseFloat(text) - - EnterKey.iconSource: "image://theme/icon-m-enter-next" - EnterKey.onClicked: speedField.focus = true - } - - TextField { - id: speedField - - label: "Speed" - inputMethodHints: Qt.ImhFormattedNumbersOnly - onTextChanged: camera.metaData.gpsSpeed = parseFloat(text) - - EnterKey.iconSource: "image://theme/icon-m-enter-next" - EnterKey.onClicked: trackField.focus = true - } - - DetailItem { - id: gpsTimestamp - label: "GPS timestamp" - value: "Not defined" - } - - Item { - width: 1 - height: Theme.paddingMedium - } - - Button { - anchors.horizontalCenter: parent.horizontalCenter - text: "Update time" - onClicked: { - var now = new Date - camera.metaData.gpsTimestamp = now - gpsTimestamp.value = now - } - } - - TextField { - id: trackField - - label: "GPS track" - description: "Measured in degrees clockwise from north" - inputMethodHints: Qt.ImhFormattedNumbersOnly - onTextChanged: camera.metaData.gpsTrack = parseFloat(text) - - EnterKey.iconSource: "image://theme/icon-m-enter-next" - EnterKey.onClicked: imgeDirectionField.focus = true - } - - TextField { - id: imgeDirectionField - - label: "GPS image direction" - description: "Direction the camera is facing at the time of capture" - inputMethodHints: Qt.ImhFormattedNumbersOnly - onTextChanged: camera.metaData.gpsImgDirection = parseFloat(text) - - EnterKey.iconSource: "image://theme/icon-m-enter-next" - EnterKey.onClicked: processingMethodField.focus = true - } - - TextField { - id: processingMethodField - label: "GPS processing method" - description: "Method for determining the GPS position" - onTextChanged: camera.metaData.gpsProcessingMethod = text - - EnterKey.iconSource: "image://theme/icon-m-enter-close" - EnterKey.onClicked: focus = false - } - } - } -} diff --git a/usr/share/contextkit/providers/Alarm.qml b/usr/share/contextkit/providers/Alarm.qml new file mode 100644 index 00000000..b3a80032 --- /dev/null +++ b/usr/share/contextkit/providers/Alarm.qml @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2020 Open Mobile Platform LLC. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 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 + * Lesser General Public License for more details. + + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301 USA + * + * http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html + */ + +import Nemo.DBus 2.0 +import org.freedesktop.contextkit 1.0 + +ContextPropertyBase { + id: root + + property bool _alarmPresent + property var _alarmTriggers: ({}) + + propertyValue: { + switch (propertyName) { + case "Present": + return _alarmPresent + case "Triggers": + return _alarmTriggers + default: + return undefined + } + } + + DBusInterface { + bus: DBus.SystemBus + service: "com.nokia.time" + path: "/com/nokia/time" + iface: "com.nokia.time" + signalsEnabled: root.subscribed + watchServiceStatus: root.subscribed + + function alarm_present_changed(value) { + root._alarmPresent = value + } + + function alarm_triggers_changed(value) { + root._alarmTriggers = value + } + + onStatusChanged: { + if (status === DBusInterface.Available) { + call("get_alarm_present", [], function(value) { + root._alarmPresent = value + }) + call("get_alarm_triggers", [], function(value) { + root._alarmTriggers = value + }) + } + } + } +} diff --git a/usr/share/contextkit/providers/Battery.qml b/usr/share/contextkit/providers/Battery.qml new file mode 100644 index 00000000..d75e489b --- /dev/null +++ b/usr/share/contextkit/providers/Battery.qml @@ -0,0 +1,158 @@ +/* + * Copyright (c) 2020 Open Mobile Platform LLC. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 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 + * Lesser General Public License for more details. + + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301 USA + * + * http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html + */ + +import Nemo.Mce 1.0 +import org.freedesktop.contextkit 1.0 +import org.freedesktop.contextkit.providers.battery 1.0 + +ContextPropertyBase { + id: root + + propertyValue: { + switch (propertyName) { + case "ChargePercentage": + return mceBatteryLevel.percent + case "Capacity": + return batteryContext.capacity + case "Energy": + return batteryContext.energy + case "EnergyFull": + return batteryContext.energyFull + case "OnBattery": + return !mceCableState.connected + case "LowBattery": + return mceBatteryStatus.status === MceBatteryStatus.Low + case "TimeUntilLow": + return batteryContext.timeUntilLow + case "TimeUntilFull": + return batteryContext.timeUntilFull + case "IsCharging": + return mceBatteryState === MceBatteryState.Charging + case "Temperature": + return batteryContext.temperature + case "Power": + return batteryContext.power + case "State": + switch (mceBatteryStatus.status) { + case MceBatteryStatus.Full: + return "full" + case MceBatteryStatus.Low: + return "low" + case MceBatteryStatus.Empty: + return "empty" + default: + return mceBatteryState.text + } + case "Voltage": + return batteryContext.voltage + case "Current": + return batteryContext.current + case "Level": + return mceBatteryStatus.text + case "ChargerType": + return mceChargerType.text + case "ChargingState": + return mceBatteryState.text + + default: + return undefined + } + } + + BatteryContextPropertyProvider { + id: batteryContext + + active: root.subscribed + } + + MceBatteryLevel { + id: mceBatteryLevel + } + + MceBatteryState { + id: mceBatteryState + + readonly property string text: { + if (valid) { + switch (value) { + case MceBatteryState.Charging: + return "charging" + case MceBatteryState.Discharging: + return "discharging" + case MceBatteryState.NotCharging: + return "unknown" + case MceBatteryState.Full: + return "idle" + } + } + return "unknown" + } + } + + MceBatteryStatus { + id: mceBatteryStatus + + readonly property string text: { + if (valid) { + switch (status) { + case MceBatteryStatus.Full: + case MceBatteryStatus.Ok: + return "normal" + case MceBatteryStatus.Low: + return "low" + case MceBatteryStatus.Empty: + return "empty" + } + } + return "unknown" + } + } + + MceChargerType { + id: mceChargerType + + readonly property string text: { + if (valid) { + switch (type) { + case MceChargerType.None: + return "None" + case MceChargerType.USB: + return "USB" + case MceChargerType.DCP: + return "DCP" + case MceChargerType.HVDCP: + return "HVDCP" + case MceChargerType.CDP: + return "CDP" + case MceChargerType.Wireless: + return "Wireless" + case MceChargerType.Other: + return "Other" + } + } + return "unknown" + } + } + + MceCableState { + id: mceCableState + } +} diff --git a/usr/share/contextkit/providers/Bluetooth.qml b/usr/share/contextkit/providers/Bluetooth.qml new file mode 100644 index 00000000..76e7d656 --- /dev/null +++ b/usr/share/contextkit/providers/Bluetooth.qml @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2020 Open Mobile Platform LLC. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 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 + * Lesser General Public License for more details. + + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301 USA + * + * http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html + */ + +import org.freedesktop.contextkit 1.0 +import org.kde.bluezqt 1.0 as BluezQt + +ContextPropertyBase { + id: root + + property var _adapter: BluezQt.Manager.usableAdapter + readonly property bool _valid: !!adapter + + propertyValue: { + switch (propertyName) { + case "Enabled": + return _valid && _adapter.powered + case "Visible": + return _valid && _adapter.discoverable + case "Connected": + return _valid && _adapter.connected + case "Address": + return _valid ? _adapter.address : "" + default: + console.log("Unknown property:", propertyName) + return undefined + } + } +} diff --git a/usr/share/contextkit/providers/Cellular.qml b/usr/share/contextkit/providers/Cellular.qml new file mode 100644 index 00000000..440d5a9a --- /dev/null +++ b/usr/share/contextkit/providers/Cellular.qml @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2020 Open Mobile Platform LLC. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 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 + * Lesser General Public License for more details. + + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301 USA + * + * http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html + */ + +import org.freedesktop.contextkit 1.0 +import "ofono" + +CellularBase { + modemPath: telephonySimManager.availableModems[0] || "" +} diff --git a/usr/share/contextkit/providers/Cellular_1.qml b/usr/share/contextkit/providers/Cellular_1.qml new file mode 100644 index 00000000..c2129d16 --- /dev/null +++ b/usr/share/contextkit/providers/Cellular_1.qml @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2020 Open Mobile Platform LLC. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 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 + * Lesser General Public License for more details. + + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301 USA + * + * http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html + */ + +import org.freedesktop.contextkit 1.0 +import "ofono" + +CellularBase { + modemPath: telephonySimManager.availableModems[1] || "" +} diff --git a/usr/share/contextkit/providers/Internet.qml b/usr/share/contextkit/providers/Internet.qml new file mode 100644 index 00000000..e386b886 --- /dev/null +++ b/usr/share/contextkit/providers/Internet.qml @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2020 Open Mobile Platform LLC. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 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 + * Lesser General Public License for more details. + + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301 USA + * + * http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html + */ + +import Connman 0.2 +import org.freedesktop.contextkit 1.0 + +ContextPropertyBase { + id: root + + property var _networkService: networkManager.defaultRoute + + function _networkTypeString(networkType) { + switch (networkType) { + case "wifi": + return "WLAN" + case "gprs": + case "cellular": + case "edge": + case "umts": + return "GPRS" + case "ethernet": + return "ethernet" + } + return "" + } + + function _networkStateString(networkState) { + switch (networkState) { + case "offline": + case "idle": + return "disconnected" + case "online": + case "ready": + return "connected" + } + return "" + } + + propertyValue: { + switch (propertyName) { + case "NetworkType": + return _networkService ? _networkTypeString(_networkService.type) : "" + case "NetworkState": + return _networkService ? _networkStateString(_networkService.state) : "disconnected" + case "NetworkName": + return _networkService ? _networkService.name : "" + case "SignalStrength": + return _networkService ? _networkService.strength : 0 + case "Tethering": + return wlanNetworkTechnology.tethering + + default: + return undefined + } + } + + NetworkManager { + id: networkManager + } + + NetworkTechnology { + id: wlanNetworkTechnology + + path: networkManager.WifiTechnology + } +} diff --git a/usr/share/contextkit/providers/Profile.qml b/usr/share/contextkit/providers/Profile.qml new file mode 100644 index 00000000..ea81ef7f --- /dev/null +++ b/usr/share/contextkit/providers/Profile.qml @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2020 Open Mobile Platform LLC. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 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 + * Lesser General Public License for more details. + + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301 USA + * + * http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html + */ + +import Nemo.DBus 2.0 +import org.freedesktop.contextkit 1.0 + +ContextPropertyBase { + id: root + + property string _profileName + + propertyValue: { + switch (propertyName) { + case "Name": + return _profileName + default: + return undefined + } + } + + DBusInterface { + bus: DBus.SessionBus + service: "com.nokia.profiled" + path: "/com/nokia/profiled" + iface: "com.nokia.profiled" + signalsEnabled: root.subscribed + watchServiceStatus: root.subscribed + + function profile_changed(arg, forActiveProfile, profileName) { + if (forActiveProfile) { + root._profileName = profileName + } + } + + onStatusChanged: { + if (status === DBusInterface.Available) { + call("get_profile", [], function(value) { + root._profileName = value + }) + } + } + } +} diff --git a/usr/share/contextkit/providers/Screen.qml b/usr/share/contextkit/providers/Screen.qml new file mode 100644 index 00000000..a9c59801 --- /dev/null +++ b/usr/share/contextkit/providers/Screen.qml @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2020 Open Mobile Platform LLC. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 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 + * Lesser General Public License for more details. + + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301 USA + * + * http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html + */ + +import Nemo.Mce 1.0 +import org.freedesktop.contextkit 1.0 + +ContextPropertyBase { + id: root + + propertyValue: { + switch (propertyName) { + case "Blanked": + return mceDisplay.valid && mceDisplay.state === MceDisplay.DisplayOff + default: + return undefined + } + } + + MceDisplay { + id: mceDisplay + } +} diff --git a/usr/share/contextkit/providers/Sensors.qml b/usr/share/contextkit/providers/Sensors.qml new file mode 100644 index 00000000..c4e0db14 --- /dev/null +++ b/usr/share/contextkit/providers/Sensors.qml @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2020 Open Mobile Platform LLC. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 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 + * Lesser General Public License for more details. + + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301 USA + * + * http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html + */ + +import QtSensors 5.2 +import org.freedesktop.contextkit 1.0 + +ContextPropertyBase { + id: root + + readonly property var _orientationNames: [ + "unknown", "top", "bottom", "left", "right", "face", "back" + ] + + propertyValue: { + switch (propertyName) { + case "Orientation": + var orientation = sensor.reading.orientation + if (orientation >= 0 && orientation < root._orientationNames.length) { + return root._orientationNames[orientation] + } + return "unknown" + default: + return undefined + } + } + + OrientationSensor { + id: sensor + + active: root.subscribed + } +} diff --git a/usr/share/contextkit/providers/System.qml b/usr/share/contextkit/providers/System.qml new file mode 100644 index 00000000..084c1c23 --- /dev/null +++ b/usr/share/contextkit/providers/System.qml @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2020 Open Mobile Platform LLC. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 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 + * Lesser General Public License for more details. + + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301 USA + * + * http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html + */ + +import Nemo.DBus 2.0 +import org.freedesktop.contextkit 1.0 + +ContextPropertyBase { + id: root + + property bool _powerSaveMode + property int _radioState: -1 + property bool _keyboard_available + + // from mce-dev + readonly property int _MCE_RADIO_STATE_MASTER: (1 << 0) + readonly property int _MCE_RADIO_STATE_CELLULAR: (1 << 1) + readonly property int _MCE_RADIO_STATE_WLAN: (1 << 2) + + propertyValue: { + switch (propertyName) { + case "PowerSaveMode": + return _powerSaveMode + case "OfflineMode": + return _radioState >= 0 && ((_radioState & _MCE_RADIO_STATE_CELLULAR) == 0) + case "WlanEnabled": + return _radioState >= 0 && ((_radioState & _MCE_RADIO_STATE_WLAN) != 0) + case "InternetEnabled": + return _radioState >= 0 && ((_radioState & _MCE_RADIO_STATE_MASTER) != 0) + case "KeyboardPresent": + return _keyboard_available + case "KeyboardOpen": + return _keyboard_available + default: + return undefined + } + } + + DBusInterface { + bus: DBus.SystemBus + service: "com.nokia.mce" + path: "/com/nokia/mce/signal" + iface: "com.nokia.mce.signal" + signalsEnabled: root.subscribed + + function psm_state_ind(value) { + root._powerSaveMode = value + } + + function radio_states_ind(value) { + root._radioState = value + } + + function keyboard_available_state_ind(value) { + root._keyboard_available = (value === "available") + } + } + + DBusInterface { + bus: DBus.SystemBus + service: "com.nokia.mce" + path: "/com/nokia/mce/request" + iface: "com.nokia.mce.request" + watchServiceStatus: root.subscribed + + onStatusChanged: { + if (status === DBusInterface.Available) { + call("get_radio_states", [], function(value) { + root._radioState = value + }) + call("get_psm_state", [], function(value) { + root._powerSaveMode = value + }) + call("keyboard_available_state_req", [], function(value) { + root._keyboard_available = (value === "available") + }) + } + } + } +} diff --git a/usr/share/contextkit/providers/ofono/CellularBase.qml b/usr/share/contextkit/providers/ofono/CellularBase.qml new file mode 100644 index 00000000..a1311260 --- /dev/null +++ b/usr/share/contextkit/providers/ofono/CellularBase.qml @@ -0,0 +1,326 @@ +/* + * Copyright (c) 2020 Open Mobile Platform LLC. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 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 + * Lesser General Public License for more details. + + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301 USA + * + * http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html + */ + +import QtQml 2.2 +import QOfono 0.2 +import Connman 0.2 +import Nemo.DBus 2.0 +import Sailfish.Telephony 1.0 +import org.freedesktop.contextkit 1.0 +import org.nemomobile.ofono 1.0 + +ContextPropertyBase { + id: root + + property string modemPath + property SimManager telephonySimManager: SimManager {} + + property var _connectionManager + property var _networkReg + property var _networkOp + property var _ofonoSimManager + property var _simInfo + property var _simToolkit + property var _voiceCallManager + + function _getConnectionManager() { + if (!_connectionManager) { + _connectionManager = connectionManagerComponent.createObject(root) + } + return _connectionManager + } + + function _getNetworkReg() { + if (!_networkReg) { + _networkReg = networkRegistrationComponent.createObject(root) + } + return _networkReg + } + + function _getNetworkOp() { + if (!_networkOp) { + _networkOp = networkOperatorComponent.createObject(root) + } + return _networkOp + } + + function _getOfonoSimManager() { + if (!_ofonoSimManager) { + _ofonoSimManager = ofonoSimManagerComponent.createObject(root) + } + return _ofonoSimManager + } + + function _getSimInfo() { + if (!_simInfo) { + _simInfo = simInfoComponent.createObject(root) + } + return _simInfo + } + + function _getSimToolkit() { + if (!_simToolkit) { + _simToolkit = simToolkitComponent.createObject(root) + } + return _simToolkit + } + + function _getVoiceCallManager() { + if (!_voiceCallManager) { + _voiceCallManager = voiceCallManagerComponent.createObject(root) + } + return _voiceCallManager + } + + propertyValue: { + switch (propertyName) { + + case "SignalStrength": + return _getNetworkReg().valid ? _getNetworkReg().strength : 0 + case "DataTechnology": + return _getNetworkReg().dataTechnologyText + case "RegistrationStatus": // fall through + case "Status": + return _getNetworkReg().networkStatusText + case "Sim": + return telephonySimManager.modemHasPresentSim(modemPath) ? "present" : "absent" + case "Technology": + return _getNetworkReg().technologyText + case "SignalBars": + return _getNetworkReg().valid ? _getNetworkReg().signalBars : 0 + + case "CellName": + return _getNetworkReg().valid ? _getNetworkReg().cellId : "" + case "NetworkName": // fall through + case "ExtendedNetworkName": + return _getNetworkReg().valid + ? _getNetworkReg().name || _getNetworkOp().name + : "" + + case "SubscriberIdentity": + return _getOfonoSimManager().valid ? _getOfonoSimManager().subscriberIdentity : "" + case "CurrentMCC": + return _getNetworkReg().valid ? _getNetworkReg().mcc : "0" + case "CurrentMNC": + return _getNetworkReg().valid ? _getNetworkReg().mnc : "0" + case "HomeMCC": + return _getOfonoSimManager().valid ? _getOfonoSimManager().mobileCountryCode : "0" + case "HomeMNC": + return _getOfonoSimManager().valid ? _getOfonoSimManager().mobileNetworkCode : "0" + + case "StkIdleModeText": + return _getSimToolkit().idleModeText + case "MMSContext": + return _getConnectionManager().mmsContext + case "DataRoamingAllowed": + return _getConnectionManager().roamingAllowed + case "GPRSAttached": + return _getConnectionManager().attached + + case "CapabilityVoice": + return telephonySimManager.availableModems.length > 0 + case "CapabilityData": + return telephonySimManager.availableModems.length > 0 + case "CallCount": + return _getVoiceCallManager().calls.length + + case "ModemPath": + return modemPath + + case "ServiceProviderName": + return _getSimInfo().serviceProviderName + case "CachedCardIdentifier": + return _getSimInfo().cardIdentifier + case "CachedSubscriberIdentity": + return _getSimInfo().subscriberIdentity + + default: + console.log("Unknown property:", propertyName) + return undefined + } + } + + Component { + id: connectionManagerComponent + + OfonoConnMan { + id: connectionManager + + property string mmsContext + + property var _mmsInstantiator: Instantiator { + model: root.subscribed ? connectionManager.contexts : [] + + delegate: OfonoContextConnection { + property bool _isMmsContext: type === "mms" && messageCenter.length > 0 + + contextPath: modelData + + on_IsMmsContextChanged: { + if (_isMmsContext) { + connectionManager.mmsContext = contextPath + } else if (connectionManager.mmsContext == contextPath) { + connectionManager.mmsContext = "" + } + } + } + } + + modemPath: root.subscribed ? root.modemPath : "" + + } + } + + Component { + id: networkRegistrationComponent + + OfonoNetworkRegistration { + id: network + + property string networkStatusText: { + if (network.valid) { + if (!telephonySimManager.modemHasPresentSim(modemPath)) { + return "no-sim" + } + switch (status) { + case "unregistered": + return "disabled" + case "registered": + return "home" + case "searching": + case "unknown": + return "offline" + case "denied": + return "forbidden" + case "roaming": + return "roam" + } + } + return "disabled" + } + + property string dataTechnologyText: network.valid && _networkTechnologies[network.technology] + ? _networkTechnologies[network.technology].dataTech + : "unknown" + + property string technologyText: network.valid && _networkTechnologies[network.technology] + ? _networkTechnologies[network.technology].tech + : "unknown" + + // 0-5 range + property int signalBars: (strength + 19) / 20 + + property var _networkTechnologies: { + "gsm": { "tech": "gsm", "dataTech": "gprs" }, + "edge": { "tech": "gsm", "dataTech": "egprs" }, + "hspa": { "tech": "umts", "dataTech": "hspa" }, + "umts": { "tech": "umts", "dataTech": "umts"}, + "lte": { "tech": "lte", "dataTech": "lte"} + }; + + modemPath: root.subscribed ? root.modemPath : "" + } + } + + Component { + id: networkOperatorComponent + + OfonoNetworkOperator { + id: operator + + operatorPath: root.subscribed ? network.currentOperatorPath : "" + } + } + + Component { + id: ofonoSimManagerComponent + + OfonoSimManager { + id: ofonoSimManager + + modemPath: root.subscribed ? root.modemPath : "" + } + } + + Component { + id: simInfoComponent + + OfonoSimInfo { + id: simInfo + + modemPath: root.subscribed ? root.modemPath : "" + } + } + + Component { + id: simToolkitComponent + + DBusInterface { + id: simToolkit + + property string idleModeText + + bus: DBus.SystemBus + service: "org.ofono" + path: root.modemPath + iface: "org.ofono.SimToolkit" + signalsEnabled: root.subscribed + + function propertyChanged(property, value) { + if (property === "IdleModeText") { + idleModeText = value + } + } + + Component.onCompleted: { + if (!root.subscribed || root.modemPath.length === 0) { + return + } + call("GetProperties", [], function(properties) { + simToolkit.idleModeText = properties["IdleModeText"] + }) + } + } + } + + Component { + id: voiceCallManagerComponent + + OfonoVoiceCallManager { + id: voiceCallManager + + property var calls: [] + + Component.onCompleted: calls = getCalls() + onCallAdded: { + if (calls.indexOf(call) < 0) { + calls.push(call) + } + } + onCallRemoved: { + var i = calls.indexOf(call) + if (i >= 0) { + calls.splice(i, 1) + } + } + } + } +} diff --git a/usr/share/csd/main.qml b/usr/share/csd/main.qml index 78e8c4e8..2dcea2b3 100644 --- a/usr/share/csd/main.qml +++ b/usr/share/csd/main.qml @@ -6,14 +6,16 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 -import org.nemomobile.dbus 2.0 -import org.nemomobile.configuration 1.0 +import Nemo.DBus 2.0 +import Nemo.Configuration 1.0 import Csd 1.0 import "pages" ApplicationWindow { initialPage: Component { FirstPage {} } cover: Qt.resolvedUrl("cover/CoverPage.qml") + allowedOrientations: defaultAllowedOrientations + _defaultPageOrientations: Orientation.All _backgroundVisible: false Rectangle { diff --git a/usr/share/csd/pages/AppAutoStart.qml b/usr/share/csd/pages/AppAutoStart.qml index a15d9ecd..21b0db47 100644 --- a/usr/share/csd/pages/AppAutoStart.qml +++ b/usr/share/csd/pages/AppAutoStart.qml @@ -6,7 +6,7 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 -import org.nemomobile.dbus 2.0 +import Nemo.DBus 2.0 DBusInterface { bus: DBus.SessionBus diff --git a/usr/share/csd/pages/RunInTestPage.qml b/usr/share/csd/pages/RunInTestPage.qml index f9272778..9e75688c 100644 --- a/usr/share/csd/pages/RunInTestPage.qml +++ b/usr/share/csd/pages/RunInTestPage.qml @@ -7,8 +7,8 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 import Nemo.KeepAlive 1.2 -import org.nemomobile.configuration 1.0 -import org.nemomobile.time 1.0 +import Nemo.Configuration 1.0 +import Nemo.Time 1.0 import Sailfish.Silica.private 1.0 as Private import Csd 1.0 import "testToolPages" diff --git a/usr/share/csd/pages/TestCaseListModel.qml b/usr/share/csd/pages/TestCaseListModel.qml index 8e079bf1..c1f30172 100644 --- a/usr/share/csd/pages/TestCaseListModel.qml +++ b/usr/share/csd/pages/TestCaseListModel.qml @@ -1,6 +1,6 @@ /* * Copyright (c) 2020 Open Mobile Platform LLC - * Copyright (c) 2016 - 2019 Jolla Ltd. + * Copyright (c) 2016 - 2023 Jolla Ltd. * * License: Proprietary */ @@ -32,9 +32,9 @@ ListModel { "VerificationHeadsetDetect", "VerificationHeadsetButtons", "VerificationHeadset", "AudioPlayMusicHeadset", "VerificationVideoPlayback", "VerificationVideoPlaybackVibrator", "VerificationFrontCamera", "VerificationFrontCameraReboot", "VerificationBackCamera", "VerificationFrontBackCamera", - "VerificationWifi", "VerificationBluetooth", "VerificationToh", "VerificationNfc", "VerificationGpsRadio", "VerificationGpsLock", "VerificationCellInfo", + "VerificationWifi", "VerificationBluetooth", "VerificationNfc", "VerificationGpsRadio", "VerificationGpsLock", "VerificationCellInfo", "VerificationFmRadio", - "Verification2G", "Verification3G", "Verification4G", + "Verification2G", "Verification3G", "Verification4G", "Verification5G", "VerificationBattery", "VerificationUsbCharging", "VerificationDischarging", "VerificationBatteryResistance", "VerificationLED", "VerificationButtonBacklight", @@ -230,9 +230,6 @@ ListModel { case "VerificationBluetooth": //% "Bluetooth" return qsTrId("csd-li-bluetooth") - case "VerificationToh": - //% "TOH" - return qsTrId("csd-li-toh") case "VerificationNfc": //% "NFC" return qsTrId("csd-li-nfc") @@ -252,15 +249,16 @@ ListModel { //% "Front camera with reboot" return qsTrId("csd-li-front_camera_reboot") case "VerificationBackCamera": - //% "Main camera and flash" - return CsdHwSettings.backCameraFlash ? qsTrId("csd-li-back_camera_and_flash_light") : - //% "Main camera" - qsTrId("csd-li-back_camera") + return CsdHwSettings.backCameraFlash + ? //% "Main camera and flash" + qsTrId("csd-li-back_camera_and_flash_light") + : //% "Main camera" + qsTrId("csd-li-back_camera") case "VerificationFrontBackCamera": - //% "Front and back camera with flash" - return CsdHwSettings.backCameraFlash ? qsTrId("csd-li-front_and_back_camera_with_flash_light") : - //% "Back and front camera" - qsTrId("csd-li-front_and_back_camera") + return CsdHwSettings.backCameraFlash ? //% "Front and back camera with flash" + qsTrId("csd-li-front_and_back_camera_with_flash_light") + : //% "Back and front camera" + qsTrId("csd-li-front_and_back_camera") case "VerificationBattery": //% "Battery" return qsTrId("csd-li-battery") @@ -327,6 +325,8 @@ ListModel { return "3G" case "Verification4G": return "4G" + case "Verification5G": + return "5G" case "VerificationCalibration": //% "Calibration check" return qsTrId("csd-li-calibration") @@ -376,7 +376,6 @@ ListModel { return qsTrId("csd-he-camera") case "VerificationWifi": case "VerificationBluetooth": - case "VerificationToh": case "VerificationNfc": case "VerificationGpsRadio": case "VerificationGpsLock": @@ -387,6 +386,7 @@ ListModel { case "Verification2G": case "Verification3G": case "Verification4G": + case "Verification5G": //: Radio frequency function check //% "RF Function Check" return qsTrId("csd-la-he-rf-function") @@ -430,63 +430,63 @@ ListModel { } property var mapping: { - "TouchSelfTest": ["TouchAuto"], - "VerificationTouch": ["Touch"], - "VerificationMultiTouch": ["Touch"], - "VerificationLcd": ["LCD"], - "VerificationLcdBacklight": ["Backlight"], - "VerificationLightSensor": ["LightSensor"], - "VerificationProxSensor": ["ProxSensor"], - "VerificationGyroAndGSensor": ["Gyro", "GSensor"], - "VerificationEcompass": ["ECompass"], - "VerificationAudio1Mic": ["AudioMic1"], - "VerificationAudio2Mic": ["AudioMic2"], - "AudioPlayMusicLoudspeaker": ["Loudspeaker"], - "AudioPlayMusicReceiver": ["Receiver"], - "VerificationHeadset": ["Headset"], - "AudioPlayStereoLoudspeaker": ["StereoLoudspeaker"], - "AudioPlayMusicHeadset": ["Headset"], - "VerificationHeadsetDetect": ["Headset"], - "VerificationHeadsetButtons": ["Headset"], - "VerificationWifi": ["Wifi"], - "VerificationBluetooth": ["Bluetooth"], - "VerificationToh": ["TOH"], - "VerificationNfc": ["NFC"], - "VerificationGpsRadio": ["GPS"], - "VerificationGpsLock": ["GPS"], - "VerificationCellInfo": ["CellInfo"], - "VerificationFrontCamera": ["FrontCamera"], - "VerificationBackCamera": ["BackCamera"], - "VerificationBattery": ["Battery"], - "VerificationDischarging": ["Battery"], - "VerificationBatteryResistance": ["Battery"], - "VerificationUsbCharging": ["UsbCharging"], - "VerificationLED": ["LED"], - "VerificationButtonBacklight": ["ButtonBacklight"], - "VerificationVibrator": ["Vibrator"], - "VerificationSim": ["SIM"], - "VerificationSdCard": ["SDCard"], - "VerificationKey": ["Key"], - "VerificationUsbOtg": ["UsbOtg"], - "VerificationHallDetect": ["Hall"], - "VerificationFingerprint": ["Fingerprint"], - "VerificationFmRadio" : ["FmRadio"], - "VerificationSuspend": ["Suspend"], - "VerificationReboot": ["Reboot"], - "VerificationVideoPlayback": ["VideoPlayback"], - "VerificationVideoPlaybackVibrator": ["VideoPlayback", "Vibrator"], - "Verification2G": ["CellularData"], - "Verification3G": ["CellularData"], - "Verification4G": ["CellularData"], - "VerificationCalibration": ["Calibration"], - "VerificationMacAddresses": ["MacAddresses"] + "TouchSelfTest": [["TouchAuto"]], + "VerificationTouch": [["Touch"]], + "VerificationMultiTouch": [["Touch"]], + "VerificationLcd": [["LCD"]], + "VerificationLcdBacklight": [["Backlight"]], + "VerificationLightSensor": [["LightSensor"]], + "VerificationProxSensor": [["ProxSensor"]], + "VerificationGyroAndGSensor": [undefined, ["Gyro", "GSensor"]], + "VerificationEcompass": [["ECompass"]], + "VerificationAudio1Mic": [["AudioMic1"]], + "VerificationAudio2Mic": [["AudioMic2"]], + "AudioPlayMusicLoudspeaker": [["Loudspeaker"]], + "AudioPlayMusicReceiver": [["Receiver"]], + "VerificationHeadset": [["Headset"]], + "AudioPlayStereoLoudspeaker": [["StereoLoudspeaker"]], + "AudioPlayMusicHeadset": [["Headset"]], + "VerificationHeadsetDetect": [["Headset"]], + "VerificationHeadsetButtons": [["Headset"]], + "VerificationWifi": [["Wifi"]], + "VerificationBluetooth": [["Bluetooth"]], + "VerificationNfc": [["NFC"]], + "VerificationGpsRadio": [["GPS"]], + "VerificationGpsLock": [["GPS"]], + "VerificationCellInfo": [["CellInfo"]], + "VerificationFrontCamera": [["FrontCamera"]], + "VerificationBackCamera": [["BackCamera"]], + "VerificationBattery": [["Battery"]], + "VerificationDischarging": [["Battery"]], + "VerificationBatteryResistance": [["Battery"]], + "VerificationUsbCharging": [["UsbCharging"]], + "VerificationLED": [["LED"]], + "VerificationButtonBacklight": [["ButtonBacklight"]], + "VerificationVibrator": [["Vibrator"]], + "VerificationSim": [["SIM"]], + "VerificationSdCard": [["SDCard"]], + "VerificationKey": [["Key"]], + "VerificationUsbOtg": [["UsbOtg"]], + "VerificationHallDetect": [["Hall"]], + "VerificationFingerprint": [["Fingerprint"]], + "VerificationFmRadio" : [["FmRadio"]], + "VerificationSuspend": [["Suspend"]], + "VerificationReboot": [["Reboot"]], + "VerificationVideoPlayback": [["VideoPlayback"]], + "VerificationVideoPlaybackVibrator": [["VideoPlayback", "Vibrator"]], + "Verification2G": [["CellularData"]], + "Verification3G": [["CellularData"]], + "Verification4G": [["CellularData"]], + "Verification5G": [["CellularData", "Cellular5G"]], + "VerificationCalibration": [["Calibration"]], + "VerificationMacAddresses": [undefined, ["Bluetooth", "Wifi"]], } // Alternative mappings used for some run-in tests property var alternativeMapping: { - "AudioPlayMusicLoudspeaker": ["StereoLoudspeaker"], - "VerificationFrontBackCamera": ["FrontCamera", "BackCamera"], - "VerificationFrontCameraReboot": ["FrontCamera"] + "AudioPlayMusicLoudspeaker": [["StereoLoudspeaker"]], + "VerificationFrontBackCamera": [["FrontCamera", "BackCamera"]], + "VerificationFrontCameraReboot": [["FrontCamera"]], } function feature(url) { @@ -516,15 +516,32 @@ ListModel { // a test is supported if *any* of its listed features are supported function testSupported(featureMapping, url) { - var testFeatures = featureMapping[url] - if (testFeatures == undefined) + var features = featureMapping[url] + if (features === undefined) { return false - for (var i=0; i minX && averageGsensorX < maxX - && averageGsensorY > minY && averageGsensorY < maxY - && averageGsensorZ > minZ && averageGsensorZ < maxZ + testPassed = (avgX || avgY || avgZ) + && minX < avgX && avgX < maxX + && minY < avgY && avgY < maxY + && minZ < avgZ && avgZ < maxZ done = true running = false } Accelerometer { - id: accelerometer - dataRate: 5000 + dataRate: sampleRate active: true onReadingChanged: { if (reading) { - gsensorX = reading.x - gsensorY = reading.y - gsensorZ = reading.z + rawX = reading.x + rawY = reading.y + rawZ = reading.z } } } Timer { - id: timer - interval: sleeptime + id: subsampleTimer + interval: 1000 / subsampleRate repeat: true - onTriggered: { - if (times > 0) { - times-- - _getValues() - } else { - timer.stop() - _checkRange() - } - } + onTriggered: _subsample() } } diff --git a/usr/share/csd/pages/testToolPages/GyroTest.qml b/usr/share/csd/pages/testToolPages/GyroTest.qml index 915a6731..fa2bcddd 100644 --- a/usr/share/csd/pages/testToolPages/GyroTest.qml +++ b/usr/share/csd/pages/testToolPages/GyroTest.qml @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016 - 2019 Jolla Ltd. + * Copyright (c) 2016 - 2023 Jolla Ltd. * * License: Proprietary */ @@ -11,81 +11,93 @@ import Csd 1.0 import ".." Item { - id: root - - property real sensorX - property real sensorY - property real sensorZ - - property real valueX - property real valueY - property real valueZ - property bool running property bool done property bool testPassed - property int secondsRemaining: timer2.count == 0 ? 0 : Math.round(16 - (timer2.count / 2)) - function start() { - if (running) { - return - } - testPassed = false - done = false - running = true + // Actual sampling rate might vary from one device to another + // Normalize ui visuals by doing timer based subsampling + readonly property int subsampleRate: 2 + readonly property int sampleRate: subsampleRate * 2 + 1 + readonly property int samplesNeeded: 32 + property int samplesAcquired + readonly property int secondsRemaining: (samplesNeeded - samplesAcquired + subsampleRate - 1) / subsampleRate - timer2.start() - timer1.start() - } + // The latest sensor values seen + property real rawX + property real rawY + property real rawZ - function _retrieveGyroData() { - if (timer2.count < 32) { - sensor.updateGyroSensor() - timer2.count = timer2.count + 1 - } + // The latest sensor values picked for use + property real curX + property real curY + property real curZ + + // Average of the first samplesNeeded picked values + property real avgX + property real avgY + property real avgZ - var sensorResult = sensor.getResult - if (!timer1.running) { - root.testPassed = sensorResult - done = true - running = false + // Pass/Fail limits from config + readonly property real gyroMin: CsdHwSettings.gyroMin + readonly property real gyroMax: CsdHwSettings.gyroMax + + function start() { + if (!running) { + testPassed = false + done = false + running = true + subsampleTimer.start() } + } - sensorX = sensor.getXResult - sensorY = sensor.getYResult - sensorZ = sensor.getZResult + function _subsample() { + curX = rawX + curY = rawY + curZ = rawZ + _accumulate() } - Timer { - id: timer1 - interval: 16500 - onTriggered: { - timer2.stop() - root._retrieveGyroData() + function _accumulate() { + if (samplesAcquired < samplesNeeded) { + avgX += curX + avgY += curY + avgZ += curZ + if (++samplesAcquired == samplesNeeded) { + _average() + } } } - Timer { - id: timer2 - property int count - interval: 500 - repeat: true - triggeredOnStart: true - onTriggered: root._retrieveGyroData() - } + function _average() { + avgX /= samplesAcquired + avgY /= samplesAcquired + avgZ /= samplesAcquired - GyroSensor { - id: sensor + testPassed = (avgX || avgY || avgZ) + && gyroMin < avgX && avgX < gyroMax + && gyroMin < avgY && avgY < gyroMax + && gyroMin < avgZ && avgZ < gyroMax + done = true + running = false } Gyroscope { + dataRate: sampleRate active: true onReadingChanged: { if (reading) { - valueX = reading.x - valueY = reading.y - valueZ = reading.z + rawX = reading.x + rawY = reading.y + rawZ = reading.z } } } + + Timer { + id: subsampleTimer + interval: 1000 / subsampleRate + repeat: true + onTriggered: _subsample() + } } diff --git a/usr/share/csd/pages/testToolPages/RebootController.qml b/usr/share/csd/pages/testToolPages/RebootController.qml index befc540e..289fe786 100644 --- a/usr/share/csd/pages/testToolPages/RebootController.qml +++ b/usr/share/csd/pages/testToolPages/RebootController.qml @@ -5,8 +5,8 @@ */ import QtQuick 2.0 -import org.nemomobile.dbus 2.0 -import org.nemomobile.configuration 1.0 +import Nemo.DBus 2.0 +import Nemo.Configuration 1.0 import ".." Item { diff --git a/usr/share/csd/pages/testToolPages/SatelliteDelegate.qml b/usr/share/csd/pages/testToolPages/SatelliteDelegate.qml index be979c11..04a674df 100644 --- a/usr/share/csd/pages/testToolPages/SatelliteDelegate.qml +++ b/usr/share/csd/pages/testToolPages/SatelliteDelegate.qml @@ -8,7 +8,7 @@ import QtQuick 2.2 import Sailfish.Silica 1.0 Item { - height: Theme.itemSizeMedium + height: Math.max(Theme.itemSizeMedium, column.height) // http://www.catb.org/gpsd/NMEA.html#_satellite_ids function getSatelliteSystem() { diff --git a/usr/share/csd/pages/testToolPages/Verification5G.qml b/usr/share/csd/pages/testToolPages/Verification5G.qml new file mode 100644 index 00000000..77fac398 --- /dev/null +++ b/usr/share/csd/pages/testToolPages/Verification5G.qml @@ -0,0 +1,14 @@ +/* + * Copyright (c) 2023 Jolla Ltd. + * + * License: Proprietary + */ + +import QtQuick 2.0 + +VerificationCellular { + //% "5G information" + pageTitle: qsTrId("csd-he-5g_information") + testTechnology: ["nr"] + requireAllModems: false +} diff --git a/usr/share/csd/pages/testToolPages/VerificationBackCamera.qml b/usr/share/csd/pages/testToolPages/VerificationBackCamera.qml index f138f249..fbefccb5 100644 --- a/usr/share/csd/pages/testToolPages/VerificationBackCamera.qml +++ b/usr/share/csd/pages/testToolPages/VerificationBackCamera.qml @@ -8,7 +8,6 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 import Sailfish.Policy 1.0 import com.jolla.settings.system 1.0 -import org.nemomobile.configuration 1.0 import QtMultimedia 5.4 import Csd 1.0 import ".." @@ -17,8 +16,6 @@ CameraTestPage { id: page focusBeforeCapture: true - viewfinderResolution: viewfinderResolution.value - imageCaptureResolution: imageResolution.value Binding { target: videoOutput.source @@ -26,16 +23,6 @@ CameraTestPage { value: CsdHwSettings.backCameraFlash ? Camera.FlashOn : Camera.FlashOff } - ConfigurationValue { - id: viewfinderResolution - key: "/apps/jolla-camera/primary/image/viewfinderResolution" - } - - ConfigurationValue { - id: imageResolution - key: "/apps/jolla-camera/primary/image/imageResolution" - } - PolicyValue { id: cameraPolicy policyType: PolicyValue.CameraEnabled diff --git a/usr/share/csd/pages/testToolPages/VerificationBatteryResistance.qml b/usr/share/csd/pages/testToolPages/VerificationBatteryResistance.qml index a2ba2872..09b1d16e 100644 --- a/usr/share/csd/pages/testToolPages/VerificationBatteryResistance.qml +++ b/usr/share/csd/pages/testToolPages/VerificationBatteryResistance.qml @@ -103,6 +103,7 @@ CsdTestPage { } Battery { id: battery } + BatteryStatus { id: batteryStatus } DisplaySettings { id: displaySettings @@ -145,6 +146,8 @@ CsdTestPage { property int _SHORT_TEST_SAMPLES: 30 + property var savedChargingMode + function startTest() { if (!displaySettings.populated) return @@ -152,7 +155,8 @@ CsdTestPage { testing = true // Setup test - battery.disableCharger() + savedChargingMode = batteryStatus.chargingMode + batteryStatus.chargingMode = BatteryStatus.DisableCharging sx = 0 sy = 0 @@ -178,7 +182,7 @@ CsdTestPage { displaySettings.brightness = testController.brightness displaySettings.ambientLightSensorEnabled = testController.ambient - battery.enableCharger() + batteryStatus.chargingMode = savedChargingMode if (samples >= _SHORT_TEST_SAMPLES) { shortTimeTestPassed = beta > -0.3 && rsq > 0.85 diff --git a/usr/share/csd/pages/testToolPages/VerificationBluetooth.qml b/usr/share/csd/pages/testToolPages/VerificationBluetooth.qml index 333be10b..e8f3488d 100644 --- a/usr/share/csd/pages/testToolPages/VerificationBluetooth.qml +++ b/usr/share/csd/pages/testToolPages/VerificationBluetooth.qml @@ -140,13 +140,17 @@ CsdTestPage { } } - Flickable { + SilicaFlickable { id: results width: parent.width height: parent.height - topInfoColumn.height contentHeight: picker.height clip: true + VerticalScrollDecorator { + Component.onCompleted: showDecorator() + } + BluetoothDevicePicker { id: picker diff --git a/usr/share/csd/pages/testToolPages/VerificationButtonBacklight.qml b/usr/share/csd/pages/testToolPages/VerificationButtonBacklight.qml index a5770598..b01651ea 100644 --- a/usr/share/csd/pages/testToolPages/VerificationButtonBacklight.qml +++ b/usr/share/csd/pages/testToolPages/VerificationButtonBacklight.qml @@ -6,7 +6,7 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 -import org.nemomobile.dbus 2.0 +import Nemo.DBus 2.0 import Csd 1.0 import ".." diff --git a/usr/share/csd/pages/testToolPages/VerificationCellInfo.qml b/usr/share/csd/pages/testToolPages/VerificationCellInfo.qml index 2e62cc47..332ea042 100644 --- a/usr/share/csd/pages/testToolPages/VerificationCellInfo.qml +++ b/usr/share/csd/pages/testToolPages/VerificationCellInfo.qml @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016 - 2019 Jolla Ltd. + * Copyright (c) 2016 - 2023 Jolla Ltd. * * License: Proprietary */ @@ -61,6 +61,7 @@ AllModemsPage { Column { id: content + width: page.width spacing: Theme.paddingLarge @@ -71,6 +72,7 @@ AllModemsPage { Item { id: resultsItem + x: Theme.horizontalPageMargin width: parent.width - 2*x height: resultLabel.implicitHeight @@ -84,15 +86,16 @@ AllModemsPage { ResultLabel { id: resultLabel + result: (cellCount > 0) && allModemsWithSimsHaveCells && locationSettings.cellPositioningEnabled anchors.verticalCenter: parent.verticalCenter opacity: resultsItem.busy ? 0 : 1 Behavior on opacity { FadeAnimation {}} - text: result ? - //% "%n cell(s) found" - qsTrId("csd-la-cells_found", cellCount) : - //% "Cell positioning is unavailable" - qsTrId("csd-la-cell_positioning_unavailable") + text: result + ? //% "%n cell(s) found" + qsTrId("csd-la-cells_found", cellCount) + : //% "Cell positioning is unavailable" + qsTrId("csd-la-cell_positioning_unavailable") } } @@ -117,6 +120,7 @@ AllModemsPage { OfonoExtCellInfo { id: cellInfo + modemPath: modelData onValidChanged: if (valid) page.supported = true } @@ -131,6 +135,7 @@ AllModemsPage { Repeater { id: cellList + model: cellInfo.valid ? cellInfo.cells : [] delegate: cellDelegate } @@ -145,8 +150,18 @@ AllModemsPage { readonly property int offset: Theme.horizontalPageMargin readonly property bool gsm: cell.valid && (cell.type == OfonoExtCell.GSM) readonly property bool lte: cell.valid && (cell.type == OfonoExtCell.LTE) + readonly property bool nr: cell.valid && (cell.type == OfonoExtCell.NR) readonly property bool wcdma: cell.valid && (cell.type == OfonoExtCell.WCDMA) + function formatCellInt(value) { + // javascript number handling ftw, comparing with == is not enough + if (Math.abs(value - OfonoExtCell.InvalidValue) < 0.1) { + return "-" + } else { + return value + } + } + OfonoExtCell { id: cell path: modelData @@ -162,15 +177,17 @@ AllModemsPage { font.bold: true text: (cell.type == OfonoExtCell.GSM) ? "GSM" : (cell.type == OfonoExtCell.LTE) ? "LTE" : + (cell.type == OfonoExtCell.NR) ? "NR" : (cell.type == OfonoExtCell.WCDMA) ? "WCDMA" : cell.type } Image { + id: mask + readonly property int bars: cell.signalStrength / 6 anchors.bottom: type.baseline - id: mask height: type.font.pixelSize visible: (cell.valid && cell.registered) - source: "image://theme/icon-status-cellular-" + Math.min(bars,5) + source: "image://theme/icon-status-cellular-" + Math.min(bars, 5) } } @@ -178,56 +195,56 @@ AllModemsPage { x: offset width: parent.width - x visible: cell.valid && cell.mcc >= 0 - text: "mcc: " + cell.mcc + text: "mcc: " + formatCellInt(cell.mcc) } Label { x: offset width: parent.width - x visible: cell.valid && cell.mnc >= 0 - text: "mnc: " + cell.mnc + text: "mnc: " + formatCellInt(cell.mnc) } Label { x: offset width: parent.width - x visible: (gsm || wcdma) && cell.lac >= 0 - text: "lac: " + cell.lac + text: "lac: " + formatCellInt(cell.lac) } Label { x: offset width: parent.width - x visible: (gsm || wcdma) && cell.cid >= 0 - text: "cid: " + cell.cid + text: "cid: " + formatCellInt(cell.cid) } Label { x: offset width: parent.width - x visible: wcdma && cell.psc >= 0 - text: "psc: " + cell.psc + text: "psc: " + formatCellInt(cell.psc) } Label { x: offset width: parent.width - x visible: lte && cell.ci >= 0 - text: "ci: " + cell.ci + text: "ci: " + formatCellInt(cell.ci) } Label { x: offset width: parent.width - x - visible: lte && cell.pci >= 0 - text: "pci: " + cell.pci + visible: (lte || nr) && cell.pci >= 0 + text: "pci: " + formatCellInt(cell.pci) } Label { x: offset width: parent.width - x - visible: lte && cell.tac >= 0 - text: "tac: " + cell.tac + visible: (lte || nr) && cell.tac >= 0 + text: "tac: " + formatCellInt(cell.tac) } Label { @@ -241,35 +258,83 @@ AllModemsPage { x: offset width: parent.width - x visible: lte && cell.rsrp >= 0 - text: "rsrp: " + cell.rsrp + text: "rsrp: " + formatCellInt(cell.rsrp) } Label { x: offset width: parent.width - x visible: lte && cell.rsrq >= 0 - text: "rsrq: " + cell.rsrq + text: "rsrq: " + formatCellInt(cell.rsrq) } Label { x: offset width: parent.width - x visible: lte && cell.rssnr >= 0 - text: "rssnr: " + cell.rssnr + text: "rssnr: " + formatCellInt(cell.rssnr) } Label { x: offset width: parent.width - x visible: lte && cell.cqi >= 0 - text: "cqi " + cell.cqi + text: "cqi " + formatCellInt(cell.cqi) } Label { x: offset width: parent.width - x visible: lte && cell.timingAdvance >= 0 - text: "timingAdvance: " + cell.timingAdvance + text: "timingAdvance: " + formatCellInt(cell.timingAdvance) + } + + Label { + x: offset + width: parent.width - x + visible: nr && cell.nci != "" + text: "nci: " + cell.nci + } + + Label { + x: offset + width: parent.width - x + visible: nr && cell.ssRsrp >= 0 + text: "ssRsrp: " + formatCellInt(cell.ssRsrp) + } + + Label { + x: offset + width: parent.width - x + visible: nr && cell.ssRsrq >= 0 + text: "ssRsrq: " + formatCellInt(cell.ssRsrq) + } + + Label { + x: offset + width: parent.width - x + visible: nr && cell.ssSinr >= 0 + text: "ssSinr: " + formatCellInt(cell.ssSinr) + } + Label { + x: offset + width: parent.width - x + visible: nr && cell.csiRsrp >= 0 + text: "csiRsrp: " + formatCellInt(cell.csiRsrp) + } + + Label { + x: offset + width: parent.width - x + visible: nr && cell.csiRsrq >= 0 + text: "csiRsrq: " + formatCellInt(cell.csiRsrq) + } + + Label { + x: offset + width: parent.width - x + visible: nr && cell.csiSinr >= 0 + text: "csiSinr: " + formatCellInt(cell.csiSinr) } Label { diff --git a/usr/share/csd/pages/testToolPages/VerificationCellular.qml b/usr/share/csd/pages/testToolPages/VerificationCellular.qml index ebe4ad3a..502c9cf2 100644 --- a/usr/share/csd/pages/testToolPages/VerificationCellular.qml +++ b/usr/share/csd/pages/testToolPages/VerificationCellular.qml @@ -6,7 +6,7 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 -import MeeGo.QOfono 0.2 +import QOfono 0.2 import org.nemomobile.ofono 1.0 AllModemsPage { @@ -88,7 +88,6 @@ AllModemsPage { id: modemTest Column { - id: column x: Theme.horizontalPageMargin width: parent.width - x*2 spacing: Theme.paddingLarge @@ -121,6 +120,7 @@ AllModemsPage { OfonoNetworkRegistration { id: ofonoNetworkRegistration + modemPath: modelData property bool registered: valid && (status === "registered" || status === "roaming") onTechnologyChanged: updateTechCount() diff --git a/usr/share/csd/pages/testToolPages/VerificationDischarging.qml b/usr/share/csd/pages/testToolPages/VerificationDischarging.qml index 77c217a5..85a14914 100644 --- a/usr/share/csd/pages/testToolPages/VerificationDischarging.qml +++ b/usr/share/csd/pages/testToolPages/VerificationDischarging.qml @@ -9,8 +9,8 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 import Csd 1.0 import ".." -import MeeGo.Connman 0.2 -import MeeGo.QOfono 0.2 +import Connman 0.2 +import QOfono 0.2 import com.jolla.settings.system 1.0 import org.nemomobile.systemsettings 1.0 import Nemo.KeepAlive 1.2 @@ -20,8 +20,33 @@ CsdTestPage { id: page property string currentModem: manager.modems.length > 0 ? manager.modems[0] : "" - property int brightness - property bool ambientLightSensorEnabled + property int savedBrightness + property bool savedAmbientLightSensorEnabled + property bool savedOfflineMode + property bool settingsSaved + property bool blockedByCharger: testController.chargerAttached && !testController.testFinished && !testController.testRunning + + function applyTestingSettings() { + if (!settingsSaved) { + savedAmbientLightSensorEnabled = displaySettings.ambientLightSensorEnabled + savedBrightness = displaySettings.brightness + savedOfflineMode = connMgr.instance.offlineMode + settingsSaved = true + } + + displaySettings.ambientLightSensorEnabled = false + displayBlanking.preventBlanking = true + } + + function restoreOriginalSettings() { + if (settingsSaved) { + displaySettings.ambientLightSensorEnabled = savedAmbientLightSensorEnabled + displaySettings.brightness = savedBrightness + setFlightMode(savedOfflineMode) + } + + displayBlanking.preventBlanking = false + } function setFlightMode(offline) { if (connMgr.instance.offlineMode === offline) @@ -42,7 +67,7 @@ CsdTestPage { anchors.fill: parent spacing: Theme.paddingLarge - model: !testController.chargerAttached ? testcases : null + model: page.blockedByCharger ? null : testcases header: Column { anchors { @@ -64,14 +89,15 @@ CsdTestPage { wrapMode: Text.Wrap font.pixelSize: Theme.fontSizeSmall height: implicitHeight + Theme.paddingMedium + text: { - if (testController.chargerAttached) { + if (page.blockedByCharger) { //% "Device is currently charging, disconnect charger to complete test." return qsTrId("csd-la-device_is_currently_charging") } else { //% "Tests battery discharge rate under different scenarios. %0 battery state samples are collected per test at %1 second intervals. " //% "Running applications may cause the test to fail due to application activity triggered by network connections being established during the test." - return qsTrId("csd-la-tests_battery_discharge_rate").arg(testController._SAMPLES_PER_TEST).arg(refreshTimer.interval / 1000) + return qsTrId("csd-la-tests_battery_discharge_rate").arg(testController._SAMPLES_TO_CAPTURE).arg(testController._SAMPLE_TIME / 1000) } } } @@ -139,14 +165,17 @@ CsdTestPage { } Label { + property bool currentDirectionFailure: positiveCurrentSeen && negativeCurrentSeen + property bool currentLimitFailure: averageCurrent < minimumCurrent || averageCurrent > maximumCurrent + property bool testSuccessfullyCompleted: completed && !currentDirectionFailure && !currentLimitFailure + visible: completed - property bool _passed: minimumCurrent <= averageCurrent && averageCurrent <= maximumCurrent - color: _passed ? "green" : "red" + color: testSuccessfullyCompleted ? "green" : "red" text: { - if (_passed) + if (testSuccessfullyCompleted) //% "Pass" return qsTrId("csd-la-pass") - else if (charging) + else if (currentDirectionFailure) //% "Failed (charging)" return qsTrId("csd-la-failed_charing") else @@ -158,16 +187,14 @@ CsdTestPage { } footer: Text { - property double currentNow - font.pixelSize: Theme.fontSizeLarge anchors.horizontalCenter: parent.horizontalCenter - visible: !testController.chargerAttached + visible: testController.testRunning || testController.testFinished height: implicitHeight + Theme.paddingLarge verticalAlignment: Text.AlignVCenter color: { - if (!testController.testCompleted) + if (!testController.testFinished) return "white" if (testController.testPassed) @@ -176,7 +203,7 @@ CsdTestPage { return "red" } text: { - if (testController.testCompleted) { + if (testController.testFinished) { if (testController.testPassed) //% "All tests passed" return qsTrId("csd-la-all_tests_passed") @@ -197,19 +224,21 @@ CsdTestPage { Timer { id: refreshTimer - interval: 3000 + interval: testController._SAMPLE_TIME repeat: true + running: testController.testRunning onTriggered: testController.recordBatteryStats() } Battery { id: battery } + Connections { + target: Qt.application + onAboutToQuit: testController.stopTest() + } + Component.onDestruction: { testController.stopTest() - - setFlightMode(false) - displaySettings.ambientLightSensorEnabled = ambientLightSensorEnabled - displaySettings.brightness = brightness } DisplayBlanking { @@ -229,85 +258,77 @@ CsdTestPage { DisplaySettings { id: displaySettings - onPopulatedChanged: { - // Save existing backlight settings - page.ambientLightSensorEnabled = displaySettings.ambientLightSensorEnabled - page.brightness = displaySettings.brightness - // Max out the brightness before test - displaySettings.brightness = displaySettings.maximumBrightness - // Also disable the ambient light sensor. - displaySettings.ambientLightSensorEnabled = false - testController.startTest() - } + onPopulatedChanged: testController.startTest() } MceChargerState { id: mceChargerState + onValidChanged: testController.startTest() } Item { id: testController + property bool testPassed + property bool testFinished + property bool testStopped + property bool testStarted + property bool testRunning: testStarted && !testStopped + property double currentNow: Number.NaN property int currentTest: -1 + readonly property int _SAMPLE_TIME: 2000 + readonly property int _SAMPLES_TO_IGNORE: 30 // e.g. L500D needs >50s settle time + readonly property int _SAMPLES_TO_CAPTURE: 15 + property var sampleData: null property int settleCounter - property bool testCompleted - property bool testPassed property bool chargerAttached: mceChargerState.charging - property double currentNow: Number.NaN - - onChargerAttachedChanged: { - if (chargerAttached) - stopTest() - else - startTest() - } - - property int _SAMPLES_PER_TEST: 10 - - property var _samples: [] function startTest() { - if (chargerAttached || !displaySettings.populated) { + if (!displaySettings.populated || !mceChargerState.valid) return - } - displayBlanking.preventBlanking = true - currentTest = 0 - testCompleted = false + if (testFinished || (testStarted && !testStopped) || chargerAttached) + return + + applyTestingSettings() + testPassed = false + testFinished = false + testStopped = false + testStarted = true currentNow = Number.NaN - refreshTimer.start() + currentTest = 0 } function stopTest() { + if (testStopped || !testStarted) + return + + testStopped = true currentTest = -1 currentNow = Number.NaN - refreshTimer.stop() - if (!testCompleted) + restoreOriginalSettings() + } + + function finishTest() { + stopTest() + + if (testFinished) return + testFinished = true + var passed = true - for (var i = 0; i < testcases.count; ++i) { + for (var i = 0; passed && i < testcases.count; ++i) { var testData = testcases.get(i) - - if (!testData.completed) { - passed = false - break - } - - if (testData.minimumCurrent <= testData.averageCurrent && - testData.averageCurrent <= testData.maximumCurrent) { - passed &= true - } else { + if (!testSuccessfullyCompleted(testData)) passed = false - break - } } testPassed = passed setTestResult(passed) - testCompleted(true) + testCompleted(false) } function average(samples) { @@ -318,6 +339,18 @@ CsdTestPage { return sum/samples.length } + function currentDirectionFailure(testData) { + return testData.negativeCurrentSeen && testData.positiveCurrentSeen + } + + function currentLimitFailure(testData) { + return testData.averageCurrent < testData.minimumCurrent || testData.averageCurrent > testData.maximumCurrent + } + + function testSuccessfullyCompleted(testData) { + return testData.completed && !currentDirectionFailure(testData) && !currentLimitFailure(testData) + } + function recordBatteryStats() { if (currentTest < 0 || currentTest >= testcases.count) return @@ -327,39 +360,54 @@ CsdTestPage { return } - var sample = [] - if (currentTest < _samples.length) - sample = _samples[currentTest] - + // Note: Current sign got flipped at Android base 10 + // Normalize to "discharging is positive" expected by CSD + // Fail test if current sign changes during measurement currentNow = battery.currentNow() - sample[sample.length] = currentNow - _samples[currentTest] = sample + if (currentNow < 0) { + testcases.setProperty(currentTest, "negativeCurrentSeen", true) + currentNow = -currentNow + } else if (currentNow > 0) { + testcases.setProperty(currentTest, "positiveCurrentSeen", true) + } - testcases.setProperty(currentTest, "averageCurrent", average(sample)) - testcases.setProperty(currentTest, "charging", chargerAttached) + sampleData[sampleData.length] = currentNow - if (sample.length >= _SAMPLES_PER_TEST) { - testcases.setProperty(currentTest, "completed", true) + testcases.setProperty(currentTest, "averageCurrent", average(sampleData)) + if (sampleData.length >= _SAMPLES_TO_CAPTURE) { + testcases.setProperty(currentTest, "completed", true) currentTest += 1 } } + onChargerAttachedChanged: { + if (chargerAttached) + stopTest() + else + startTest() + } + onCurrentTestChanged: { if (currentTest < 0) return if (currentTest >= testcases.count) { - testCompleted = true - stopTest() + finishTest() return } - settleCounter = 4 + testcases.setProperty(currentTest, "completed", false) + testcases.setProperty(currentTest, "averageCurrent", 0) + testcases.setProperty(currentTest, "positiveCurrentSeen", false) + testcases.setProperty(currentTest, "negativeCurrentSeen", false) + + sampleData = [] + settleCounter = _SAMPLES_TO_IGNORE currentNow = Number.NaN listView.positionViewAtIndex(currentTest, ListView.Contain) - var testData = testcases.get(currentTest) + var testData = testcases.get(currentTest) setFlightMode(testData.flightMode) displaySettings.ambientLightSensorEnabled = testData.ambientLightSensorEnabled displaySettings.brightness = testData.brightness @@ -369,35 +417,35 @@ CsdTestPage { id: testcases ListElement { - type: "low" + type: "high" - flightMode: true + flightMode: false ambientLightSensorEnabled: false - brightness: 0 + brightness: 100 // Expected current range (µA) minimumCurrent: 0 - maximumCurrent: 485000 + maximumCurrent: 800000 averageCurrent: 0 - charging: false - + positiveCurrentSeen: false + negativeCurrentSeen: false completed: false } ListElement { - type: "high" + type: "low" - flightMode: false + flightMode: true ambientLightSensorEnabled: false - brightness: 100 + brightness: 0 // Expected current range (µA) minimumCurrent: 0 - maximumCurrent: 800000 + maximumCurrent: 485000 averageCurrent: 0 - charging: false - + positiveCurrentSeen: false + negativeCurrentSeen: false completed: false } } diff --git a/usr/share/csd/pages/testToolPages/VerificationEcompass.qml b/usr/share/csd/pages/testToolPages/VerificationEcompass.qml index 530de28d..4debc36a 100644 --- a/usr/share/csd/pages/testToolPages/VerificationEcompass.qml +++ b/usr/share/csd/pages/testToolPages/VerificationEcompass.qml @@ -11,109 +11,126 @@ import ".." CsdTestPage { id: page - property bool result: compass.level === 1 - property real passingCalibrationLevel: 1 + readonly property real passingCalibrationLevel: 1 + property bool testPassed + readonly property int testDuration: 25 + readonly property int testSubsampleRate: 4 + property int testRemainingSamples: testDuration * testSubsampleRate + readonly property int testRemainingTime: testRemainingSamples / testSubsampleRate + readonly property bool testRunning: !testPassed && testRemainingSamples > 0 - function showPassOrFail() { - if (page.result || timer.count == 25) { - timer.running = false - setTestResult(page.result) + onTestRunningChanged: { + if (!testRunning) { + setTestResult(testPassed) testCompleted(false) } - - timer.count = timer.count + 1 } - Compass { - id: compass - dataRate: 100 - active: page.status == PageStatus.Active - property real level - property real azimuth + SilicaFlickable { + anchors.fill: parent + contentHeight: contentColumn.height + Theme.paddingLarge - onReadingChanged: { - level = compass.reading.calibrationLevel - azimuth = compass.reading.azimuth - if (compass.reading.calibrationLevel === 1) { - timer.stop() - page.showPassOrFail() - } + VerticalScrollDecorator { + Component.onCompleted: showDecorator() } - } - - Column { - id: column1 - width: parent.width - spacing: Theme.paddingLarge - CsdPageHeader { - //% "Compass sensor" - title: qsTrId("csd-he-compass_sensor") - } - DescriptionItem { - //% "1. Please wave your device in a figure 8 for 5-20 seconds to calibrate the sensor." - text: qsTrId("csd-la-compass_description") - } - } - - Column { - id: column2 - width: page.width - spacing: Theme.paddingLarge - anchors.top: column1.bottom + Column { + id: contentColumn + width: parent.width + spacing: Theme.paddingLarge + CsdPageHeader { + //% "Compass sensor" + title: qsTrId("csd-he-compass_sensor") + } + DescriptionItem { + //% "1. Please wave your device in a figure 8 for 5-20 seconds to calibrate the sensor." + text: qsTrId("csd-la-compass_description") + } - CsdPageHeader { - //% "Compass test result" - title: qsTrId("csd-he-compass_test_result") - } + SectionHeader { + //% "Compass test result" + text: qsTrId("csd-he-compass_test_result") + } - Label { - id: description - visible: timer.running - width: parent.width - 2*Theme.paddingLarge - wrapMode: Text.Wrap - x: Theme.paddingLarge - //% "Checking compass..." - text: qsTrId("csd-la-checking_compass") + "\n\n" + - //% "Time remaining: %1" - qsTrId("csd-la-compass_timer %1").arg(25 - timer.count) - } + Label { + width: parent.width - 2*Theme.paddingLarge + wrapMode: Text.Wrap + x: Theme.paddingLarge + text: //% "Pass calibration level: %0" + qsTrId("csd-la-pass_calibration_level").arg(page.passingCalibrationLevel) + + //% "Current calibration level: %0" + "\n\n" + qsTrId("csd-la-calibration_level").arg(compassSubsampleTimer.calibrationLevel) + + //% "Current azimuth: %0" + "\n\n" + qsTrId("csd-la-azimuth").arg(compassSubsampleTimer.azimuth) + } - Label { - id: resultLabel - width: parent.width - 2*Theme.paddingLarge - wrapMode: Text.Wrap - x: Theme.paddingLarge - text: //% "Pass calibration level: %0" - qsTrId("csd-la-pass_calibration_level").arg(page.passingCalibrationLevel) + - //% "Current calibration level: %0" - "\n\n" + qsTrId("csd-la-calibration_level").arg(compass.level) + - //% "Current azimuth: %0" - "\n\n" + qsTrId("csd-la-azimuth").arg(compass.azimuth) - } + Label { + x: Theme.paddingLarge + visible: !page.testRunning + width: parent.width - 2*Theme.paddingLarge + wrapMode: Text.Wrap + font.pixelSize: Theme.fontSizeExtraLarge + font.family: Theme.fontFamilyHeading + color: page.testPassed ? "green" : "red" + text: { + if (page.testPassed) { + //% "Pass" + return qsTrId("csd-la-pass") + } + //% "Fail" + return qsTrId("csd-la-fail") + } + } - Label { - id: label - x: Theme.paddingLarge - visible: !timer.running - width: parent.width - 2*Theme.paddingLarge - wrapMode: Text.Wrap - font.pixelSize: Theme.fontSizeExtraLarge - font.family: Theme.fontFamilyHeading - color: page.result ? "green" : "red" - //% "Pass" - text: page.result ? qsTrId("csd-la-pass") - : //% "Fail" - qsTrId("csd-la-fail") + Label { + visible: page.testRunning + width: parent.width - 2*Theme.paddingLarge + wrapMode: Text.Wrap + x: Theme.paddingLarge + text: //% "Checking compass..." + qsTrId("csd-la-checking_compass") + "\n\n" + + //% "Time remaining: %1" + qsTrId("csd-la-compass_timer %1").arg(page.testRemainingTime) + } } } Timer { - id: timer - property int count - interval: 1000 - running: true + id: compassSubsampleTimer + property real calibrationLevel + property real azimuth + interval: 1000 / page.testSubsampleRate + running: page.status == PageStatus.Active repeat: true - onTriggered: page.showPassOrFail() + onTriggered: { + calibrationLevel = compassSensor.calibrationLevel + azimuth = compassSensor.azimuth + if (page.testRemainingSamples > 0) { + if (calibrationLevel >= page.passingCalibrationLevel) { + page.testPassed = true + page.testRemainingSamples = 0 + } else { + page.testRemainingSamples -= 1 + } + } + } + } + + Compass { + id: compassSensor + /* Test code change history suggests that a relatively high datarate + * is needed for the actual calibration at lower SW levels to complete + * in expected manner / time. However, we do not want to reevaluate + * test status / update screen at such pace. Therefore: cache the + * latest sensor values seen in here and then use timer to subsample + * at pace that makes sense for test logic / updating ui elements. */ + dataRate: 100 + property real calibrationLevel + property real azimuth + active: page.status == PageStatus.Active + onReadingChanged: { + calibrationLevel = reading.calibrationLevel + azimuth = reading.azimuth + } } } diff --git a/usr/share/csd/pages/testToolPages/VerificationFingerprint.qml b/usr/share/csd/pages/testToolPages/VerificationFingerprint.qml index c3d0aaf9..5997e9ec 100644 --- a/usr/share/csd/pages/testToolPages/VerificationFingerprint.qml +++ b/usr/share/csd/pages/testToolPages/VerificationFingerprint.qml @@ -7,7 +7,7 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 import Sailfish.Silica.private 1.0 as Private -import org.nemomobile.dbus 2.0 +import Nemo.DBus 2.0 import ".." CsdTestPage { @@ -144,7 +144,7 @@ CsdTestPage { } Timer { - // The typedCall() from org.nemomobile.dbus.DBusInterface offers no way + // The typedCall() from Nemo.DBus.DBusInterface offers no way // to deal with D-Bus error replies. This timer is used as a workaround // for dealing with situations such as not having the daemon running. id: ipcTimeout diff --git a/usr/share/csd/pages/testToolPages/VerificationFmRadio.qml b/usr/share/csd/pages/testToolPages/VerificationFmRadio.qml index 48ce26da..76207b4a 100644 --- a/usr/share/csd/pages/testToolPages/VerificationFmRadio.qml +++ b/usr/share/csd/pages/testToolPages/VerificationFmRadio.qml @@ -72,13 +72,22 @@ CsdTestPage { BottomButton { id: startButton - visible: !_testing - enabled: route.wiredOutputConnected + visible: !_testing && route.wiredOutputConnected //% "Start" text: qsTrId("csd-la-start") onClicked: startTest() } + FailBottomButton { + visible: !_testing && !route.wiredOutputConnected + //% "Headset not detected - test can't be executed" + reason: qsTrId("csd-la-disabled_headset_not_detected") + onClicked: { + setTestResult(false) + testCompleted(true) + } + } + Column { width: parent.width spacing: Theme.paddingLarge diff --git a/usr/share/csd/pages/testToolPages/VerificationFrontBackCamera.qml b/usr/share/csd/pages/testToolPages/VerificationFrontBackCamera.qml index 130ed9de..078ac0b5 100644 --- a/usr/share/csd/pages/testToolPages/VerificationFrontBackCamera.qml +++ b/usr/share/csd/pages/testToolPages/VerificationFrontBackCamera.qml @@ -8,7 +8,6 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 import Sailfish.Policy 1.0 import com.jolla.settings.system 1.0 -import org.nemomobile.configuration 1.0 import QtMultimedia 5.4 import Csd 1.0 import ".." @@ -19,8 +18,6 @@ CameraTestPage { focusBeforeCapture: backFaceCameraActive imagePreview.mirror: !backFaceCameraActive - viewfinderResolution: viewfinderResolution.value - imageCaptureResolution: backFaceCameraActive ? primaryImageResolution.value : secondaryImageResolution.value switchBetweenFrontAndBack: true Binding { @@ -29,21 +26,6 @@ CameraTestPage { value: CsdHwSettings.backCameraFlash ? Camera.FlashOn : Camera.FlashOff } - ConfigurationValue { - id: viewfinderResolution - key: "/apps/jolla-camera/primary/image/viewfinderResolution" - } - - ConfigurationValue { - id: primaryImageResolution - key: "/apps/jolla-camera/primary/image/imageResolution" - } - - ConfigurationValue { - id: secondaryImageResolution - key: "/apps/jolla-camera/secondary/image/imageResolution" - } - PolicyValue { id: cameraPolicy policyType: PolicyValue.CameraEnabled diff --git a/usr/share/csd/pages/testToolPages/VerificationFrontCamera.qml b/usr/share/csd/pages/testToolPages/VerificationFrontCamera.qml index c080dba7..a0aa4028 100644 --- a/usr/share/csd/pages/testToolPages/VerificationFrontCamera.qml +++ b/usr/share/csd/pages/testToolPages/VerificationFrontCamera.qml @@ -9,15 +9,12 @@ import Sailfish.Silica 1.0 import Sailfish.Policy 1.0 import com.jolla.settings.system 1.0 import QtMultimedia 5.4 -import org.nemomobile.configuration 1.0 import ".." CameraTestPage { id: page imagePreview.mirror: true - viewfinderResolution: viewfinderResolution.value - imageCaptureResolution: imageResolution.value CsdPageHeader { id: header @@ -42,16 +39,6 @@ CameraTestPage { } } - ConfigurationValue { - id: viewfinderResolution - key: "/apps/jolla-camera/secondary/image/viewfinderResolution" - } - - ConfigurationValue { - id: imageResolution - key: "/apps/jolla-camera/secondary/image/imageResolution" - } - PolicyValue { id: cameraPolicy policyType: PolicyValue.CameraEnabled diff --git a/usr/share/csd/pages/testToolPages/VerificationFrontCameraReboot.qml b/usr/share/csd/pages/testToolPages/VerificationFrontCameraReboot.qml index 619b3de5..baf55254 100644 --- a/usr/share/csd/pages/testToolPages/VerificationFrontCameraReboot.qml +++ b/usr/share/csd/pages/testToolPages/VerificationFrontCameraReboot.qml @@ -9,7 +9,6 @@ import Sailfish.Silica 1.0 import Sailfish.Policy 1.0 import com.jolla.settings.system 1.0 import QtMultimedia 5.4 -import org.nemomobile.configuration 1.0 import ".." CameraTestPage { @@ -18,8 +17,6 @@ CameraTestPage { readonly property bool resumeTest: camera.imageCapture.ready && canActivateCamera imagePreview.mirror: true - viewfinderResolution: viewfinderResolution.value - imageCaptureResolution: imageResolution.value onResumeTestChanged: { if (resumeTest) { @@ -71,16 +68,6 @@ CameraTestPage { } } - ConfigurationValue { - id: viewfinderResolution - key: "/apps/jolla-camera/secondary/image/viewfinderResolution" - } - - ConfigurationValue { - id: imageResolution - key: "/apps/jolla-camera/secondary/image/imageResolution" - } - PolicyValue { id: cameraPolicy policyType: PolicyValue.CameraEnabled diff --git a/usr/share/csd/pages/testToolPages/VerificationGpsLock.qml b/usr/share/csd/pages/testToolPages/VerificationGpsLock.qml index a60bbf19..0ccb675b 100644 --- a/usr/share/csd/pages/testToolPages/VerificationGpsLock.qml +++ b/usr/share/csd/pages/testToolPages/VerificationGpsLock.qml @@ -9,7 +9,7 @@ import Sailfish.Silica 1.0 import Sailfish.Policy 1.0 import com.jolla.settings.system 1.0 import QtPositioning 5.0 -import org.nemomobile.time 1.0 +import Nemo.Time 1.0 import org.nemomobile.systemsettings 1.0 import Csd 1.0 import ".." @@ -152,6 +152,8 @@ CsdTestPage { contentWidth: column.width contentHeight: column.height + bottomMargin: Theme.paddingLarge + Column { id: column diff --git a/usr/share/csd/pages/testToolPages/VerificationGpsRadio.qml b/usr/share/csd/pages/testToolPages/VerificationGpsRadio.qml index 506ea34c..b18eb3f9 100644 --- a/usr/share/csd/pages/testToolPages/VerificationGpsRadio.qml +++ b/usr/share/csd/pages/testToolPages/VerificationGpsRadio.qml @@ -94,6 +94,8 @@ CsdTestPage { contentWidth: column.width contentHeight: column.height + bottomMargin: Theme.paddingLarge + Column { id: column diff --git a/usr/share/csd/pages/testToolPages/VerificationGyroAndGSensor.qml b/usr/share/csd/pages/testToolPages/VerificationGyroAndGSensor.qml index 498d56c9..e256896c 100644 --- a/usr/share/csd/pages/testToolPages/VerificationGyroAndGSensor.qml +++ b/usr/share/csd/pages/testToolPages/VerificationGyroAndGSensor.qml @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016 - 2019 Jolla Ltd. + * Copyright (c) 2016 - 2023 Jolla Ltd. * * License: Proprietary */ @@ -16,7 +16,7 @@ CsdTestPage { property bool runGSensorTest: Features.supported("GSensor") property bool _runBothTests: runGyroTest && runGSensorTest - property int _resultsColumnWidth: _runBothTests ? width/2 : width + property int _resultsColumnWidth: _runBothTests && orientation == Orientation.Landscape ? width/2 : width function _checkForFinished() { if ((!runGyroTest || gyroTest.done) && (!runGSensorTest || gSensorTest.done)) { @@ -25,7 +25,11 @@ CsdTestPage { } } - allowedOrientations: _runBothTests ? Orientation.Landscape : Orientation.Portrait + // Workaround for string.arg(real) with zero format controls + function rounded(val) { + var sign = val < 0 ? "" : "+" + return sign + val.toFixed(4) + } GyroTest { id: gyroTest @@ -49,7 +53,7 @@ CsdTestPage { } } - Flickable { + SilicaFlickable { anchors.fill: parent contentHeight: contentColumn.height + Theme.paddingLarge @@ -113,13 +117,7 @@ CsdTestPage { //: X, Y and Z values of the Gyroscope sensor //% "X: %1
Y: %2
Z: %3" - text: { - if (gyroTest.running) { - return qsTrId("csd-la-gyro_output").arg(gyroTest.sensorX).arg(gyroTest.sensorY).arg(gyroTest.sensorZ) - } else { - return qsTrId("csd-la-gyro_output").arg(gyroTest.valueX).arg(gyroTest.valueY).arg(gyroTest.valueZ) - } - } + text: qsTrId("csd-la-gyro_output").arg(rounded(gyroTest.curX)).arg(rounded(gyroTest.curY)).arg(rounded(gyroTest.curZ)) } Label { @@ -137,16 +135,14 @@ CsdTestPage { visible: gyroTest.done x: Theme.horizontalPageMargin - //% "Pass" - text: gyroResultLabel.result ? qsTrId("csd-la-pass") + "\n" + - "X: " + gyroTest.sensorX + "\n" + - "Y: " + gyroTest.sensorY + "\n" + - "Z: " + gyroTest.sensorZ : - //% "Fail" - qsTrId("csd-la-fail")+ "\n" + - "X: " + gyroTest.sensorX + "\n" + - "Y: " + gyroTest.sensorY + "\n" + - "Z: " + gyroTest.sensorZ + text: (gyroResultLabel.result + //% "Pass" + ? qsTrId("csd-la-pass") + //% "Fail" + : qsTrId("csd-la-fail")) + + "\nX: %1".arg(rounded(gyroTest.avgX)) + + "\nY: %1".arg(rounded(gyroTest.avgY)) + + "\nZ: %1".arg(rounded(gyroTest.avgZ)) } Label { @@ -187,11 +183,11 @@ CsdTestPage { width: parent.width - 2*x wrapMode: Text.Wrap text: //% "Gx: %0 (min: %1, max: %2)" - qsTrId("csd-la-accelerometer_readings_x").arg(Math.round(gSensorTest.gsensorX * 1000) / 1000).arg(gSensorTest.minX).arg(gSensorTest.maxX) + "\n" + + qsTrId("csd-la-accelerometer_readings_x").arg(rounded(gSensorTest.curX)).arg(gSensorTest.minX).arg(gSensorTest.maxX) + "\n" + //% "Gy: %0 (min: %1, max: %2)" - qsTrId("csd-la-accelerometer_readings_y").arg(Math.round(gSensorTest.gsensorY * 1000) / 1000).arg(gSensorTest.minY).arg(gSensorTest.maxY) + "\n" + + qsTrId("csd-la-accelerometer_readings_y").arg(rounded(gSensorTest.curY)).arg(gSensorTest.minY).arg(gSensorTest.maxY) + "\n" + //% "Gz: %0 (min: %1, max: %2)" - qsTrId("csd-la-accelerometer_readings_z").arg(Math.round(gSensorTest.gsensorZ * 1000) / 1000).arg(gSensorTest.minZ).arg(gSensorTest.maxZ) + qsTrId("csd-la-accelerometer_readings_z").arg(rounded(gSensorTest.curZ)).arg(gSensorTest.minZ).arg(gSensorTest.maxZ) } @@ -201,22 +197,17 @@ CsdTestPage { visible: gSensorTest.done x: Theme.horizontalPageMargin - //% "Pass" - text: gSensorResultLabel.result ? qsTrId("csd-la-pass") + "\n" + - //% "Gx: %0 (min: %1, max: %2)" - qsTrId("csd-la-accelerometer_readings_x").arg(gSensorTest.averageGsensorX).arg(gSensorTest.minX).arg(gSensorTest.maxX) + "\n" + - //% "Gy: %0 (min: %1, max: %2)" - qsTrId("csd-la-accelerometer_readings_y").arg(gSensorTest.averageGsensorY).arg(gSensorTest.minY).arg(gSensorTest.maxY) + "\n" + - //% "Gz: %0 (min: %1, max: %2)" - qsTrId("csd-la-accelerometer_readings_z").arg(gSensorTest.averageGsensorZ).arg(gSensorTest.minZ).arg(gSensorTest.maxZ) : - //% "Fail" - qsTrId("csd-la-fail") + "\n" + - //% "Gx: %0 (min: %1, max: %2)" - qsTrId("csd-la-accelerometer_readings_x").arg(gSensorTest.averageGsensorX).arg(gSensorTest.minX).arg(gSensorTest.maxX) + "\n" + - //% "Gy: %0 (min: %1, max: %2)" - qsTrId("csd-la-accelerometer_readings_y").arg(gSensorTest.averageGsensorY).arg(gSensorTest.minY).arg(gSensorTest.maxY) + "\n" + - //% "Gz: %0 (min: %1, max: %2)" - qsTrId("csd-la-accelerometer_readings_z").arg(gSensorTest.averageGsensorZ).arg(gSensorTest.minZ).arg(gSensorTest.maxZ) + text: (gSensorResultLabel.result + //% "Pass" + ? qsTrId("csd-la-pass") + //% "Fail" + : qsTrId("csd-la-fail")) + //% "Gx: %0 (min: %1, max: %2)" + + "\n" + qsTrId("csd-la-accelerometer_readings_x").arg(rounded(gSensorTest.avgX)).arg(gSensorTest.minX).arg(gSensorTest.maxX) + //% "Gy: %0 (min: %1, max: %2)" + + "\n" + qsTrId("csd-la-accelerometer_readings_y").arg(rounded(gSensorTest.avgY)).arg(gSensorTest.minY).arg(gSensorTest.maxY) + //% "Gz: %0 (min: %1, max: %2)" + + "\n" + qsTrId("csd-la-accelerometer_readings_z").arg(rounded(gSensorTest.avgZ)).arg(gSensorTest.minZ).arg(gSensorTest.maxZ) } Label { diff --git a/usr/share/csd/pages/testToolPages/VerificationHallDetect.qml b/usr/share/csd/pages/testToolPages/VerificationHallDetect.qml index 3d0de22a..59cb4b4a 100644 --- a/usr/share/csd/pages/testToolPages/VerificationHallDetect.qml +++ b/usr/share/csd/pages/testToolPages/VerificationHallDetect.qml @@ -7,7 +7,7 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 import Csd 1.0 -import org.nemomobile.dbus 2.0 +import Nemo.DBus 2.0 import ".." CsdTestPage { @@ -60,7 +60,7 @@ CsdTestPage { mce.originalValue = r }) mce.typedCall("set_config", [ { type: "s", value: "/system/osso/dsm/locks/lid_sensor_enabled"}, {type: "v", value: false} ]) - _ngfEffect = Qt.createQmlObject("import org.nemomobile.ngf 1.0; NonGraphicalFeedback { event: 'unlock_device' }", + _ngfEffect = Qt.createQmlObject("import Nemo.Ngf 1.0; NonGraphicalFeedback { event: 'unlock_device' }", page, 'NonGraphicalFeedback'); } diff --git a/usr/share/csd/pages/testToolPages/VerificationKey.qml b/usr/share/csd/pages/testToolPages/VerificationKey.qml index 4e94f03d..7bb0340f 100644 --- a/usr/share/csd/pages/testToolPages/VerificationKey.qml +++ b/usr/share/csd/pages/testToolPages/VerificationKey.qml @@ -9,8 +9,8 @@ import Sailfish.Silica 1.0 import Sailfish.Silica.private 1.0 as Private import Sailfish.Media 1.0 import Csd 1.0 -import org.nemomobile.policy 1.0 -import org.nemomobile.dbus 2.0 +import Nemo.Policy 1.0 +import Nemo.DBus 2.0 import ".." CsdTestPage { diff --git a/usr/share/csd/pages/testToolPages/VerificationLED.qml b/usr/share/csd/pages/testToolPages/VerificationLED.qml index 772cece4..4546f197 100644 --- a/usr/share/csd/pages/testToolPages/VerificationLED.qml +++ b/usr/share/csd/pages/testToolPages/VerificationLED.qml @@ -6,7 +6,7 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 -import org.nemomobile.dbus 2.0 +import Nemo.DBus 2.0 import Csd 1.0 import ".." diff --git a/usr/share/csd/pages/testToolPages/VerificationLcd.qml b/usr/share/csd/pages/testToolPages/VerificationLcd.qml index e5d9162a..58aa8859 100644 --- a/usr/share/csd/pages/testToolPages/VerificationLcd.qml +++ b/usr/share/csd/pages/testToolPages/VerificationLcd.qml @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016 - 2019 Jolla Ltd. + * Copyright (c) 2016 - 2022 Jolla Ltd. * * License: Proprietary */ @@ -11,56 +11,51 @@ import ".." CsdTestPage { id: page + readonly property var testData: [ + //% "Showing: Color gradient" + [qsTrId("csd-la-lcd_show_gradient"), "black"], + //% "Showing: Black" + [qsTrId("csd-la-lcd_show_black"), "black"], + //% "Showing: White" + [qsTrId("csd-la-lcd_show_white"), "white"], + //% "Showing: Red" + [qsTrId("csd-la-lcd_show_red"), "red"], + //% "Showing: Green" + [qsTrId("csd-la-lcd_show_green"), "green"], + //% "Showing: Blue" + [qsTrId("csd-la-lcd_show_blue"), "blue"], + // Sentinel + ["", Theme.overlayBackgroundColor]] property int testCase + readonly property string testLabel: testData[testCase][0] + readonly property string testColor: testData[testCase][1] + readonly property bool testEnded: testLabel == "" + readonly property string textColor: testColor == "white" ? "black" : "yellow" function progressTest() { testCase++ - - rect.visible = true - - switch (testCase) { - case 1: - rect.color = "white" - break - case 2: - rect.color = "black" - break - case 3: - rect.color = "red" - break - case 4: - rect.color = "green" - break - case 5: - rect.color = "blue" - break - default: - rect.color = Theme.overlayBackgroundColor - buttonText.visible = true - buttonRow.visible = true - ma.enabled = false - break - } } Image { + id: image + visible: testCase == 0 anchors.fill: parent source: "/usr/share/csd/testdata/lcdtest.png" } Rectangle { id: rect - color: "white" + color: testColor anchors.fill: parent - visible: false + visible: !image.visible Label { id: buttonText anchors.centerIn: parent width: parent.width - (Theme.paddingLarge * 2) wrapMode: Text.Wrap - visible: false + visible: testEnded font.pixelSize: Theme.fontSizeLarge //% "Does it show RGB?" @@ -69,7 +64,7 @@ CsdTestPage { ButtonLayout { id: buttonRow - visible: false + visible: testEnded anchors { top: buttonText.bottom @@ -93,11 +88,37 @@ CsdTestPage { } } + Label { + id: showingColorLabel + color: textColor + visible: !buttonRow.visible + anchors { + left: tapToProceedLabel.left + bottom: tapToProceedLabel.top + } + text: testLabel + } + + Label { + id: tapToProceedLabel + color: textColor + visible: showingColorLabel.visible + anchors { + left: parent.left + leftMargin: Theme.paddingLarge + bottom: parent.bottom + bottomMargin: Theme.paddingLarge + } + //% "Tap screen to proceed" + text: qsTrId("csd-la-lcd_tap_to_proceed") + } + MouseArea { id: ma anchors.fill: parent onClicked: progressTest() + enabled: !testEnded } Timer { diff --git a/usr/share/csd/pages/testToolPages/VerificationLcdBacklight.qml b/usr/share/csd/pages/testToolPages/VerificationLcdBacklight.qml index f12610f6..a54ce98e 100644 --- a/usr/share/csd/pages/testToolPages/VerificationLcdBacklight.qml +++ b/usr/share/csd/pages/testToolPages/VerificationLcdBacklight.qml @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016 - 2019 Jolla Ltd. + * Copyright (c) 2016 - 2022 Jolla Ltd. * * License: Proprietary */ @@ -14,20 +14,52 @@ CsdTestPage { property bool ambientLightSensorEnabled property int originalBrightness + property bool originalValuesSaved + property bool testStarted + property bool testStopped + readonly property bool testRunning: testStarted && !testStopped function resetOriginalValues() { - displaySettings.ambientLightSensorEnabled = ambientLightSensorEnabled - displaySettings.brightness = originalBrightness + if (originalValuesSaved) { + displaySettings.ambientLightSensorEnabled = ambientLightSensorEnabled + displaySettings.brightness = originalBrightness + } + } + + function setupTest() { + // Save original backlight settings + if (!originalValuesSaved) { + ambientLightSensorEnabled = displaySettings.ambientLightSensorEnabled + originalBrightness = displaySettings.brightness + originalValuesSaved = true + } + + // Max out the brightness before test + displaySettings.brightness = displaySettings.maximumBrightness + // Also disable the ambient light sensor. + displaySettings.ambientLightSensorEnabled = false + + if (runInTests) { + startTest() + } } function startTest() { - hintText.visible = false - okButton.visible = false - rect.color = "white" + testStarted = true + testStartTimer.start() + } - displaySettings.brightness = 1 + function stopTest() { + testStopped = true + resetOriginalValues() + if (runInTests) { + completeTest(true) + } + } - testTimer.start() + function completeTest(result) { + setTestResult(result) + testCompleted(true) } onStatusChanged: { @@ -41,10 +73,8 @@ CsdTestPage { } Rectangle { - id: rect - anchors.fill: parent - color: Theme.overlayBackgroundColor + color: testRunning ? "white" : Theme.overlayBackgroundColor } Column { @@ -55,14 +85,15 @@ CsdTestPage { title: qsTrId("csd-he-lcd_backlight") } DescriptionItem { - id: hintText + visible: !testStarted //% "This test will display a white screen, after which the screen should become visibly dimmer. Press 'Start' to test." text: qsTrId("csd-la-verification_lcd_backglight_operation_hint_description") } } BottomButton { - id: okButton + visible: !testStarted + enabled: originalValuesSaved //% "Start" text: qsTrId("csd-la-start") onClicked: startTest() @@ -71,7 +102,7 @@ CsdTestPage { Label { id: buttonText - visible: false + visible: testStopped font.pixelSize: Theme.fontSizeLarge x: Theme.paddingLarge width: parent.width - 2*x @@ -83,58 +114,47 @@ CsdTestPage { } ButtonLayout { - id: buttonRow anchors { top: buttonText.bottom topMargin: Theme.paddingLarge horizontalCenter: parent.horizontalCenter } rowSpacing: Theme.paddingMedium - visible: false + visible: testStopped PassButton { - onClicked: { - setTestResult(true) - testCompleted(true) - } + onClicked: completeTest(true) } FailButton { - onClicked: { - setTestResult(false) - testCompleted(true) - } + onClicked: completeTest(false) } } Timer { - id: testTimer - interval: 4000 + // Hold white screen at maximum brightness for a while to + // calm things down and thus highlight the dimming when it + // actually commences. + id: testStartTimer + interval: 1000 onTriggered: { - rect.color = Theme.overlayBackgroundColor - buttonText.visible = true - buttonRow.visible = true - resetOriginalValues() - if (runInTests) { - setTestResult(true) - testCompleted(true) - } + displaySettings.brightness = 1 + testStopTimer.start() } } + Timer { + // When there are brightness setting changes (like what we have + // here), mce drives the fade in/out through in 600 ms. + // + // Waiting a bit longer than that yields stable state also at + // the minimum brightness end. + id: testStopTimer + interval: 600 + 1000 + onTriggered: stopTest() + } + DisplaySettings { id: displaySettings - onPopulatedChanged: { - // Save existing backlight settings - page.ambientLightSensorEnabled = displaySettings.ambientLightSensorEnabled - originalBrightness = displaySettings.brightness - // Max out the brightness before test - displaySettings.brightness = displaySettings.maximumBrightness - // Also disable the ambient light sensor. - displaySettings.ambientLightSensorEnabled = false - - if (runInTests) { - startTest() - } - } + onPopulatedChanged: setupTest() } } diff --git a/usr/share/csd/pages/testToolPages/VerificationLightSensor.qml b/usr/share/csd/pages/testToolPages/VerificationLightSensor.qml index c59bfb9b..2c193ec3 100644 --- a/usr/share/csd/pages/testToolPages/VerificationLightSensor.qml +++ b/usr/share/csd/pages/testToolPages/VerificationLightSensor.qml @@ -18,32 +18,66 @@ CsdTestPage { property int valueLow: -1 property int valueHigh: -1 property int currentLightValue - - Column { - width: parent.width - spacing: Theme.paddingLarge - CsdPageHeader { - //% "Light sensor" - title: qsTrId("csd-he-light_sensor") - } - DescriptionItem { - id: guideText - //% "1. Cover the light sensor
2. Uncover the sensor and put light source over it
" - text: qsTrId("csd-la-verification_light_sensor_description") - } - Label { - x: Theme.paddingLarge - //% "Lowest sensor value: %1
Highest sensor value: %2
Difference pass criteria: %3
Difference: %4
Current light value: %5" - text: qsTrId("csd-la-verification_light_sensor_value").arg(valueLow).arg(valueHigh).arg(passCriteria).arg(sensorDifference).arg(currentLightValue) - } - ResultLabel { - id: passText - x: Theme.paddingLarge - visible: false - result: true + readonly property int columnWidth: orientation == Orientation.Landscape ? width/2 : width + + SilicaFlickable { + anchors.fill: parent + contentHeight: contentColumn.height + Theme.paddingLarge + + VerticalScrollDecorator {} + + Column { + id: contentColumn + + width: parent.width + CsdPageHeader { + //% "Light sensor" + title: qsTrId("csd-he-light_sensor") + } + + Flow { + width: parent.width + Column { + width: columnWidth + spacing: Theme.paddingLarge + + DescriptionItem { + id: guideText + //% "1. Cover the light sensor
2. Uncover the sensor and put light source over it
" + text: qsTrId("csd-la-verification_light_sensor_description") + } + Label { + x: Theme.paddingLarge + //% "Lowest sensor value: %1
Highest sensor value: %2
Difference pass criteria: %3
Difference: %4
Current light value: %5" + text: qsTrId("csd-la-verification_light_sensor_value").arg(valueLow).arg(valueHigh).arg(passCriteria).arg(sensorDifference).arg(currentLightValue) + } + } + Column { + width: columnWidth + spacing: Theme.paddingLarge + + ResultLabel { + id: passText + x: Theme.paddingLarge + visible: false + result: true + } + + FailBottomButton { + id: failButton + anchors.bottom: undefined + anchors.bottomMargin: 0 + onClicked: { + setTestResult(false) + testCompleted(true) + } + } + } + } } } + LightSensor { id: lightSensor active: true @@ -73,12 +107,4 @@ CsdTestPage { } } } - - FailBottomButton { - id: failButton - onClicked: { - setTestResult(false) - testCompleted(true) - } - } } diff --git a/usr/share/csd/pages/testToolPages/VerificationMacAddresses.qml b/usr/share/csd/pages/testToolPages/VerificationMacAddresses.qml index 62f95315..52be6c75 100644 --- a/usr/share/csd/pages/testToolPages/VerificationMacAddresses.qml +++ b/usr/share/csd/pages/testToolPages/VerificationMacAddresses.qml @@ -13,8 +13,15 @@ import ".." CsdTestPage { id: page - property bool wlanValid: macValidator.isMacValid("wireless", aboutSettings.wlanMacAddress) - property bool bluetoothValid: macValidator.isMacValid("bluetooth", macValidator.getMac("bluetooth")) + property bool wlanSupported: Features.supported("Wifi") + property bool bluetoothSupported: Features.supported("Bluetooth") + + property string wlanMac: wlanSupported ? aboutSettings.wlanMacAddress : "" + property string bluetoothMac: bluetoothSupported ? macValidator.getMac("bluetooth") : "" + + property bool wlanValid: !wlanSupported || macValidator.isMacValid("wireless", wlanMac) + property bool bluetoothValid: !bluetoothSupported || macValidator.isMacValid("bluetooth", bluetoothMac) + property bool allMacsOk: wlanValid && bluetoothValid Component.onDestruction: { @@ -40,12 +47,14 @@ CsdTestPage { SectionHeader { //% "Wireless MAC" text: qsTrId("csd-he-wireless-mac") + visible: wlanSupported } Label { x: Theme.paddingLarge width: page.width - (2 * Theme.paddingLarge) wrapMode: Text.Wrap + visible: wlanSupported color: wlanValid ? "green" @@ -62,9 +71,10 @@ CsdTestPage { x: Theme.paddingLarge width: page.width - (2 * Theme.paddingLarge) wrapMode: Text.Wrap + visible: wlanSupported //% "Value: %1" - text: qsTrId("csd-la-value").arg(aboutSettings.wlanMacAddress) + text: qsTrId("csd-la-value").arg(wlanMac) } Label { @@ -81,12 +91,14 @@ CsdTestPage { SectionHeader { //% "Bluetooth MAC" text: qsTrId("csd-he-bluetooth-mac") + visible: bluetoothSupported } Label { x: Theme.paddingLarge width: page.width - (2 * Theme.paddingLarge) wrapMode: Text.Wrap + visible: bluetoothSupported color: bluetoothValid ? "green" @@ -103,9 +115,10 @@ CsdTestPage { x: Theme.paddingLarge width: page.width - (2 * Theme.paddingLarge) wrapMode: Text.Wrap + visible: bluetoothSupported //% "Value: %1" - text: qsTrId("csd-la-value").arg(macValidator.getMac("bluetooth")) + text: qsTrId("csd-la-value").arg(bluetoothMac) } Label { diff --git a/usr/share/csd/pages/testToolPages/VerificationMultiTouch.qml b/usr/share/csd/pages/testToolPages/VerificationMultiTouch.qml index 6e5a97fd..5009a6f9 100644 --- a/usr/share/csd/pages/testToolPages/VerificationMultiTouch.qml +++ b/usr/share/csd/pages/testToolPages/VerificationMultiTouch.qml @@ -13,6 +13,7 @@ import ".." CsdTestPage { id: page backNavigation: false + onOrientationChanged: canvas.clear() Private.WindowGestureOverride { id: windowGestureOverride diff --git a/usr/share/csd/pages/testToolPages/VerificationSim.qml b/usr/share/csd/pages/testToolPages/VerificationSim.qml index ab4528b6..71914993 100644 --- a/usr/share/csd/pages/testToolPages/VerificationSim.qml +++ b/usr/share/csd/pages/testToolPages/VerificationSim.qml @@ -7,7 +7,7 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 import org.nemomobile.ofono 1.0 -import MeeGo.QOfono 0.2 +import QOfono 0.2 import Csd 1.0 import ".." diff --git a/usr/share/csd/pages/testToolPages/VerificationSimAutoTest.qml b/usr/share/csd/pages/testToolPages/VerificationSimAutoTest.qml index 0005b515..fa1dbdc3 100644 --- a/usr/share/csd/pages/testToolPages/VerificationSimAutoTest.qml +++ b/usr/share/csd/pages/testToolPages/VerificationSimAutoTest.qml @@ -7,7 +7,7 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 import org.nemomobile.ofono 1.0 -import MeeGo.QOfono 0.2 +import QOfono 0.2 import Csd 1.0 import ".." diff --git a/usr/share/csd/pages/testToolPages/VerificationTechnologyModel.qml b/usr/share/csd/pages/testToolPages/VerificationTechnologyModel.qml index 76840520..a0cea386 100644 --- a/usr/share/csd/pages/testToolPages/VerificationTechnologyModel.qml +++ b/usr/share/csd/pages/testToolPages/VerificationTechnologyModel.qml @@ -5,7 +5,7 @@ */ import QtQml 2.2 -import MeeGo.Connman 0.2 +import Connman 0.2 import Sailfish.Policy 1.0 TechnologyModel { diff --git a/usr/share/csd/pages/testToolPages/VerificationToh.qml b/usr/share/csd/pages/testToolPages/VerificationToh.qml deleted file mode 100644 index 9bf7177e..00000000 --- a/usr/share/csd/pages/testToolPages/VerificationToh.qml +++ /dev/null @@ -1,127 +0,0 @@ -/* - * Copyright (c) 2016 - 2019 Jolla Ltd. - * - * License: Proprietary - */ - -import QtQuick 2.0 -import Sailfish.Silica 1.0 -import Csd 1.0 -import ".." - -CsdTestPage { - id: page - - Column { - width: page.width - - CsdPageHeader { - //% "TOH" - title: qsTrId("csd-he-toh") - } - - Label { - wrapMode: Text.Wrap - x: Theme.paddingLarge - width: parent.width - 2*Theme.paddingLarge - color: Theme.highlightColor - //% "Tests TOH cover detection and identification. Attach a TOH enabled cover." - text: qsTrId("csd-la-toh_instructions") - } - - Label { - x: Theme.paddingLarge - width: parent.width - 2*Theme.paddingLarge - wrapMode: Text.Wrap - text: { - if (!toh.docked) { - //% "Cover is not attached" - return qsTrId("csd-la-cover_not_attached") - } else if (!toh.ready) { - //% "Scanning..." - return qsTrId("csd-la-scanning") - } else if (toh.tohId === "") { - //% "No tag found" - return qsTrId("csd-la-tag_not_found") - } else { - //% "TOH detected and identified" - return qsTrId("csd-la-toh_identified") - } - } - } - - Label { - x: Theme.paddingLarge - color: toh.passed ? "green" : "red" - //% "Pass" - text: toh.passed ? qsTrId("csd-la-pass") - //% "Fail" - : qsTrId("csd-la-fail") - } - - SectionHeader { - //% "TOH status information" - text: qsTrId("csd-he-toh_status_information") - } - - Row { - x: Theme.paddingLarge - spacing: Theme.paddingSmall - - Label { - //% "Cover state:" - text: qsTrId("csd-la-cover_state") - } - - Label { - //% "attached" - text: toh.docked ? qsTrId("csd-la-attached") - //% "detached" - : qsTrId("csd-la-detached") - } - } - - Row { - x: Theme.paddingLarge - spacing: Theme.paddingSmall - - Label { - //% "TOH state:" - text: qsTrId("csd-la-toh_state") - } - - Label { - //% "ready" - text: toh.ready ? qsTrId("csd-la-ready") - //% "not ready" - : qsTrId("csd-la-not_ready") - } - } - - Label { - x: Theme.paddingLarge - //% "TOH Id: %1" - text: qsTrId("csd-la-toh_id").arg(toh.tohId) - } - } - - - Toh { - id: toh - - property bool passed: toh.docked && toh.ready && toh.tohId !== "" - onPassedChanged: check() - - function check() { - setTestResult(toh.passed) - testCompleted(false) - } - } - - Timer { - id: timer - interval: 1000 - running: true - onTriggered: toh.check() - } -} diff --git a/usr/share/csd/pages/testToolPages/VerificationTouch.qml b/usr/share/csd/pages/testToolPages/VerificationTouch.qml index 502b035f..40a9c114 100644 --- a/usr/share/csd/pages/testToolPages/VerificationTouch.qml +++ b/usr/share/csd/pages/testToolPages/VerificationTouch.qml @@ -16,10 +16,10 @@ CsdTestPage { backNavigation: false property int targetCellSize: 60 * Theme.pixelRatio - property int horizontalCells: Math.floor(Screen.width / targetCellSize) - property int verticalCells: Math.floor(Screen.height / targetCellSize) - property real cellWidth: Screen.width / horizontalCells - property real cellHeight: Screen.height / verticalCells + property int horizontalCells: Math.floor(width / targetCellSize) + property int verticalCells: Math.floor(height / targetCellSize) + property real cellWidth: width / horizontalCells + property real cellHeight: height / verticalCells Private.WindowGestureOverride { id: windowGestureOverride @@ -98,7 +98,7 @@ CsdTestPage { Rectangle { width: cellWidth height: cellHeight - color: index % 2 == 0 ? "white" : "grey" + color: index % 2 == 0 ? "grey" : "white" } } } diff --git a/usr/share/csd/pages/testToolPages/VerificationVideoPlayback.qml b/usr/share/csd/pages/testToolPages/VerificationVideoPlayback.qml index 53d74bb8..e40027f5 100644 --- a/usr/share/csd/pages/testToolPages/VerificationVideoPlayback.qml +++ b/usr/share/csd/pages/testToolPages/VerificationVideoPlayback.qml @@ -22,12 +22,6 @@ CsdTestPage { property int minimumPlayingTime: runInTests ? page.parameters["RunInTestTime"] * 60*1000 : 15000 - - allowedOrientations: firstVideoLoaded ? ((video.contentRect.height > video.contentRect.width) ? - Orientation.Portrait : Orientation.Landscape) : - Orientation.Portrait - orientation: allowedOrientations - property bool originalAmbientLightSensor property int originalBrightness diff --git a/usr/share/csd/pages/testToolPages/VerificationWifi.qml b/usr/share/csd/pages/testToolPages/VerificationWifi.qml index fe418ec3..436dc3fe 100644 --- a/usr/share/csd/pages/testToolPages/VerificationWifi.qml +++ b/usr/share/csd/pages/testToolPages/VerificationWifi.qml @@ -9,7 +9,7 @@ import Sailfish.Silica 1.0 import Sailfish.Policy 1.0 import com.jolla.settings.system 1.0 import Sailfish.Settings.Networking 1.0 -import MeeGo.Connman 0.2 +import Connman 0.2 import ".." CsdTestPage { @@ -142,19 +142,29 @@ CsdTestPage { } } - Column { - id: contentColumn - anchors.top: mdmBanner.active ? mdmBanner.bottom : header.bottom - anchors.topMargin: mdmBanner.active ? Theme.paddingLarge : 0 - x: Theme.horizontalPageMargin - width: parent.width - 2*x - height: parent.height - header.height - (mdmBanner.active ? (mdmBanner.height+Theme.paddingLarge) : 0) - (buttonSet.height + buttonSet.anchors.bottomMargin) - spacing: Theme.paddingLarge + + SilicaFlickable { + anchors { + top: mdmBanner.active ? mdmBanner.bottom : header.bottom + topMargin: mdmBanner.active ? Theme.paddingLarge : 0 + bottom: buttonSet.top + bottomMargin: Theme.paddingLarge + left: parent.left + right: parent.right + } + contentHeight: contentColumn.height clip: true + VerticalScrollDecorator { + Component.onCompleted: showDecorator() + } + Column { - id: topInfoColumn - width: parent.width + id: contentColumn + + x: Theme.horizontalPageMargin + width: parent.width - 2*x + spacing: Theme.paddingLarge Label { width: parent.width @@ -181,70 +191,58 @@ CsdTestPage { text: qsTrId("csd-la-wifi_networks") visible: wifiTechModel.count > 0 } - } - - SilicaFlickable { - id: results - width: parent.width - height: parent.height - topInfoColumn.height - contentHeight: foundNetworks.height - clip: true - Column { - id: foundNetworks - width: parent.width - Repeater { - model: wifiTechModel - delegate: Column { + Repeater { + model: wifiTechModel + delegate: Column { + width: parent.width + Item { width: parent.width - Item { - width: parent.width - height: wifiName.height - - Label { - id: wifiName - anchors { - left: parent.left - right: icon.right - } - text: networkService.name - ? networkService.name - //% "Hidden network" - : qsTrId("csd-la-hidden_network") - color: Theme.highlightColor - } + height: wifiName.height - Image { - id: icon - anchors { - right: parent.right - } - source: "image://theme/icon-m-wlan-" + WlanUtils.getStrengthString(modelData.strength) + "?" + Theme.highlightColor + Label { + id: wifiName + anchors { + left: parent.left + right: icon.right } + text: networkService.name + ? networkService.name + //% "Hidden network" + : qsTrId("csd-la-hidden_network") + color: Theme.highlightColor } - Label { - text: networkService.frequency + " MHz" - color: Theme.secondaryHighlightColor - font.pixelSize: Theme.fontSizeSmall + Image { + id: icon + anchors { + right: parent.right + } + source: "image://theme/icon-m-wlan-" + WlanUtils.getStrengthString(modelData.strength) + "?" + Theme.highlightColor } + } - Label { - text: networkService.strength + " %" - color: Theme.secondaryHighlightColor - font.pixelSize: Theme.fontSizeSmall - } + Label { + text: networkService.frequency + " MHz" + color: Theme.secondaryHighlightColor + font.pixelSize: Theme.fontSizeSmall + } - Label { - text: networkService.security - color: Theme.secondaryHighlightColor - font.pixelSize: Theme.fontSizeSmall - } + Label { + text: networkService.strength + " %" + color: Theme.secondaryHighlightColor + font.pixelSize: Theme.fontSizeSmall + } - Item { - width: parent.width - height: Theme.paddingLarge - } + Label { + text: networkService.security + color: Theme.secondaryHighlightColor + font.pixelSize: Theme.fontSizeSmall + } + + Item { + width: parent.width + height: Theme.paddingLarge } } } @@ -253,6 +251,7 @@ CsdTestPage { Column { id: buttonSet + visible: timerLabel.visible || restartButton.visible || exitButton.visible anchors { left: parent.left leftMargin: Theme.horizontalPageMargin @@ -268,6 +267,7 @@ CsdTestPage { width: parent.width wrapMode: Text.Wrap font.bold: true + visible: opacity > 0 opacity: timeoutTimer.running ? 1 : 0 Behavior on opacity { FadeAnimation { } } diff --git a/usr/share/doc/qt5/global/template/scripts/extras.js b/usr/share/doc/qt5/global/template/scripts/extras.js new file mode 100644 index 00000000..ba7a4a50 --- /dev/null +++ b/usr/share/doc/qt5/global/template/scripts/extras.js @@ -0,0 +1,80 @@ +var vOffset_init = 65; +var vOffset = vOffset_init; +var c = 'collapsed'; + +function toggleList(toggle, content, maxItems) { + if (toggle.css('display') == 'none') { + vOffset = vOffset_init; + toggle.removeClass(c); + content.show(); + return; + } else + vOffset = 8; + + if (maxItems > content.children().length) + return; + content.hide(); + toggle.addClass(c); +} + +$(function () { + $('a[href*=#]:not([href=#])').on('click', function (e) { + if (e.which == 2 || e.metaKey || e.ctrlKey || e.shiftKey) + return true; + var target = $(this.hash.replace(/(\.)/g, "\\$1")); + target = target.length ? target : $('[name=' + this.hash.slice(1) + ']'); + if (target.length) { + setTimeout(function () { + $('html, body').animate({scrollTop: target.offset().top - vOffset}, 50);}, 50); + } + }); +}); + +$(window).load(function () { + var hashChanged = function() { + var h = window.location.hash; + var re = /[^a-z0-9_\.\#\-]/i + if (h.length > 1 && !re.test(h)) { + setTimeout(function () { + var tgt = $(h.replace(/(\.)/g, "\\$1")); + tgt = tgt.length ? tgt : $('[name=' + h.slice(1) + ']'); + $(window).scrollTop(tgt.offset().top - vOffset); + }, 0); + } + } + $(window).bind('hashchange', hashChanged); + hashChanged.call(); + + if (!$('.sidebar toc').is(':empty')) { + $('
').prependTo('.sidebar .toc'); + var toc = $('.sidebar .toc ul'); + var tocToggle = $('#toc-toggle'); + var tocCallback = function() { toggleList(tocToggle, toc, 4); }; + + $('#toc-toggle').on('click', function(e) { + e.stopPropagation(); + toc.toggle(); + tocToggle.toggleClass(c); + }); + + tocCallback.call(); + $(window).resize(tocCallback); + } + + if (!$('#sidebar-content').is(':empty')) { + $('#sidebar-content h2').first().clone().prependTo('#sidebar-content'); + $('').prependTo('#sidebar-content'); + var sb = $('#sidebar-content .sectionlist'); + var sbToggle = $('#sidebar-toggle'); + var sbCallback = function() { toggleList(sbToggle, sb, 0); }; + + $('#sidebar-toggle').on('click', function(e) { + e.stopPropagation(); + sb.toggle(); + sbToggle.toggleClass(c); + }); + + sbCallback.call(); + $(window).resize(sbCallback); + } +}); diff --git a/usr/share/doc/qt5/global/template/scripts/main.js b/usr/share/doc/qt5/global/template/scripts/main.js new file mode 100644 index 00000000..823cebec --- /dev/null +++ b/usr/share/doc/qt5/global/template/scripts/main.js @@ -0,0 +1,241 @@ +"use strict"; + +function createCookie(name, value, days) { + var expires; + if (days) { + var date = new Date(); + date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000)); + expires = "; expires=" + date.toGMTString(); + } else { + expires = ""; + } + document.cookie = escape(name) + "=" + escape(value) + expires + "; path=/"; + $('.cookies_yum').click(function() { + $(this).fadeOut() + }); +} +function readCookie(name) { + var nameEQ = escape(name) + "="; + var ca = document.cookie.split(';'); + for (var i = 0; i < ca.length; i++) { + var c = ca[i]; + while (c.charAt(0) === ' ') c = c.substring(1, c.length); + if (c.indexOf(nameEQ) === 0) return unescape(c.substring(nameEQ.length, c.length)); + } + return null; +} +function eraseCookie(name) { + createCookie(name, "", -1); +} +function load_sdk(s, id, src) { + var js, fjs = document.getElementsByTagName(s)[0]; + if (document.getElementById(id)) return; + js = document.createElement(s); + js.id = id; + js.src = src; + fjs.parentNode.insertBefore(js, fjs); +} +$(document).ready(function($) { + if (document.documentElement.clientWidth < 1280) { + oneQt.extraLinksToMain(); + } + + $('#menuextras .search').click(function(e){ + e.preventDefault(); + $('.big_bar.account').slideUp(); + $('.big_bar.search').slideToggle(); + $('.big_bar_search').focus(); + $(this).toggleClass('open'); + }); + $('.cookies_yum').click(function() { + $('.cookies_yum').fadeOut(); + createCookie("cookies_nom", "yum", 180); + var cookie_added = 1; + }); + if (!(readCookie('cookies_nom') == 'yum')) { + $('.cookies_yum').fadeIn(); + } else { + var cookie_added = 1; + } + + Modernizr.load({test: Modernizr.input.placeholder, + nope: wpThemeFolder + '/js/placeholders.min.js'}); + + $('#navbar .navbar-toggle').click(function(e) { + e.preventDefault(); + if ($(this).hasClass('opened')) { + $(this).removeClass('opened'); + $('#navbar .navbar-menu').css('max-height', '0px'); + } + else { + $(this).addClass('opened'); + $('#navbar .navbar-menu').css('max-height', $('#navbar .navbar-menu ul').outerHeight() + 'px'); + } + }); + + $(window).resize(function() { + oneQt.stickySidebar(); + oneQt.footerPosition(); + if (document.documentElement.clientWidth < 1280) { + oneQt.extraLinksToMain(); + } else { + oneQt.mainLinkstoExtra(); + } + }); + + $(window).scroll(function() { + oneQt.stickySidebar(); + oneQt.stickyHeader(); + }); + + oneQt.stickySidebar(); + oneQt.footerPosition(); + oneQt.tabContents(); +}); + +$( window ).load(function() { + load_sdk('script', 'facebook-jssdk','//connect.facebook.net/en_US/sdk.js#xfbml=1&appId=207346529386114&version=v2.0'); + load_sdk('script', 'twitter-wjs', '//platform.twitter.com/widgets.js'); + $.getScript("//www.google.com/jsapi", function(){ + google.load("feeds", "1", {"callback": oneQt.liveFeeds}); + }); +}); + +var oneQt = { + stickySidebar: function() { + if ($('#sidebar').length && $('#sidebar').outerHeight() > 20) { + var $sidebar = $('#sidebar'); + var $win = $(window); + var $sidebarContainer = $sidebar.parent(); + var headerHeight = $('#navbar').outerHeight(); + if ($win.outerHeight() - headerHeight > $sidebar.innerHeight() && + $win.scrollTop() > $sidebarContainer.offset().top) { + var newTop = headerHeight + $win.scrollTop() - $sidebarContainer.offset().top; + if (newTop + $sidebar.innerHeight() > $sidebarContainer.innerHeight()) + newTop = $sidebarContainer.innerHeight() - $sidebar.innerHeight(); + + $sidebar.css({top: newTop +'px'}) + } + else { + $sidebar.css({top: '0'}) + } + } + }, + + footerPosition: function () { + $('#footerbar').removeClass('fixed'); + if (($('.hbspt-form').length > 0) || ($('#customerInfo').length > 0) || ($('.purchase_bar').length > 0)) { + var footerBottomPos = $('#footerbar').offset().top + $('#footerbar').outerHeight(); + if (footerBottomPos < $(window).height()) + $('#footerbar').addClass('fixed'); + } + }, + + stickyHeader: function () { + var originalHeaderHeight = 79; + if ($(window).scrollTop() > originalHeaderHeight) { + $('#navbar').addClass('fixed'); + $('#bottom_header').fadeOut(); + + if (!(cookie_added == 1)) { + $('.cookies_yum').fadeOut(); + createCookie("cookies_nom", "yum", 180); + var cookie_added = 1; + } + } + else { + $('#navbar').removeClass('fixed'); + $('#bottom_header').fadeIn(); + } + }, + + tabContents: function () { + $('.tab-container').each(function(i) { + var $el = $(this); + $el.find('.tab-titles li:eq(0)').addClass('active'); + $el.find('.tab-contents .tab:eq(0)').addClass('active'); + $el.find('.tab-titles a').click(function(e) { + e.preventDefault(); + var index = $(this).parent().index(); + $el.find('.tab-titles li').removeClass('active'); + $el.find('.tab-contents .tab').removeClass('active'); + $(this).parent().addClass('active'); + $el.find('.tab-contents .tab').eq(index).addClass('active'); + }) + }); + }, + + liveFeeds: function () { + $('.feed-container').each(function(i) { + var feedUrl = $(this).data('url'); + if (feedUrl != "") oneQt.blogFeed($(this), feedUrl); + }); + }, + + blogFeed: function ($container, feedUrl) { + var feed = new google.feeds.Feed(feedUrl); + feed.setNumEntries(3); + feed.load(function(result) { + $container.html(''); + if (!result.error) { + for (var i = 0; i < result.feed.entries.length; i++) { + var entry = result.feed.entries[i]; + var $article = $('
'); + $container.append($article); + var html = '
'; + html += ' '; + html += '
'; + html += '
'; + html += '

' + html += '

' + html += '

'; + html += '
    '; + html += '
'; + html += '
'; + $article.append(html); + $article.find('h4 a').text(result.feed.title); + $article.find('h3 a').text(entry.title); + $article.find('p a').text(entry.author); + try { + for (var j=0; j'); + $li.find('a').text(entry.categories[j]); + $article.find('.taglist').append($li); + } + } catch(e) {} + } + if (result.feed.link && result.feed.link != "") { + var linkHtml = 'Show all'; + $container.append(linkHtml); + } + } + }); + }, + + extraLinksToMain: function() { + var extramenuLinks = $('#menuextras').find('li'); + var mainmenu = $('#mainmenu'); + var count = 0; + if ($(extramenuLinks).length > 2) { + $(extramenuLinks).each(function() { + if (count < 3) { + var newLink = $(this); + $(newLink).addClass('dynamic-add'); + $(mainmenu).append(newLink); + } + count++; + }); + } + }, + + mainLinkstoExtra: function() { + var mainmenuLinks = $('#mainmenu').find('.dynamic-add'); + var extramenu = $('#menuextras'); + var count = 0; + $(mainmenuLinks).each(function() { + var newLink = $(this); + $(extramenu).prepend(newLink); + count++; + }); + } +} diff --git a/usr/share/fingerterm/Main.qml b/usr/share/fingerterm/Main.qml index a12e0afe..ab79cf18 100644 --- a/usr/share/fingerterm/Main.qml +++ b/usr/share/fingerterm/Main.qml @@ -119,10 +119,8 @@ Item { Keyboard { id: vkb - property bool visibleSetting: true - y: parent.height-vkb.height - visible: page.activeFocus && visibleSetting + visible: page.activeFocus && util.keyboardMode !== Util.KeyboardOff } // area that handles gestures/select/scroll modes and vkb-keypresses @@ -239,7 +237,7 @@ Item { property int duration property int cutAfter: height - height: parent.height + height: parent.height - (util.keyboardMode == Util.KeyboardFixed ? vkb.height : 0) width: parent.width fontPointSize: util.fontSize opacity: (util.keyboardMode == Util.KeyboardFade && vkb.active) ? 0.3 @@ -342,7 +340,7 @@ Item { function wakeVKB() { - if(!vkb.visibleSetting) + if (util.keyboardMode == Util.KeyboardOff) return; textrender.duration = window.fadeOutTime; @@ -366,7 +364,7 @@ Item { function updateVKB() { - if(!vkb.visibleSetting) + if (util.keyboardMode == Util.KeyboardOff) return; textrender.duration = 0; @@ -394,18 +392,11 @@ Item { function setTextRenderAttributes() { - var solidKeyboard = (util.keyboardMode === Util.KeyboardMove) - || (util.keyboardMode === Util.KeyboardFixed) + vkb.active |= (util.keyboardMode === Util.KeyboardFixed) - if (solidKeyboard) - { - vkb.active |= (util.keyboardMode === Util.KeyboardFixed); - vkb.visibleSetting = true; + if (util.keyboardMode === Util.KeyboardMove) { _applyKeyboardOffset() - } - else - { - vkb.visibleSetting = (util.keyboardMode === Util.KeyboardFade); + } else { textrender.y = 0; textrender.cutAfter = textrender.height; } diff --git a/usr/share/jolla-alarm-ui/jolla-alarm-ui.qml b/usr/share/jolla-alarm-ui/jolla-alarm-ui.qml index 72584df9..65240f5a 100644 --- a/usr/share/jolla-alarm-ui/jolla-alarm-ui.qml +++ b/usr/share/jolla-alarm-ui/jolla-alarm-ui.qml @@ -1,8 +1,8 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 -import org.nemomobile.alarms 1.0 +import Nemo.Alarms 1.0 import com.jolla.alarmui 1.0 -import org.nemomobile.dbus 2.0 as NemoDBus +import Nemo.DBus 2.0 as NemoDBus import "pages" ApplicationWindow { @@ -111,7 +111,9 @@ ApplicationWindow { notificationManager.publishMissedClockNotification(date, displayedAlarm.title, displayedAlarm.id, !!snoozed) } else if (displayedAlarm.type === Alarm.Calendar) { var occurrence = displayedAlarm.startDate - notificationManager.publishMissedCalendarNotification(occurrence, displayedAlarm.calendarEventUid, + notificationManager.publishMissedCalendarNotification(occurrence, + displayedAlarm.notebookUid, + displayedAlarm.calendarEventUid, displayedAlarm.calendarEventRecurrenceId, Qt.formatDateTime(occurrence, Qt.ISODate), displayedAlarm.title, displayedAlarm.id, !!snoozed) diff --git a/usr/share/jolla-alarm-ui/pages/AlarmDialogBase.qml b/usr/share/jolla-alarm-ui/pages/AlarmDialogBase.qml index cde9e7bb..e834d8f0 100644 --- a/usr/share/jolla-alarm-ui/pages/AlarmDialogBase.qml +++ b/usr/share/jolla-alarm-ui/pages/AlarmDialogBase.qml @@ -1,7 +1,8 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 -import org.nemomobile.ngf 1.0 -import org.nemomobile.alarms 1.0 +import Nemo.Ngf 1.0 +import Nemo.Alarms 1.0 +import Nemo.Configuration 1.0 import com.jolla.alarmui 1.0 SilicaFlickable { @@ -31,7 +32,9 @@ SilicaFlickable { alarmDialogBase.alarm = alarm status = AlarmDialogStatus.Open opacity = 1.0 - feedback.play() + if (alarm.type !== Alarm.Calendar || !doNotDisturb.value) { + feedback.play() + } timeoutTimer.start() } @@ -49,7 +52,7 @@ SilicaFlickable { if (fadeOut.running || opacity == 0) { return // Dialog is fading out or is already hidden } - fadeOut.start(); + fadeOut.start() } function hideImmediatedly() { @@ -65,6 +68,7 @@ SilicaFlickable { QtObject { id: dummy + property string title property date startDate property date endDate @@ -72,6 +76,7 @@ SilicaFlickable { property int hour property int minute property int second + property string notebookUid property string calendarEventUid property string calendarEventRecurrenceId property int type @@ -79,6 +84,7 @@ SilicaFlickable { PulleyAnimationHint { id: pulleyAnimationHint + anchors.fill: parent pushUpHint: true pullDownDistance: Theme.itemSizeLarge + (pushUpHint ? Theme.itemSizeExtraSmall : 0) @@ -86,17 +92,27 @@ SilicaFlickable { NonGraphicalFeedback { id: feedback + event: alarm.type === Alarm.Calendar ? "calendar" : "clock" } + ConfigurationValue { + id: doNotDisturb + + defaultValue: false + key: "/lipstick/do_not_disturb" + } + Image { id: topIcon + anchors.horizontalCenter: parent.horizontalCenter y: Theme.paddingLarge } Column { id: content + anchors { left: parent.left right: parent.right @@ -157,4 +173,3 @@ SilicaFlickable { ScriptAction { script: { dialogHiddenDelayer.start() } } } } - diff --git a/usr/share/jolla-alarm-ui/pages/CalendarAlarmDialog.qml b/usr/share/jolla-alarm-ui/pages/CalendarAlarmDialog.qml index 2dd8c818..9a26c8e1 100644 --- a/usr/share/jolla-alarm-ui/pages/CalendarAlarmDialog.qml +++ b/usr/share/jolla-alarm-ui/pages/CalendarAlarmDialog.qml @@ -1,7 +1,7 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 import com.jolla.alarmui 1.0 -import org.nemomobile.dbus 2.0 +import Nemo.DBus 2.0 AlarmDialogBase { id: root @@ -44,6 +44,7 @@ AlarmDialogBase { closeDialog(AlarmDialogStatus.Dismissed) var ok = calendar.typedCall("viewEvent", [ + { "type":"s", "value": alarm.notebookUid }, { "type":"s", "value": alarm.calendarEventUid }, { "type":"s", "value": alarm.calendarEventRecurrenceId }, { "type":"s", "value": Qt.formatDateTime(alarm.startDate, Qt.ISODate) } @@ -69,7 +70,9 @@ AlarmDialogBase { } horizontalAlignment: Text.AlignHCenter maximumLineCount: 4 - text: alarm.title + //: Fallback text on calendar alarm for events without a title + //% "(Unnamed event)" + text: alarm.title.trim() != "" ? alarm.title : qsTrId("jolla-alarm-la-untitled_calendar_event") wrapMode: Text.Wrap } diff --git a/usr/share/jolla-alarm-ui/pages/ClockAlarmDialog.qml b/usr/share/jolla-alarm-ui/pages/ClockAlarmDialog.qml index 1a54884e..d17d1848 100644 --- a/usr/share/jolla-alarm-ui/pages/ClockAlarmDialog.qml +++ b/usr/share/jolla-alarm-ui/pages/ClockAlarmDialog.qml @@ -1,7 +1,7 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 import com.jolla.alarmui 1.0 -import org.nemomobile.time 1.0 +import Nemo.Time 1.0 AlarmDialogBase { onTimeout: closeDialog(AlarmDialogStatus.Closed) diff --git a/usr/share/jolla-calculator/calculator.qml b/usr/share/jolla-calculator/calculator.qml new file mode 100644 index 00000000..1759687c --- /dev/null +++ b/usr/share/jolla-calculator/calculator.qml @@ -0,0 +1,57 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Sailfish.Calculator 1.0 +import "pages" + +ApplicationWindow { + id: calculator + + _defaultLabelFormat: Text.PlainText + + property Calculation activeCalculation + property Component calculationComponent: Component { Calculation {}} + property real squareWidth: Screen.width / (Screen.sizeCategory > Screen.Medium ? 7 : 5) + + allowedOrientations: defaultAllowedOrientations + _defaultPageOrientations: Orientation.All + + function formatResult(value) { + if (value === "nan") { + //% "NaN" + return qsTrId("calculator-la-not_a_number") + } + return value + } + + ListModel { + id: calculations + + function newCalculation() { + var calculation = calculationComponent.createObject(calculator) + insert(0, {"calculation": calculation}) + return calculation + } + + function clear() { + if (count > 0) { + while (count > 0) { + get(0).calculation.destroy() + remove(0) + } + Calculator.reset() + } + + activeCalculation = newCalculation() + } + + Component.onCompleted: clear() + } + + Connections { + target: activeCalculation + onCompleted: activeCalculation = calculations.newCalculation() + } + + initialPage: Component { CalculatorPage {} } + cover: Qt.resolvedUrl("cover/CalculatorCover.qml") +} diff --git a/usr/share/jolla-calculator/cover/CalculatorCover.qml b/usr/share/jolla-calculator/cover/CalculatorCover.qml new file mode 100644 index 00000000..095710ad --- /dev/null +++ b/usr/share/jolla-calculator/cover/CalculatorCover.qml @@ -0,0 +1,21 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import "../pages" + +CoverBackground { + OpacityRampEffect { + direction: OpacityRamp.RightToLeft + sourceItem: calculationsListView + offset: 0.5 + } + CalculationsListView { + id: calculationsListView + + coverMode: true + anchors { + fill: parent + rightMargin: Theme.paddingLarge + bottomMargin: Theme.paddingLarge + } + } +} diff --git a/usr/share/jolla-calculator/pages/AdvancedButton.qml b/usr/share/jolla-calculator/pages/AdvancedButton.qml new file mode 100644 index 00000000..d80c7f2d --- /dev/null +++ b/usr/share/jolla-calculator/pages/AdvancedButton.qml @@ -0,0 +1,6 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 + +CalculatorButton { + font.pixelSize: isLandscape ? Theme.fontSizeMedium : Theme.fontSizeExtraLarge +} diff --git a/usr/share/jolla-calculator/pages/CalculationsListView.qml b/usr/share/jolla-calculator/pages/CalculationsListView.qml new file mode 100644 index 00000000..0471ee02 --- /dev/null +++ b/usr/share/jolla-calculator/pages/CalculationsListView.qml @@ -0,0 +1,154 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Sailfish.Calculator 1.0 + +SilicaListView { + id: calculationsListView + + property bool coverMode + property Item focusEquation + property real layoutMultiplier: coverMode ? 0.5 : (pageStack.currentPage.isLandscape ? 0.75 : 1.0) + property int primaryFontSize: coverMode ? Theme.fontSizeMedium + : (pageStack.currentPage.isLandscape ? Theme.fontSizeLarge : Theme.fontSizeExtraLarge) + property int secondaryFontSize: coverMode ? Theme.fontSizeExtraSmall + : (pageStack.currentPage.isLandscape ? Theme.fontSizeMedium : Theme.fontSizeLarge) + + model: calculations + verticalLayoutDirection: ListView.BottomToTop + quickScroll: false + + delegate: ListItem { + id: listItem + contentHeight: equation.height + menu: Component { + ContextMenu { + MenuItem { + //% "Copy" + text: qsTrId("calculator-me-copy") + onClicked: Clipboard.text = calculation.result.valueText + } + } + } + Flickable { + id: equation + + width: calculationsListView.width + + // The implicit height of fraction delegate can grow over the design spec + // e.g. if user has chosen huge system-wide fonts in Display settings + height: Math.max(layoutMultiplier * 1.5 * squareWidth, fractionField.height + Theme.paddingSmall) + + flickableDirection: Flickable.HorizontalFlick + contentWidth: equationRow.width + Theme.paddingLarge + transform: Scale { origin.x: equation.width/2; xScale: -1} + boundsBehavior: Flickable.StopAtBounds + + Component.onCompleted: { + calculation.operationMadeToEmptyCalculation.connect(function() { + if (calculations.count > 1) { + calculation.focusField.link(calculations.get(1).calculation.result) + } + }) + } + + ListView.onAdd: AddAnimation { target: equation } + + Row { + id: equationRow + transform: Scale { origin.x: equationRow.width/2; xScale: -1} + anchors.verticalCenter: parent.verticalCenter + + function highlightItemAt(index) { + activeCalculation = calculation + activeCalculation.currentIndex = index + } + + Repeater { + model: calculation + Loader { + property bool activeItem: calculation == activeCalculation && calculation.currentIndex == index + + anchors.verticalCenter: parent ? parent.verticalCenter : undefined + sourceComponent: type == Calculation.Field ? fieldComponent + : (type == Calculation.Function ? functionComponent : operationComponent) + Component { + id: fieldComponent + FieldItem { + id: fieldItem + + focused: activeItem + linkText: field.linkText + fractionBar: field.fraction + numerator: field.numerator + denominator: field.denominator + coverMode: calculationsListView.coverMode + anchors.verticalCenter: parent.verticalCenter + + onClicked: equationRow.highlightItemAt(index) + + Binding { + // focus equation is the equation, which has the focused field + when: fieldItem.focused && !calculationsListView.coverMode + target: calculationsListView + property: "focusEquation" + value: equation + } + } + } + Component { + id: operationComponent + OperationItem { + coverMode: calculationsListView.coverMode + anchors.verticalCenter: parent.verticalCenter + text: model.text + } + } + Component { + id: functionComponent + FunctionItem { + coverMode: calculationsListView.coverMode + anchors.verticalCenter: parent.verticalCenter + text: model.text + } + } + } + } + + Row { + id: resultRow + + visible: calculation.result.valid + anchors.verticalCenter: parent.verticalCenter + OperationItem { + anchors.verticalCenter: parent.verticalCenter + text: "=" + coverMode: calculationsListView.coverMode + } + ResultItem { + anchors.verticalCenter: parent.verticalCenter + text: calculation.result.valueText + linkText: calculation.result.linkText + coverMode: calculationsListView.coverMode + + onClicked: { + if (activeCalculation != calculation) { + activeCalculation.focusField.link(calculation.result) + } + } + onPressAndHold: listItem.openMenu() + } + } + } + } + } + + FieldItem { + id: fractionField + visible: false + fractionBar: true + coverMode: calculationsListView.coverMode + numerator: "1" + denominator: "2" + } + VerticalScrollDecorator {} +} diff --git a/usr/share/jolla-calculator/pages/CalculatorButton.qml b/usr/share/jolla-calculator/pages/CalculatorButton.qml new file mode 100644 index 00000000..ccab1062 --- /dev/null +++ b/usr/share/jolla-calculator/pages/CalculatorButton.qml @@ -0,0 +1,36 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 + +BackgroundItem { + id: calculatorButton + + property string text + property alias font: label.font + property bool active: true + + implicitWidth: squareWidth + + highlighted: active && down + _showPress: highlighted + _pressEffectDelay: false + height: implicitWidth * (pageStack.currentPage.isLandscape ? 0.75 : 1.0) + width: implicitWidth + + onPressed: { + if (active && _feedbackEffect) { + _feedbackEffect.play() + } + } + onClicked: if (active && calculatorPanel) calculatorPanel.buttonClicked() + + Label { + id: label + font { + family: Theme.fontFamilyHeading + pixelSize: Theme.fontSizeExtraLarge + } + anchors.centerIn: parent + text: calculatorButton.text + color: highlighted ? Theme.highlightColor : Theme.primaryColor + } +} diff --git a/usr/share/jolla-calculator/pages/CalculatorPage.qml b/usr/share/jolla-calculator/pages/CalculatorPage.qml new file mode 100644 index 00000000..20a57264 --- /dev/null +++ b/usr/share/jolla-calculator/pages/CalculatorPage.qml @@ -0,0 +1,115 @@ +/* + * Copyright (c) 2013 - 2021 Jolla Ltd. + * + * License: Proprietary + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 + +Page { + id: calculatorPage + + property Item advancedPanel: calculatorPanel.advancedPanel + + MouseArea { + id: dragArea + anchors.fill: parent + drag { + target: advancedPanel + axis: Drag.YAxis + minimumY: -advancedPanel.maximumHeight + maximumY: 0 + filterChildren: true + } + + MouseArea { + enabled: calculatorPage.isPortrait && !advancedPanel.animating + anchors { + top: parent.top + bottom: calculatorPanel.top + bottomMargin: calculatorPage.isPortrait ? advancedPanel.height : 0 + right: parent.right + left: parent.left + } + Behavior on height { + enabled: advancedPanel.animating + NumberAnimation { easing.type: Easing.InOutQuad; duration: advancedPanel.animationDuration } + } + + CalculationsListView { + id: calculationsListView + anchors.fill: parent + clip: true + + // view autoscroll implementation + property real equationY + property real equationHeight + + // store the position of focused equation as delegates + // can get destroyed when moved outside the view port + function calculateAutoScrollPosition() { + if (focusEquation) { + equationY = contentItem.mapFromItem(focusEquation, 0, 0).y + equationHeight = focusEquation.height + } + } + + function autoScroll() { + var _equationY = mapFromItem(contentItem, 0, equationY).y + var scrollMargin = 0 + var animate = false + if (_equationY < scrollMargin) { + animate = true + autoScrollAnimation.to = Math.max(originY, contentY + _equationY - scrollMargin) + } else if (_equationY + equationHeight + scrollMargin > height) { + animate = true + autoScrollAnimation.to = Math.min(originY + contentHeight - height, + contentY + _equationY + equationHeight + scrollMargin - height) + } + if (animate && !moving) { + autoScrollAnimation.restart() + } + } + + onFocusEquationChanged: positionTimer.restart() + Component.onCompleted: positionTimer.restart() + onMovingChanged: if (moving) autoScrollAnimation.stop() + + Timer { + id: positionTimer + interval: 10 + onTriggered: parent.calculateAutoScrollPosition() + } + NumberAnimation { + id: autoScrollAnimation + easing.type: Easing.InOutQuad + target: calculationsListView + property: "contentY" + duration: 400 + } + } + } + + ScientificCalculatorHint { + id: hint + width: parent.width + anchors { + top: parent.top + bottom: calculatorPanel.top + bottomMargin: advancedPanel.height + } + } + + CalculatorPanel { + id: calculatorPanel + + onButtonClicked: calculationsListView.autoScroll() + onMenuClosed: positionTimer.restart() + onClear: calculations.clear() + + calculation: activeCalculation + anchors.bottom: parent.bottom + } + } +} diff --git a/usr/share/jolla-calculator/pages/CalculatorPanel.qml b/usr/share/jolla-calculator/pages/CalculatorPanel.qml new file mode 100644 index 00000000..cb8b4983 --- /dev/null +++ b/usr/share/jolla-calculator/pages/CalculatorPanel.qml @@ -0,0 +1,363 @@ +/* + * Copyright (c) 2013 - 2021 Jolla Ltd. + * + * License: Proprietary + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Sailfish.Calculator 1.0 + +PanelBackground { + id: calculatorPanel + + property Calculation calculation + property QtObject _feedbackEffect + property Item advancedPanel: advanced + + signal clear + signal buttonClicked + signal menuClosed + + Component.onCompleted: { + // avoid hard dependency to QtFeedback module + _feedbackEffect = Qt.createQmlObject("import QtQuick 2.0; import QtFeedback 5.0; ThemeEffect { effect: ThemeEffect.PressWeak }", + calculatorPanel, 'ThemeEffect'); + } + + width: parent.width + height: numericColumn.height + + states: State { + name: "advanced" + when: calculatorPage.isLandscape + + PropertyChanges { + target: calculatorPanel + height: numericColumn.height + } + PropertyChanges { + target: advanced + visible: true + width: squareWidth * 3 + height: parent.height + clip: false + open: false + dragging: false + } + AnchorChanges { + target: advanced + anchors.bottom: parent.bottom + } + AnchorChanges { + target: operations + anchors.right: parent.right + } + AnchorChanges { + target: numericColumn + anchors.left: undefined + anchors.horizontalCenter: middlePlaceholder.horizontalCenter + } + } + + Item { + id: advanced + + readonly property bool animating: showTimer.running || animation.running + readonly property alias animationDuration: animation.duration + readonly property alias maximumHeight: advancedFlow.implicitHeight + property bool dragging: dragArea.drag.active + property bool open: showTimer.running + property real lastY + property real lastYOnDirectionChange + + clip: dragging || animating + width: parent.width + height: -y + visible: dragging || open || animating + + onYChanged: { + // check direction and set open accordingly + if (dragging) { + if ((lastY > y && lastYOnDirectionChange < y) || (lastY < y && lastYOnDirectionChange > y)) { + lastYOnDirectionChange = y + } + if (Math.abs(lastYOnDirectionChange - y) >= dragArea.drag.threshold) { + open = lastYOnDirectionChange > y + } + lastY = y + } + } + onDraggingChanged: { + if (dragging) { + lastY = y + lastYOnDirectionChange = y + } else { + // animate to fully open or closed position after dragging + animate() + } + } + + Timer { + // Keeps this panel open for a little while when the app is started and hint is shown + id: showTimer + running: hint.active + interval: 3200 + onRunningChanged: if (running) running = true // break binding + onTriggered: { + advanced.open = false + advanced.animate() + } + } + + Binding { + target: advanced + property: "y" + value: -advanced.maximumHeight + when: showTimer.running + } + + ParallelAnimation { + id: animation + property real targetHeight + property int duration + + NumberAnimation { + target: advanced + property: "y" + easing.type: Easing.InOutQuad + duration: animation.duration + to: -animation.targetHeight + } + + NumberAnimation { + target: advanced + property: "height" + easing.type: Easing.InOutQuad + duration: animation.duration + to: animation.targetHeight + } + } + + Rectangle { + anchors.fill: parent + color: Theme.highlightColor + opacity: Theme.highlightBackgroundOpacity + } + + Flow { + id: advancedFlow + anchors.fill: parent + ScientificButton { + text: calculation.functionText(Calculation.Sine) + onClicked: calculation.sine() + } + ScientificButton { + text: calculation.functionText(Calculation.Cosine) + onClicked: calculation.cosine() + } + ScientificButton { + text: calculation.functionText(Calculation.Tangent) + onClicked: calculation.tangent() + } + ScientificButton { + text: calculation.functionText(Calculation.Logarithm) + onClicked: calculation.logarithm() + } + ScientificButton { + text: calculation.functionText(Calculation.LogarithmBase10) + onClicked: calculation.logarithmBase10() + } + ScientificButton { + text: calculation.functionText(Calculation.Factorial) + onClicked: calculation.factorial() + } + ScientificButton { + text: calculation.constantText(Calculation.Pi) + onClicked: calculation.setConstant(Calculation.Pi) + } + ScientificButton { + text: calculation.constantText(Calculation.E) + onClicked: calculation.setConstant(Calculation.E) + } + ScientificButton { + text: calculation.symbolText(Calculation.Power) + onClicked: calculation.power() + } + ScientificButton { + text: calculation.symbolText(Calculation.OpenBracket) + onClicked: calculation.openBracket() + } + ScientificButton { + text: calculation.symbolText(Calculation.CloseBracket) + onClicked: calculation.closeBracket() + } + ScientificButton { + text: calculation.functionText(Calculation.SquareRoot) + onClicked: calculation.squareRoot() + } + } + + function animate() { + animation.targetHeight = open ? maximumHeight : 0 + animation.duration = 150 * Math.abs(animation.targetHeight - height) / maximumHeight + animation.start() + } + } + + Image { + id: handleTopHalf + anchors { horizontalCenter: advanced.horizontalCenter; bottom: advanced.top } + visible: calculatorPage.isPortrait + source: "image://theme/graphic-edge-swipe-handle-top" + } + + Image { + anchors { horizontalCenter: advanced.horizontalCenter; top: advanced.top } + visible: calculatorPage.isPortrait + source: "image://theme/graphic-edge-swipe-handle-bottom" + } + + Item { + id: operations + + anchors.top: parent.top + anchors.right: centerPlaceholder.right + width: operationsColumn.width + height: parent.height + + Rectangle { + anchors.fill: parent + color: Theme.highlightColor + opacity: Theme.highlightBackgroundOpacity + } + + Column { + id: operationsColumn + anchors.horizontalCenter: parent.horizontalCenter + + Row { + AdvancedButton { + id: pasteKey + active: Clipboard.hasText + Image { + anchors { centerIn: parent; verticalCenterOffset: -Theme.paddingSmall } + opacity: pasteKey.active ? 1.0 : Theme.opacityLow + source: "image://theme/icon-m-clipboard?" + (pasteKey.highlighted ? Theme.highlightColor : Theme.primaryColor) + } + onClicked: active && calculation.paste() + } + AdvancedButton { + id: backspaceKey + Image { + anchors.centerIn: parent + source: "image://theme/icon-m-backspace?" + (backspaceKey.highlighted ? Theme.highlightColor : Theme.primaryColor) + } + onClicked: calculation.backspace() + } + } + Row { + OperationButton { + text: calculation.symbolText(Calculation.Divide) + onClicked: calculation.divide() + } + OperationButton { + text: calculation.symbolText(Calculation.Multiply) + onClicked: calculation.multiply() + } + } + Row { + OperationButton { + text: calculation.symbolText(Calculation.Add) + onClicked: calculation.add() + } + OperationButton { + text: calculation.symbolText(Calculation.Subtract) + onClicked: calculation.subtract() + } + } + Row { + OperationButton { + text: "C" + onClicked: calculatorPanel.clear() + } + OperationButton { + text: "=" + onClicked: calculation.calculate() + + Rectangle { + z: -1 + opacity: Theme.highlightBackgroundOpacity + anchors.fill: parent + color: Theme.highlightColor + } + } + } + } + } + + Item { + id: middlePlaceholder // empty space between two panels + height: 1 + anchors.left: advanced.right + anchors.right: operations.left + } + + Item { + id: centerPlaceholder // combination of numbers and basic operations centered + height: 1 + width: numericColumn.width + operations.width + anchors.horizontalCenter: parent.horizontalCenter + } + + + Column { + id: numericColumn + + anchors.top: parent.top + anchors.left: centerPlaceholder.left + + Row { + Repeater { + model: 3 + CalculatorButton { + text: (7 + index).toLocaleString() + onClicked: calculation.insert(text) + } + } + } + Row { + Repeater { + model: 3 + CalculatorButton { + text: (4 + index).toLocaleString() + onClicked: calculation.insert(text) + } + } + } + Row { + Repeater { + model: 3 + CalculatorButton { + text: (1 + index).toLocaleString() + onClicked: calculation.insert(text) + } + } + } + Row { + CalculatorButton { + text: (0).toLocaleString() + onClicked: calculation.insert(text) + } + CalculatorButton { + text: Qt.locale().decimalPoint + onClicked: calculation.insert(text) + } + CalculatorButton { + font.pixelSize: Theme.fontSizeLarge + text: "±" + onClicked: calculation.changeSign() + } + } + } +} diff --git a/usr/share/jolla-calculator/pages/FieldItem.qml b/usr/share/jolla-calculator/pages/FieldItem.qml new file mode 100644 index 00000000..767c6735 --- /dev/null +++ b/usr/share/jolla-calculator/pages/FieldItem.qml @@ -0,0 +1,69 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 + +Rectangle { + id: fieldItem + + property bool coverMode + property bool fractionBar + property bool focused + property alias linkText: linkLabel.text + property alias numerator: numeratorLabel.text + property alias denominator: denominatorLabel.text + property bool highlighted: focused || mouseArea.down + + signal clicked + + // cover content is tight, need negative padding to avoid overlap + height: content.height + 2 * (coverMode ? (fractionBar ? -0.2*Theme.paddingSmall : 0.3*Theme.paddingLarge) + : (fractionBar ? Theme.paddingSmall : Theme.paddingLarge)) + // square size until expanded with content + width: Math.max((numeratorLabel.height + 2 * (coverMode ? 0.3*Theme.paddingLarge : Theme.paddingLarge)), + Math.max(numeratorLabel.width, denominatorLabel.width) + 2*(coverMode ? Theme.paddingMedium + : Theme.paddingLarge)) + + color: Theme.rgba(highlighted ? Theme.highlightColor : Theme.primaryColor, + highlighted ? Theme.highlightBackgroundOpacity : 0.1) + MouseArea { + id: mouseArea + + property bool down: pressed && containsMouse + + anchors.fill: parent + onClicked: fieldItem.clicked() + } + + Column { + id: content + + width: parent.width + anchors.verticalCenter: parent.verticalCenter + Label { + id: numeratorLabel + color: mouseArea.down ? Theme.highlightColor : Theme.primaryColor + font.pixelSize: primaryFontSize + anchors.horizontalCenter: parent.horizontalCenter + } + Rectangle { + height: Math.round(Theme.paddingSmall/3) + visible: fractionBar + color: Theme.highlightColor + anchors { + left: parent.left + right: parent.right + margins: (coverMode ? 0.5 : 1.0) * Theme.paddingMedium + } + } + Label { + id: denominatorLabel + visible: fractionBar + color: mouseArea.down ? Theme.highlightColor : Theme.primaryColor + font.pixelSize: primaryFontSize + anchors.horizontalCenter: parent.horizontalCenter + } + } + LinkLabel { + id: linkLabel + coverMode: parent.coverMode + } +} diff --git a/usr/share/jolla-calculator/pages/FunctionItem.qml b/usr/share/jolla-calculator/pages/FunctionItem.qml new file mode 100644 index 00000000..eb8cdc15 --- /dev/null +++ b/usr/share/jolla-calculator/pages/FunctionItem.qml @@ -0,0 +1,19 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 + +Item { + id: functionItem + + property bool coverMode + property alias text: label.text + + height: label.height + 2 * Theme.paddingSmall + width: Math.max(layoutMultiplier/2 * squareWidth, label.width + 2*(coverMode ? Theme.paddingSmall : Theme.paddingMedium)) + Label { + id: label + color: Theme.highlightColor + font.pixelSize: secondaryFontSize + anchors.verticalCenter: parent.verticalCenter + x: parent.width - width + } +} diff --git a/usr/share/jolla-calculator/pages/LinkLabel.qml b/usr/share/jolla-calculator/pages/LinkLabel.qml new file mode 100644 index 00000000..51d051f9 --- /dev/null +++ b/usr/share/jolla-calculator/pages/LinkLabel.qml @@ -0,0 +1,16 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 + +Label { + property bool coverMode + font { + weight: Font.Bold + pixelSize: coverMode ? Theme.fontSizeTiny : Theme.fontSizeExtraSmall + } + anchors { + top: parent.top + right: parent.left + topMargin: -Theme.paddingSmall + rightMargin: Theme.paddingSmall + } +} diff --git a/usr/share/jolla-calculator/pages/OperationButton.qml b/usr/share/jolla-calculator/pages/OperationButton.qml new file mode 100644 index 00000000..160a1aff --- /dev/null +++ b/usr/share/jolla-calculator/pages/OperationButton.qml @@ -0,0 +1,6 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 + +AdvancedButton { + font.pixelSize: isLandscape ? Theme.fontSizeLarge : Theme.fontSizeExtraLarge +} diff --git a/usr/share/jolla-calculator/pages/OperationItem.qml b/usr/share/jolla-calculator/pages/OperationItem.qml new file mode 100644 index 00000000..61acacc9 --- /dev/null +++ b/usr/share/jolla-calculator/pages/OperationItem.qml @@ -0,0 +1,18 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 + +Item { + id: field + + property bool coverMode + property alias text: label.text + + height: label.height + 2 * Theme.paddingSmall + width: layoutMultiplier/2 * squareWidth + Label { + id: label + color: Theme.highlightColor + font.pixelSize: secondaryFontSize + anchors.centerIn: parent + } +} diff --git a/usr/share/jolla-calculator/pages/ResultItem.qml b/usr/share/jolla-calculator/pages/ResultItem.qml new file mode 100644 index 00000000..a69af90b --- /dev/null +++ b/usr/share/jolla-calculator/pages/ResultItem.qml @@ -0,0 +1,36 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 + +Rectangle { + id: resultItem + + property bool coverMode + property alias text: label.text + property alias linkText: linkLabel.text + signal clicked + signal pressAndHold + + height: label.height + 2 * Theme.paddingLarge * (coverMode ? 0.3 : 1.0) + width: Math.max(height, label.width + 2*(coverMode ? Theme.paddingMedium : Theme.paddingLarge)) + color: Theme.highlightBackgroundColor + + MouseArea { + id: mouseArea + + readonly property bool down: pressed && containsMouse + anchors.fill: parent + onClicked: resultItem.clicked() + onPressAndHold: resultItem.pressAndHold() + } + Label { + id: label + + anchors.centerIn: parent + color: mouseArea.down ? Theme.highlightColor : Theme.primaryColor + font.pixelSize: primaryFontSize + } + LinkLabel { + id: linkLabel + coverMode: parent.coverMode + } +} diff --git a/usr/share/jolla-calculator/pages/ScientificButton.qml b/usr/share/jolla-calculator/pages/ScientificButton.qml new file mode 100644 index 00000000..7ecd94d5 --- /dev/null +++ b/usr/share/jolla-calculator/pages/ScientificButton.qml @@ -0,0 +1,14 @@ +/* + * Copyright (c) 2021 Jolla Ltd. + * + * License: Proprietary + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 + +CalculatorButton { + font.pixelSize: Theme.fontSizeMedium + height: implicitWidth * 0.75 + width: pageStack.currentPage.isLandscape ? implicitWidth : implicitWidth * (5/6) +} diff --git a/usr/share/jolla-calculator/pages/ScientificCalculatorHint.qml b/usr/share/jolla-calculator/pages/ScientificCalculatorHint.qml new file mode 100644 index 00000000..4022f246 --- /dev/null +++ b/usr/share/jolla-calculator/pages/ScientificCalculatorHint.qml @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2016 - 2021 Jolla Ltd. + * + * License: Proprietary + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 + +Loader { + width: parent.width + active: counter.active + onActiveChanged: if (active) active = true // remove binding + sourceComponent: Component { + Item { + property bool pageActive: calculatorPage.status == PageStatus.Active && Qt.application.active + + onPageActiveChanged: { + if (pageActive) { + + // If the app is started in landscape no need to advertise + // scientific mode, user can find it without help + if (calculatorPage.isPortrait) { + timer.restart() + } + pageActive = true // delete binding + } + } + + anchors.fill: parent + InteractionHintLabel { + //% "Drag the panel open to enable scientific mode" + text: qsTrId("calculator-la-scientific_calculator_hint") + anchors.bottom: parent.bottom + opacity: timer.running ? 1.0 : 0.0 + Behavior on opacity { FadeAnimation { duration: 1000 } } + } + Timer { + id: timer + interval: 2400 + onTriggered: counter.increase() + } + } + } + FirstTimeUseCounter { + id: counter + limit: 1 + key: "/sailfish/calculator/scientific_calculator_hint_count" + } +} diff --git a/usr/share/jolla-calendar/DbusInvoker.qml b/usr/share/jolla-calendar/DbusInvoker.qml new file mode 100644 index 00000000..11bbaa31 --- /dev/null +++ b/usr/share/jolla-calendar/DbusInvoker.qml @@ -0,0 +1,83 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Sailfish.Calendar 1.0 +import Nemo.DBus 2.0 + +DBusAdaptor { + service: "com.jolla.calendar.ui" + path: "/com/jolla/calendar/ui" + iface: "com.jolla.calendar.ui" + + function viewEvent(notebookId, id, recurrenceId, startDate) { + viewEventByIdentifier(CalendarUtils.instanceId(notebookId, id, recurrenceId), startDate) + } + + function viewEventByIdentifier(id, startDate) { + var occurrence = CalendarUtils.parseTime(startDate) + if (isNaN(occurrence.getTime())) { + console.warn("Invalid event start date, unable to show event") + return + } + + if (pageStack.currentPage.objectName === "EventViewPage") { + pageStack.currentPage.instanceId = id + pageStack.currentPage.startTime = occurrence + } else { + pageStack.push("pages/EventViewPage.qml", + { instanceId: id, startTime: occurrence }, + PageStackAction.Immediate) + } + requestActive.start() + } + + function viewDate(dateTime) { + var parsedDate = new Date(dateTime) + if (isNaN(parsedDate.getTime())) { + console.warn("Invalid date, unable to show events for date") + return + } + + var page = pageStack.find(function(page) { + return page.objectName === "CalendarPage" + }) + if (page) { + page.gotoDate(parsedDate) + pageStack.pop(page, PageStackAction.Immediate) + } else { + console.warn("Cannot find CalendarPage in the stack") + } + requestActive.start() + } + + function importFile(fileName) { + if (pageStack.currentPage.objectName === "ImportPage") { + pageStack.currentPage.fileName = fileName + pageStack.currentPage.icsString = "" + } else { + pageStack.push("pages/ImportPage.qml", { "fileName": fileName }, PageStackAction.Immediate) + } + requestActive.start() + } + + function importIcsData(icsString) { + if (pageStack.currentPage.objectName === "ImportPage") { + pageStack.currentPage.icsString = icsString + pageStack.currentPage.fileName = "" + } else { + pageStack.push("pages/ImportPage.qml", { "icsString": icsString }, PageStackAction.Immediate) + } + requestActive.start() + } + + function openUrl(arguments) { + if (arguments.length === 0) { + app.activate() + } else { + importFile(arguments[0]) + } + } + + function activateWindow(arg) { + app.activate() + } +} diff --git a/usr/share/jolla-calendar/calendar.qml b/usr/share/jolla-calendar/calendar.qml new file mode 100644 index 00000000..5a1514a0 --- /dev/null +++ b/usr/share/jolla-calendar/calendar.qml @@ -0,0 +1,59 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import org.nemomobile.calendar 1.0 +import Nemo.DBus 2.0 +import Calendar.syncHelper 1.0 +import "pages" + +ApplicationWindow { + id: app + + initialPage: Component { CalendarPage { } } + cover: Qt.resolvedUrl("cover/CalendarCover.qml") + allowedOrientations: defaultAllowedOrientations + _defaultPageOrientations: Orientation.All + _defaultLabelFormat: Text.PlainText + + function showMainPage(operationType) { + var first = pageStack.currentPage + var temp = pageStack.previousPage(pageStack.currentPage) + while (temp) { + first = temp + temp = pageStack.previousPage(temp) + } + + pageStack.pop(first, operationType) + } + + function qsTrIdStrings() + { + //% "Show agenda" + QT_TRID_NOOP("calendar-me-show_agenda") + } + + DbusInvoker {} + + Timer { + id: requestActive + property int count: 0 + interval: 100 + repeat: true + triggeredOnStart: true + onTriggered: { + ++count + if (Qt.application.active || count >= 10) { + stop() + count = 0 + } else { + app.activate() + } + } + } + + property SyncHelper syncHelper: SyncHelper { } + Component.onCompleted: { + //TODO: enable FB sync on startup when delta sync is supported! JB#12118 + syncHelper.triggerUpdateImmediately() + } +} + diff --git a/usr/share/jolla-calendar/cover/CalendarCover.qml b/usr/share/jolla-calendar/cover/CalendarCover.qml new file mode 100644 index 00000000..73d2db72 --- /dev/null +++ b/usr/share/jolla-calendar/cover/CalendarCover.qml @@ -0,0 +1,148 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Nemo.Time 1.0 +import org.nemomobile.calendar 1.0 +import Sailfish.Calendar 1.0 + +CoverBackground { + Label { + //% "New event" + text: qsTrId("calendar-la-new_event") + x: Theme.paddingLarge + visible: !eventList.count + width: parent.width - 2*Theme.paddingLarge + color: Theme.secondaryColor + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + wrapMode: Text.Wrap + y: dateLabel.height + height: parent.height - y - coverActionArea.height + } + + CoverActionList { + CoverAction { + iconSource: "image://theme/icon-cover-new" + onTriggered: { + app.activate() + app.showMainPage(PageStackAction.Immediate) + pageStack.push("../pages/EventEditPage.qml", {}, PageStackAction.Immediate) + } + } + } + Column { + x: Theme.paddingLarge + spacing: Theme.paddingSmall + width: parent.width - 2*Theme.paddingLarge + anchors { + top: parent.top + bottom: coverActionArea.top + } + + DateLabel { + id: dateLabel + + day: Qt.formatDate(wallClock.time, "d") + weekDay: capitalize(Format.formatDate(wallClock.time, Formatter.WeekdayNameStandalone)) + month: capitalize(Format.formatDate(wallClock.time, Formatter.MonthNameStandalone)) + + function capitalize(string) { + return string.charAt(0).toUpperCase() + string.substr(1) + } + + WallClock { + id: wallClock + + // TODO: only update when Switcher is visible + enabled: !app.applicationActive + updateFrequency: WallClock.Day + onSystemTimeUpdated: { + eventUpdater.interval = 1000 + eventUpdater.update() + } + } + } + Item { + width: parent.width + Theme.paddingLarge + height: parent.height - dateLabel.height - parent.spacing + + ListModel { + id: activeAndComing + } + + Timer { + id: eventUpdater + + onTriggered: update() + + function update() { + activeAndComing.clear() + + var now = new Date + var nextEnding = undefined + + for (var i = 0; i < allEvents.count; ++i) { + var occurrence = allEvents.get(i, AgendaModel.OccurrenceObjectRole) + var event = allEvents.get(i, AgendaModel.EventObjectRole) + + if (event.allDay || now < occurrence.endTime) { + activeAndComing.append({ displayLabel: event.displayLabel, allDay: event.allDay, + startTime: occurrence.startTime, endTime: occurrence.endTime, + color: event.color, cancelled: event.status == CalendarEvent.StatusCancelled }) + + if (!event.allDay && (nextEnding == undefined || occurrence.endTime < nextEnding)) { + nextEnding = occurrence.endTime + } + } + } + + if (nextEnding !== undefined) { + var timeout = Math.max(0, nextEnding.getTime() - now.getTime() + 1000) + if (timeout > 0) { + eventUpdater.interval = timeout + eventUpdater.start() + } else { + eventUpdater.stop() + } + } else { + eventUpdater.stop() + } + } + } + + AgendaModel { + id: allEvents + startDate: wallClock.time + onUpdated: eventUpdater.update() + } + + ListView { + id: eventList + + property int eventHeight: (parent.height - Theme.paddingSmall - spacing)/2 + + clip: true + model: activeAndComing + interactive: false + width: parent.width + height: 2*eventHeight + spacing + spacing: Theme.paddingSmall + visible: count > 0 + + delegate: CoverEventItem { + eventName: CalendarTexts.ensureEventTitle(model.displayLabel) + allDay: model.allDay + startTime: model.startTime + endtime: model.endTime + activeDay: wallClock.time + color: model.color + height: eventList.eventHeight + cancelled: model.cancelled + } + } + OpacityRampEffect { + offset: 0.5 + sourceItem: eventList + } + } + } +} diff --git a/usr/share/jolla-calendar/cover/CoverEventItem.qml b/usr/share/jolla-calendar/cover/CoverEventItem.qml new file mode 100644 index 00000000..cf4c9459 --- /dev/null +++ b/usr/share/jolla-calendar/cover/CoverEventItem.qml @@ -0,0 +1,46 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Sailfish.Calendar 1.0 + +Row { + id: root + + property alias eventName: nameLabel.text + property alias allDay: timeLabel.allDay + property alias startTime: timeLabel.startTime + property alias endtime: timeLabel.endTime + property alias activeDay: timeLabel.activeDay + property alias color: rectangle.color + property alias cancelled: timeLabel.font.strikeout + + spacing: Theme.paddingSmall + + Rectangle { + id: rectangle + + radius: Theme.paddingSmall/3 + width: Theme.paddingSmall + height: parent.height - Theme.paddingMedium - Theme.paddingSmall/2 + anchors.verticalCenter: parent.verticalCenter + } + Column { + id: labelColumn + spacing: -Theme.paddingSmall + anchors.verticalCenter: parent.verticalCenter + EventTimeLabel { + id: timeLabel + opacity: Theme.opacityHigh + font.pixelSize: Theme.fontSizeSmall + verticalAlignment: Text.AlignVCenter + fontSizeMode: Text.VerticalFit + height: root.height/2 + } + Label { + id: nameLabel + font.pixelSize: Theme.fontSizeSmall + verticalAlignment: Text.AlignVCenter + fontSizeMode: Text.VerticalFit + height: root.height/2 + } + } +} diff --git a/usr/share/jolla-calendar/cover/DateLabel.qml b/usr/share/jolla-calendar/cover/DateLabel.qml new file mode 100644 index 00000000..179e682e --- /dev/null +++ b/usr/share/jolla-calendar/cover/DateLabel.qml @@ -0,0 +1,48 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 + +Item { + property alias day: dayLabel.text + property alias weekDay: weekDayLabel.text + property alias month: monthLabel.text + + width: parent.width + height: dayLabel.height + dayLabel.y + Label { + id: weekDayLabel + + width: parent.width + color: Theme.highlightColor + font.pixelSize: Theme.fontSizeSmall + truncationMode: TruncationMode.Fade + anchors { + left: parent.left + right: dayLabel.left + bottom: monthLabel.top + bottomMargin: -Theme.paddingSmall + } + } + Label { + id: monthLabel + + width: parent.width + font.pixelSize: Theme.fontSizeExtraSmall + truncationMode: TruncationMode.Fade + color: Theme.secondaryHighlightColor + anchors { + left: parent.left + right: dayLabel.left + baseline: dayLabel.baseline + } + } + Label { + id: dayLabel + y: Theme.paddingMedium + font { + pixelSize: Theme.fontSizeHuge + family: Theme.fontFamilyHeading + } + anchors.right: parent.right + } +} + diff --git a/usr/share/jolla-calendar/pages/AgendaPage.qml b/usr/share/jolla-calendar/pages/AgendaPage.qml new file mode 100644 index 00000000..22c40e6f --- /dev/null +++ b/usr/share/jolla-calendar/pages/AgendaPage.qml @@ -0,0 +1,48 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import org.nemomobile.calendar 1.0 +import "Util.js" as Util + +Page { + id: root + + property date date + + SilicaListView { + anchors.fill: parent + header: Item { + height: pageHeader.height + Theme.paddingLarge + width: parent.width + + PageHeader { + id: pageHeader + title: Util.capitalize(Format.formatDate(root.date, Formatter.WeekdayNameStandalone)) + } + Text { + y: Theme.itemSizeSmall + anchors { + right: parent.right + rightMargin: Theme.horizontalPageMargin + } + color: Theme.secondaryHighlightColor + font.pixelSize: Theme.fontSizeMedium + text: Format.formatDate(root.date, Formatter.DateLong) + } + } + + model: AgendaModel { + startDate: root.date + endDate: QtDate.addDays(root.date, 7) + } + + delegate: DeletableListDelegate {} + + section { + property: "sectionBucket" + delegate: EventListSectionDelegate { + onClicked: pageStack.animatorPush("DayPage.qml", {defaultDate: section}) + } + } + VerticalScrollDecorator {} + } +} diff --git a/usr/share/jolla-calendar/pages/AttendeeListItem.qml b/usr/share/jolla-calendar/pages/AttendeeListItem.qml new file mode 100644 index 00000000..e990e6d2 --- /dev/null +++ b/usr/share/jolla-calendar/pages/AttendeeListItem.qml @@ -0,0 +1,66 @@ +import QtQuick 2.6 +import Sailfish.Silica 1.0 +import org.nemomobile.calendar 1.0 + +ListItem { + id: root + + property bool required + property string name + property string email + + signal removed() + signal moved() + + menu: attendeeMenuComponent + contentHeight: Math.max(labels.height, removeButton.height) + 2*Theme.paddingSmall + + Column { + id: labels + anchors { + verticalCenter: parent.verticalCenter + left: parent.left + leftMargin: Theme.horizontalPageMargin + right: removeButton.left + rightMargin: Theme.paddingMedium + } + Label { + text: name.length > 0 ? name : email + truncationMode: TruncationMode.Fade + width: parent.width + font.pixelSize: Theme.fontSizeMedium + color: root.highlighted ? Theme.highlightColor : Theme.primaryColor + } + Label { + text: (name.length > 0 && name != email) ? email : "" + truncationMode: TruncationMode.Fade + width: parent.width + font.pixelSize: Theme.fontSizeTiny + color: root.highlighted ? Theme.secondaryHighlightColor : Theme.secondaryColor + visible: text != "" + } + } + + IconButton { + id: removeButton + anchors.right: parent.right + anchors.rightMargin: Theme.horizontalPageMargin + anchors.verticalCenter: parent.verticalCenter + icon.source: "image://theme/icon-m-clear" + highlighted: root.highlighted || down + onClicked: root.removed() + } + + Component { + id: attendeeMenuComponent + ContextMenu { + MenuItem { + text: root.required ? //% "Move to optional" + qsTrId("calendar-move_to_optional") + : //% "Move to invited" + qsTrId("calendar-move_to_invited") + onClicked: root.moved() + } + } + } +} diff --git a/usr/share/jolla-calendar/pages/AttendeeSelectionPage.qml b/usr/share/jolla-calendar/pages/AttendeeSelectionPage.qml new file mode 100644 index 00000000..e708219b --- /dev/null +++ b/usr/share/jolla-calendar/pages/AttendeeSelectionPage.qml @@ -0,0 +1,234 @@ +import QtQuick 2.6 +import Sailfish.Silica 1.0 +import org.nemomobile.contacts 1.0 as Contacts // avoid namespace clashes +import Sailfish.Contacts 1.0 as SailfishContacts +import org.nemomobile.calendar 1.0 + +Page { + id: root + + property QtObject requiredAttendees + property QtObject optionalAttendees + + signal modified() + + onStatusChanged: { + if (status === PageStatus.Active && requiredAttendees.count == 0 && optionalAttendees.count == 0) { + searchField.forceActiveFocus() + } + } + + function removeAttendee(required, index) { + if (required) { + requiredAttendees.remove(index) + } else { + optionalAttendees.remove(index) + } + + modified() + } + + function changeAttendeeParticipation(fromRequired, index) { + var name + var email + + if (fromRequired) { + name = requiredAttendees.name(index) + email = requiredAttendees.email(index) + requiredAttendees.remove(index) + optionalAttendees.prepend(name, email) + } else { + name = optionalAttendees.name(index) + email = optionalAttendees.email(index) + optionalAttendees.remove(index) + requiredAttendees.prepend(name, email) + } + + modified() + } + + function addRequiredAttendee(name, email) { + if (requiredAttendees.hasEmail(email) || optionalAttendees.hasEmail(email)) { + console.log("skipping duplicate email", email) + return + } + requiredAttendees.prepend(name, email) + modified() + } + + Contacts.PeopleModel { + id: contactSearchModel + + filterPattern: searchField.text + filterType: filterPattern == "" ? Contacts.PeopleModel.FilterNone : Contacts.PeopleModel.FilterAll + requiredProperty: Contacts.PeopleModel.EmailAddressRequired + } + + ContactListItem { + id: dummyContact + property string displayLabel: "X" + property var emailDetails: [] + visible: false + } + + AttendeeListItem { + id: dummyAttendee + name: "X" + email: "X" + visible: false + } + + SilicaFlickable { + anchors.fill: parent + contentHeight: column.height + + Column { + id: column + width: parent.width + bottomPadding: Theme.paddingLarge + + PageHeader { + //% "Invite People" + title: qsTrId("calendar-ph-invite_people") + } + + Item { + height: searchField.height + width: parent.width + + TextField { + id: searchField + + width: addButton.x + textRightMargin: Theme.paddingLarge + inputMethodHints: Qt.ImhNoAutoUppercase + EnterKey.enabled: text.length > 0 + EnterKey.onClicked: { + // just a rough check here + if (text.indexOf("@") > 0 && text.indexOf(".") > 0) { + addRequiredAttendee("", text) + text = "" + } else { + invalidEmailTimer.restart() + } + } + + //% "Search" + placeholderText: qsTrId("calendar-search_contact") + label: invalidEmailTimer.running + ? //% "Invalid email address" + qsTrId("calendar-invalid_email_address") + : placeholderText + + Timer { + id: invalidEmailTimer + interval: 2000 + } + } + + IconButton { + id: addButton + anchors.verticalCenter: searchField.Center + anchors.right: parent.right + anchors.rightMargin: Theme.horizontalPageMargin + icon.source: "image://theme/icon-m-add" + onClicked: { + var pickerPage = pageStack.push("Sailfish.Contacts.ContactsMultiSelectDialog", + {"requiredProperty": Contacts.PeopleModel.EmailAddressRequired}) + pickerPage.accepted.connect(function() { addContacts(pickerPage.selectedContacts) }) + } + + function addContacts(contacts) { + for (var i = 0; i < contacts.count; i++) { + var contact = contactSearchModel.personById(contacts.get(i), SailfishContacts.ContactSelectionModel.ContactIdRole) + var property = contacts.get(i, SailfishContacts.ContactSelectionModel.PropertyRole) + addRequiredAttendee(contact.displayLabel, property.address) + } + } + } + } + + ColumnView { + id: filteredContacts + + itemHeight: dummyContact.height + model: contactSearchModel + delegate: ContactListItem { + searchText: searchField.text + openMenuOnPressAndHold: false + onClicked: handleMenu(true) + onPressAndHold: handleMenu(false) + menu: Component { + ContextMenu { + Repeater { + model: emailsModel + MenuItem { + text: email + onClicked: { + addRequiredAttendee(name, email) + searchField.text = "" + searchField.forceActiveFocus() + } + } + } + } + } + function handleMenu(click) { + var emails = Contacts.Person.removeDuplicateEmailAddresses(emailDetails) + + if (emails.length > 1) { + emailsModel.clear() + for (var i=0; i < emails.length; ++i) { + emailsModel.append({"name": displayLabel, "email": emails[i].address}) + } + openMenu() + } else if (click && emails.length == 1) { + addRequiredAttendee(displayLabel, emails[0].address) + searchField.text = "" + searchField.forceActiveFocus() + } + } + } + + ListModel { + id: emailsModel + } + } + + SectionHeader { + visible: requiredAttendees.count > 0 + //% "Invited" + text: qsTrId("calendar-invited_attendee") + } + + ColumnView { + model: requiredAttendees + itemHeight: dummyAttendee.height + delegate: AttendeeListItem { + required: true + name: model.name + email: model.email + onRemoved: root.removeAttendee(true, index) + onMoved: changeAttendeeParticipation(true, index) + } + } + + SectionHeader { + visible: optionalAttendees.count > 0 + //% "Optional" + text: qsTrId("calendar-optional_attendee") + } + + ColumnView { + model: optionalAttendees + itemHeight: dummyAttendee.height + delegate: AttendeeListItem { + name: model.name + email: model.email + onRemoved: root.removeAttendee(false, index) + onMoved: changeAttendeeParticipation(false, index) + } + } + } + } +} diff --git a/usr/share/jolla-calendar/pages/CalendarPage.qml b/usr/share/jolla-calendar/pages/CalendarPage.qml new file mode 100644 index 00000000..ed1198d6 --- /dev/null +++ b/usr/share/jolla-calendar/pages/CalendarPage.qml @@ -0,0 +1,135 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 + +Page { + id: root + + objectName: "CalendarPage" // Used in DbusInvoker.qml + + function addEvent() { + var now = new Date + var d = tabHeader.date + + if (now.getHours() < 23 && now.getMinutes() > 0) { + d.setHours(now.getHours() + 1) + } + + d.setMinutes(0) + d.setSeconds(0) + + pageStack.animatorPush("EventEditPage.qml", { defaultDate: d }) + } + + function gotoDate(date) { + if (view.item) { + view.item.gotoDate(date) + } + } + + TabHeader { + id: tabHeader + width: parent ? parent.width : root.width + title: view.item ? view.item.title : "" + description: view.item ? view.item.description : "" + date: view.item ? view.item.date : new Date + model: ListModel { + ListElement { + icon: "image://theme/icon-m-month-view" + view: "MonthView.qml" + } + ListElement { + icon: "image://theme/icon-m-week-view" + view: "WeekView.qml" + } + ListElement { + icon: "image://theme/icon-m-day-view" + view: "DayView.qml" + } + } + } + + Item { + id: view + property Item item + property string source: tabHeader.currentView + property var _cache + + anchors.fill: parent + + onSourceChanged: { + if (_cache === undefined) { + // Cannot assign ': []' to _cache otherwise the assignation + // may run after the source changed signal on initialisation + _cache = [] + } + var currentDate = tabHeader.date + if (item) { + item.visible = false + item.detachHeader() + } + if (source in _cache) { + item = _cache[source] + } else { + var component = Qt.createComponent(source) + if (component.status == Component.Error) console.warn(component.errorString()) + item = component.createObject(view, {}) + item.anchors.fill = view + _cache[source] = item + } + item.gotoDate(currentDate) + item.attachHeader(tabHeader) + item.visible = true + } + + Binding { + target: pullDownMenu + property: "flickable" + value: view.item.flickable + } + } + + PullDownMenu { + id: pullDownMenu + busy: syncHelper.synchronizing + + MenuItem { + //% "Sync" + text: qsTrId("calendar-me-sync") + onClicked: app.syncHelper.triggerRefresh() + } + MenuItem { + //% "Settings" + text: qsTrId("calendar-me-settings") + onClicked: pageStack.animatorPush("SettingsPage.qml") + } + MenuItem { + //% "Search" + text: qsTrId("calendar-me-search") + onClicked: pageStack.animatorPush("SearchPage.qml") + } + MenuItem { + //% "Go to today" + text: qsTrId("calendar-me-go_to_today") + onClicked: root.gotoDate(new Date) + } + /* Disabled for now + MenuItem { + //% "Show agenda" + text: qsTrId("calendar-me-show_agenda") + onClicked: pageStack.animatorPush("AgendaPage.qml", {date: datePicker.date}) + } + */ + MenuItem { + //% "New event" + text: qsTrId("calendar-me-new_event") + onClicked: root.addEvent() + } + + _inactivePosition: flickable.pullDownMenuOrigin !== undefined + ? flickable.pullDownMenuOrigin + : Math.round(flickable.originY - (_inactiveHeight + spacing)) + y: flickable.pullDownMenuOrigin !== undefined + ? Math.max(flickable.contentY, flickable.pullDownMenuOrigin) - height + : flickable.originY - height + } +} diff --git a/usr/share/jolla-calendar/pages/CalendarPicker.qml b/usr/share/jolla-calendar/pages/CalendarPicker.qml new file mode 100644 index 00000000..f5570107 --- /dev/null +++ b/usr/share/jolla-calendar/pages/CalendarPicker.qml @@ -0,0 +1,75 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import org.nemomobile.calendar 1.0 +import Calendar.sortFilterModel 1.0 +import Sailfish.Calendar 1.0 + +Page { + id: root + + signal calendarClicked(string uid) + property string selectedCalendarUid + property bool hideExcludedCalendars + + SilicaFlickable { + anchors.fill: parent + contentHeight: column.height + + Column { + id: column + + width: parent.width + + PageHeader { + //: Header for page where calendar is selected for a event + //% "Select calendar" + title: qsTrId("calendar-he_select_calendar") + } + + Repeater { + model: SortFilterModel { + model: NotebookModel { } + filterRole: "readOnly" + filterRegExp: /false/ + sortRole: "name" + } + + delegate: BackgroundItem { + height: Math.max(calendarDelegate.height + 2*Theme.paddingSmall, Theme.itemSizeMedium) + onClicked: root.calendarClicked(model.uid) + visible: !model.excluded || !hideExcludedCalendars + + CalendarSelectorDelegate { + id: calendarDelegate + accountIcon: model.accountIcon + calendarName: localCalendar ? CalendarTexts.getLocalCalendarName() : model.name + calendarDescription: model.description + selected: root.selectedCalendarUid === model.uid + width: calendarColor.x - 2*Theme.paddingLarge + + anchors { + left: parent.left + verticalCenter: parent.verticalCenter + margins: Theme.paddingLarge + } + } + + Rectangle { + id: calendarColor + + anchors { + right: parent.right + rightMargin: Theme.paddingLarge + verticalCenter: parent.verticalCenter + } + color: model.color + height: Theme.itemSizeExtraSmall + radius: Math.round(width / 3) + width: Theme.paddingSmall + } + } + } + } + VerticalScrollDecorator {} + } +} diff --git a/usr/share/jolla-calendar/pages/CalendarSelectorDelegate.qml b/usr/share/jolla-calendar/pages/CalendarSelectorDelegate.qml new file mode 100644 index 00000000..973397ca --- /dev/null +++ b/usr/share/jolla-calendar/pages/CalendarSelectorDelegate.qml @@ -0,0 +1,54 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 + +Column { + // client code must set width explicitly or with anchors + id: root + + // squeezing content a bit. division just good enough approximation. + spacing: Math.round(-calendarNameLabel.height / 7) + + property alias accountIcon: calendarAccountIcon.source + property alias calendarName: calendarNameLabel.text + property alias calendarDescription: calendarDescriptionLabel.text + property bool selected + + Item { + width: parent.width + height: Math.max(calendarAccountIcon.height, calendarNameLabel.height) + Image { + id: calendarAccountIcon + anchors.verticalCenter: parent.verticalCenter + height: Theme.iconSizeSmall + width: visible ? Theme.iconSizeSmall : 0 + visible: source != "" + } + Label { + id: calendarNameLabel + anchors { + left: calendarAccountIcon.right + leftMargin: calendarAccountIcon.visible ? Theme.paddingMedium : 0 + verticalCenter: parent.verticalCenter + right: parent.right + rightMargin: Theme.paddingMedium + } + truncationMode: TruncationMode.Fade + font.pixelSize: Theme.fontSizeLarge + maximumLineCount: 1 + color: selected ? Theme.highlightColor + : (highlighted ? Theme.highlightColor : Theme.primaryColor) + } + } + Label { + id: calendarDescriptionLabel + anchors { + left: parent.left + right: parent.right + rightMargin: Theme.paddingMedium + } + truncationMode: TruncationMode.Fade + maximumLineCount: 3 + color: (selected || highlighted) ? Theme.secondaryHighlightColor : Theme.secondaryColor + visible: text.length > 0 + } +} diff --git a/usr/share/jolla-calendar/pages/CalendarYearPage.qml b/usr/share/jolla-calendar/pages/CalendarYearPage.qml new file mode 100644 index 00000000..a87de539 --- /dev/null +++ b/usr/share/jolla-calendar/pages/CalendarYearPage.qml @@ -0,0 +1,41 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 + +Page { + id: root + + property int startYear: 1980 + property int endYear: 2300 + property int defaultYear: 2100 + + signal yearSelected(int year) + + SilicaListView { + id: view + anchors.fill: parent + model: root.endYear - root.startYear + delegate: BackgroundItem { + width: parent.width + height: dateText.height + Label { + id: dateText + width: parent.width + horizontalAlignment: Text.AlignHCenter + text: index + root.startYear + color: index == view.currentIndex || highlighted ? Theme.highlightColor : Theme.primaryColor + font.pixelSize: Theme.fontSizeHuge + } + onClicked: { + view.currentIndex = index + root.yearSelected(index + root.startYear) + pageStack.pop() + } + } + } + + Component.onCompleted: { + var index = defaultYear - startYear + view.positionViewAtIndex(index, ListView.Center) + view.currentIndex = index + } +} diff --git a/usr/share/jolla-calendar/pages/ChangeMonthHint.qml b/usr/share/jolla-calendar/pages/ChangeMonthHint.qml new file mode 100644 index 00000000..5f1a06e9 --- /dev/null +++ b/usr/share/jolla-calendar/pages/ChangeMonthHint.qml @@ -0,0 +1,47 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 + +Loader { + anchors.fill: parent + active: counter.active + sourceComponent: Component { + Item { + property bool pageActive: root.status == PageStatus.Active + onPageActiveChanged: { + if (pageActive) { + timer.restart() + counter.increase() + pageActive = false + } + } + + anchors.fill: parent + Timer { + id: timer + interval: 500 + onTriggered: touchInteractionHint.restart() + } + + InteractionHintLabel { + //: Swipe here to change month + //% "Swipe here to change month" + text: qsTrId("calendar-la-change_month_hint") + anchors.bottom: parent.bottom + opacity: touchInteractionHint.running ? 1.0 : 0.0 + Behavior on opacity { FadeAnimation { duration: 1000 } } + } + TouchInteractionHint { + id: touchInteractionHint + + direction: TouchInteraction.Right + anchors.verticalCenter: parent.verticalCenter + } + } + } + FirstTimeUseCounter { + id: counter + limit: 3 + defaultValue: 1 // display hint twice for existing users + key: "/sailfish/calendar/change_month_hint_count" + } +} diff --git a/usr/share/jolla-calendar/pages/ContactListItem.qml b/usr/share/jolla-calendar/pages/ContactListItem.qml new file mode 100644 index 00000000..3fa3316f --- /dev/null +++ b/usr/share/jolla-calendar/pages/ContactListItem.qml @@ -0,0 +1,58 @@ +import QtQuick 2.6 +import Sailfish.Silica 1.0 +import org.nemomobile.contacts 1.0 as Contacts + +ListItem { + id: root + + property string searchText + + contentHeight: content.height + 2*Theme.paddingSmall + + function getEmailText(emailDetails) { + if (!emailDetails || emailDetails.length === 0) { + return "" + } + + emailDetails = Contacts.Person.removeDuplicateEmailAddresses(emailDetails) + if (emailDetails.length > 1) { + var addressString = emailDetails[0].address + for (var i = 0; i < emailDetails.length; ++i) { + if (emailDetails[i].address.indexOf(searchText.toLocaleLowerCase()) > -1) { + addressString = Theme.highlightText(emailDetails[i].address, searchText, Theme.highlightColor) + break + } + } + + //: %1 replaced with best match email and %n tells how many other addresses this contact has + //% "%1 + %n other" + return qsTrId("calendar-other_click_to_select", emailDetails.length - 1).arg(addressString) + } else { + return Theme.highlightText(emailDetails[0].address, searchText, Theme.highlightColor) + } + } + + Column { + id: content + + x: Theme.horizontalPageMargin + width: parent.width - 2*x + anchors.verticalCenter: parent.verticalCenter + + Label { + width: parent.width + text: Theme.highlightText(displayLabel, searchText, Theme.highlightColor) + textFormat: Text.StyledText + truncationMode: TruncationMode.Fade + color: root.highlighted ? Theme.highlightColor : Theme.primaryColor + } + Label { + width: parent.width + text: getEmailText(emailDetails) + font.pixelSize: Theme.fontSizeTiny + textFormat: Text.StyledText + truncationMode: TruncationMode.Fade + color: root.highlighted ? Theme.secondaryHighlightColor : Theme.secondaryColor + } + } +} diff --git a/usr/share/jolla-calendar/pages/DatePickerPanel.qml b/usr/share/jolla-calendar/pages/DatePickerPanel.qml new file mode 100644 index 00000000..9cba2299 --- /dev/null +++ b/usr/share/jolla-calendar/pages/DatePickerPanel.qml @@ -0,0 +1,135 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import org.nemomobile.calendar 1.0 +import Nemo.Time 1.0 +import "Util.js" as Util + +Item { + property alias date: datePicker.date + property alias viewMoving: datePicker.viewMoving + property string dstIndication: dstIndicator.visible + && dstIndicator.transitionDay == date.getDate() + ? dstIndicator.transitionDescription : "" + + property bool _largeScreen: Screen.sizeCategory > Screen.Medium + + width: parent.width + height: datePicker.height + Theme.horizontalPageMargin + + WallClock { + id: wallClock + updateFrequency: WallClock.Day + } + + DatePicker { + id: datePicker + + property int indicatorWidth: 1.5 * Theme.paddingSmall + property int indicatorSpacing: Theme.paddingSmall + property int maxIndicatorCount: Math.min(5, (datePicker.cellWidth + indicatorSpacing - 2*Theme.paddingSmall) / (indicatorWidth + indicatorSpacing)) + + anchors { + top: parent.top + topMargin: Theme.horizontalPageMargin + } + + leftMargin: _largeScreen ? Theme.horizontalPageMargin : 0 + rightMargin: 0 + + width: _largeScreen && isPortrait ? parent.width*0.6 : parent.width + cellHeight: isPortrait ? cellWidth + : Math.min(cellWidth, ((Screen.width - dayRowHeight - 2*anchors.topMargin) / 6)) + daysVisible: true + monthYearVisible: !_largeScreen + delegate: Component { + MouseArea { + id: mouseArea + // noon time to protect against timezone screw ups + property date modelDate: new Date(model.year, model.month-1, model.day, 12, 0) + + width: datePicker.cellWidth + height: datePicker.cellHeight + + AgendaModel { + id: events + filterMode: AgendaModel.FilterMultipleEventsPerNotebook + } + + Binding { + target: events + property: "startDate" + value: modelDate + when: !datePicker.viewMoving + } + + Text { + id: label + anchors.centerIn: parent + text: model.day.toLocaleString() + font.pixelSize: Theme.fontSizeMedium + font.bold: model.day === wallClock.time.getDate() + && model.month === wallClock.time.getMonth()+1 + && model.year === wallClock.time.getFullYear() + color: { + if (model.day === datePicker.day && + model.month === datePicker.month && + model.year === datePicker.year) { + return Theme.highlightColor + } else if (label.font.bold) { + return Theme.highlightColor + } else if (model.month === model.primaryMonth) { + return Theme.primaryColor + } + return Theme.secondaryColor + } + } + + Row { + spacing: datePicker.indicatorSpacing + anchors { + top: label.baseline + topMargin: Theme.paddingMedium + horizontalCenter: parent.horizontalCenter + } + + Repeater { + model: events + Rectangle { + width: datePicker.indicatorWidth + height: width + radius: width/2 + color: model.event.color + visible: model.index < datePicker.maxIndicatorCount + } + } + } + + // TODO: How are we meant to switch to day view? + onClicked: datePicker.date = modelDate + + Binding { + when: dstIndicator.transitionDay == model.day + && dstIndicator.transitionMonth == model.primaryMonth - 1 + && dstIndicator.transitionYear == model.year + target: dstIndicator + property: "parent" + value: mouseArea + } + } + } + DstIndicator { + id: dstIndicator + anchors { + left: parent.left + verticalCenter: parent.verticalCenter + verticalCenterOffset: -textVerticalCenterOffset + } + referenceDateTime: new Date(datePicker.date.getFullYear(), + datePicker.date.getMonth(), 1, 0, 0) + visible: transitionMonth == datePicker.date.getMonth() + && transitionYear == datePicker.date.getFullYear() + } + + ChangeMonthHint {} + } +} diff --git a/usr/share/jolla-calendar/pages/DayOverlapPage.qml b/usr/share/jolla-calendar/pages/DayOverlapPage.qml new file mode 100644 index 00000000..a7c1dc21 --- /dev/null +++ b/usr/share/jolla-calendar/pages/DayOverlapPage.qml @@ -0,0 +1,71 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Sailfish.Calendar 1.0 +import org.nemomobile.calendar 1.0 +import "Util.js" as Util + +Page { + id: root + + property alias model: eventList.model + property date date + + SilicaFlickable { + anchors.fill: parent + contentHeight: content.height + + Column { + id: content + width: parent.width + + PageHeader { + id: pageHeader + title: Util.capitalize(Format.formatDate(root.date, Formatter.WeekdayNameStandalone)) + } + Text { + y: Theme.itemSizeSmall + anchors { + right: parent.right + rightMargin: Theme.horizontalPageMargin + } + color: Theme.secondaryHighlightColor + font.pixelSize: Theme.fontSizeMedium + text: Format.formatDate(root.date, Formatter.DateLong) + } + + Repeater { + model: AgendaModel { + filterMode: AgendaModel.FilterNonAllDay + startDate: root.date + } + + delegate: CalendarEventListDelegate { + width: parent.width + activeDay: root.date + onClicked: { + pageStack.animatorPush("EventViewPage.qml", + { instanceId: model.event.instanceId, + startTime: model.occurrence.startTime, + 'remorseParent': root + }) + } + } + } + + Repeater { + id: eventList + delegate: CalendarEventListDelegate { + width: parent.width + activeDay: root.date + onClicked: { + pageStack.animatorPush("EventViewPage.qml", + { instanceId: model.event.instanceId, + startTime: model.occurrence.startTime, + 'remorseParent': root + }) + } + } + } + } + } +} diff --git a/usr/share/jolla-calendar/pages/DayPageEventDelegate.qml b/usr/share/jolla-calendar/pages/DayPageEventDelegate.qml new file mode 100644 index 00000000..a7f31587 --- /dev/null +++ b/usr/share/jolla-calendar/pages/DayPageEventDelegate.qml @@ -0,0 +1,76 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import org.nemomobile.calendar 1.0 +import Sailfish.Calendar 1.0 + +BackgroundItem { + id: eventItem + property alias fontSize: displayLabel.font.pixelSize + property bool oneLiner: true + + width: 0 // layouting sets + + Rectangle { + id: bar + x: oneLiner ? Theme.paddingMedium : Theme.paddingSmall + width: Theme.paddingSmall + radius: Math.round(width/3) + color: event.color + + anchors { + top: parent.top + topMargin: Theme.paddingSmall + bottom: parent.bottom + bottomMargin: Theme.paddingSmall + } + } + + Label { + id: displayLabel + anchors { + left: bar.right + leftMargin: oneLiner ? Theme.paddingMedium : Theme.paddingSmall + right: parent.right + } + visible: width > 0 + color: highlighted ? Theme.highlightColor : Theme.primaryColor + text: CalendarTexts.ensureEventTitle(event.displayLabel) + opacity: event.status == CalendarEvent.StatusCancelled ? Theme.opacityHigh : 1. + truncationMode: oneLiner ? TruncationMode.Fade : TruncationMode.None + wrapMode: oneLiner ? Text.NoWrap : Text.Wrap + clip: !oneLiner + height: oneLiner ? implicitHeight : Math.min(parent.height, implicitHeight) + } + OpacityRampEffect { + enabled: displayLabel.implicitHeight > displayLabel.height + direction: OpacityRamp.TopToBottom + sourceItem: displayLabel + slope: Math.max(1, displayLabel.height / Theme.paddingLarge) + offset: 1 - 1 / slope + } + + Label { + visible: eventItem.height >= (displayLabel.height + implicitHeight) + anchors { + left: displayLabel.left + right: parent.right + top: displayLabel.bottom + topMargin: -Math.round(Theme.paddingSmall/2) + } + font.pixelSize: displayLabel.font.pixelSize + //% "The event is cancelled." + text: event.status == CalendarEvent.StatusCancelled ? qsTrId("calendar-la-event_cancelled") : event.location + maximumLineCount: 1 + color: highlighted ? Theme.secondaryHighlightColor : Theme.secondaryColor + truncationMode: TruncationMode.Fade + } + + onClicked: { + pageStack.animatorPush("EventViewPage.qml", + { instanceId: event.instanceId, + startTime: occurrence.startTime, + 'remorseParent': eventItem + }) + + } +} diff --git a/usr/share/jolla-calendar/pages/DayPageHeaderFooterEvent.qml b/usr/share/jolla-calendar/pages/DayPageHeaderFooterEvent.qml new file mode 100644 index 00000000..decad715 --- /dev/null +++ b/usr/share/jolla-calendar/pages/DayPageHeaderFooterEvent.qml @@ -0,0 +1,78 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import org.nemomobile.calendar 1.0 +import Sailfish.Calendar 1.0 + +BackgroundItem { + id: root + + property QtObject event + property date currentDate + + property date _startTime: event && event.occurrence ? event.occurrence.startTime : new Date() + property bool _showDate: currentDate.getYear() !== _startTime.getYear() + || currentDate.getMonth() !== _startTime.getMonth() + || currentDate.getDate() !== _startTime.getDate() + + height: event ? Theme.itemSizeSmall : 0 + opacity: event ? 1.0 : 0.0 + visible: opacity > 0.0 + + Behavior on height { NumberAnimation { easing.type: "InOutQuad"; duration: 200 } } + Behavior on opacity { FadeAnimation { } } + + Label { + id: time + anchors { + left: parent.left + leftMargin: Screen.sizeCategory > Screen.Medium ? Theme.horizontalPageMargin : Theme.paddingSmall + verticalCenter: parent.verticalCenter + } + visible: event && !event.event.allDay + color: root.highlighted ? Theme.highlightColor : Theme.primaryColor + font.pixelSize: Theme.fontSizeSmall + font.strikeout: event && event.event.status == CalendarEvent.StatusCancelled + text: Format.formatDate(_startTime, Formatter.TimeValue) + } + + Rectangle { + id: calendarColor + anchors { + left: time.right; top: parent.top + bottom: parent.bottom; margins: Theme.paddingMedium + leftMargin: Theme.paddingMedium + Theme.paddingSmall + } + width: Theme.paddingSmall + radius: Math.round(width / 3) + color: event ? event.event.color : "transparent" + } + + Label { + anchors { + left: calendarColor.right; right: date.left + verticalCenter: parent.verticalCenter + margins: Theme.paddingMedium + } + color: root.highlighted ? Theme.highlightColor : Theme.primaryColor + font.pixelSize: Theme.fontSizeSmall + text: CalendarTexts.ensureEventTitle(event ? event.event.displayLabel : "") + truncationMode: TruncationMode.Fade + opacity: event && event.event.status == CalendarEvent.StatusCancelled ? Theme.opacityHigh : 1. + } + + Label { + id: date + anchors { + right: parent.right + verticalCenter: parent.verticalCenter + rightMargin: Theme.horizontalPageMargin - Theme.paddingMedium + } + color: root.highlighted ? Theme.highlightColor : Theme.primaryColor + font.pixelSize: Theme.fontSizeSmall + //% "d MMMM" + text: event && _showDate ? Qt.formatDate(_startTime, qsTrId("calendar-date_pattern_date_month")) : "" + opacity: _showDate ? 1.0 : 0.0 + visible: opacity > 0.0 + Behavior on opacity { FadeAnimation { } } + } +} diff --git a/usr/share/jolla-calendar/pages/DayPageOverlapDelegate.qml b/usr/share/jolla-calendar/pages/DayPageOverlapDelegate.qml new file mode 100644 index 00000000..605e083e --- /dev/null +++ b/usr/share/jolla-calendar/pages/DayPageOverlapDelegate.qml @@ -0,0 +1,61 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 + +BackgroundItem { + id: root + property alias fontSize: description.font.pixelSize + property bool oneLiner: true + + Rectangle { + x: Theme.paddingSmall + y: Theme.paddingSmall + width: parent.width - Theme.paddingSmall - Theme.horizontalPageMargin + Theme.paddingLarge + height: parent.height - 2 * Theme.paddingSmall + color: Theme.secondaryHighlightColor + radius: Theme.paddingSmall / 3 + Label { + id: description + x: Theme.paddingSmall + width: parent.width - 2 * Theme.paddingSmall + text: overlapTitles.join(Format.listSeparator) + wrapMode: Text.Wrap + elide: oneLiner ? Text.ElideRight : Text.ElideNone + visible: height > 0 + height: Math.min(implicitHeight, parent.height - overview.height - Theme.paddingLarge) + } + OpacityRampEffect { + enabled: !oneLiner && description.visible + && description.implicitHeight > description.height + direction: OpacityRamp.TopToBottom + sourceItem: description + slope: Math.max(1, description.height / Theme.paddingLarge) + offset: 1 - 1 / slope + } + Label { + id: overview + + width: Math.min(parent.width - 2 * Theme.paddingSmall, implicitWidth) + truncationMode: oneLiner ? TruncationMode.Fade : TruncationMode.None + wrapMode: oneLiner ? Text.NoWrap : Text.Wrap + clip: !oneLiner + height: Math.min(implicitHeight, parent.height) + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottom: parent.bottom + //: label on hour view for too many overlapping events to be shown + //% "See overlapping events" + text: qsTrId("calendar-la-overlapping_events") + font.pixelSize: description.font.pixelSize + } + OpacityRampEffect { + enabled: !oneLiner + && overview.implicitHeight > overview.height + direction: OpacityRamp.TopToBottom + sourceItem: overview + slope: Math.max(1, overview.height / Theme.paddingLarge) + offset: 1 - 1 / slope + } + } + onClicked: { + pageStack.animatorPush("DayOverlapPage.qml", { model: overlapEvents, date: date }) + } +} diff --git a/usr/share/jolla-calendar/pages/DayTimesBackground.qml b/usr/share/jolla-calendar/pages/DayTimesBackground.qml new file mode 100644 index 00000000..0526e529 --- /dev/null +++ b/usr/share/jolla-calendar/pages/DayTimesBackground.qml @@ -0,0 +1,110 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Nemo.Configuration 1.0 + +Column { + id: root + + property date date + property int pageHeaderHeight + + // Make height calculations explicit. Column does not update its implicit + // height when invisible. Cell height follows system font size changes + height: pageHeaderHeight + 2*24*dayPage.cellHeight + + ConfigurationValue { + id: timeFormatConfig + key: "/sailfish/i18n/lc_timeformat24h" + } + + Item { + width: 1 + height: root.pageHeaderHeight + + Item { + height: parent.height + width: dayPage.width + + Rectangle { + anchors.fill: parent + color: Theme.primaryColor + opacity: 0.15 + } + + Label { + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter + color: Theme.highlightColor + text: Format.formatDate(root.date, Formatter.DateFull) + font.pixelSize: Theme.fontSizeMedium + } + } + } + + Repeater { + id: timesRepeater + model: 24 + delegate: Item { + width: timeLabel.x + timeLabel.width + Theme.paddingSmall // note: only label, background has page width + height: dayPage.cellHeight * 2 + + BackgroundItem { + id: backgroundItem + + anchors.fill: timeRect + onClicked: { + dayPage.timeClicked(getTime()) + } + onPressAndHold: { + dayPage.timePressAndHold(getTime()) + } + function getTime() { + var time = new Date(root.date.getTime()) + time.setMinutes(time.getMinutes() + index * 60) + return time + } + } + + Rectangle { + id: timeRect + width: dayPage.width + height: parent.height + color: Theme.primaryColor + opacity: 0.05 + visible: (index) & 1 + } + + Label { + id: timeLabel + x: Screen.sizeCategory > Screen.Medium ? Theme.horizontalPageMargin : Theme.paddingSmall + height: parent.cellHeight + verticalAlignment: Text.AlignVCenter + font.pixelSize: Theme.fontSizeSmall + color: backgroundItem.highlighted ? Theme.highlightColor : Theme.primaryColor + opacity: !((index) & 1) ? Theme.opacityLow : Theme.opacityHigh + text: { + if (timeFormatConfig.value == "12") { + var hour = index % 12 + if (hour == 0) + hour = 12 + + if (index % 6 == 0) { + var amPm = Format.formatArticle(index < 12 ? Formatter.AnteMeridiemIndicator + : Formatter.PostMeridiemIndicator) + //: Hour pattern in day page flickable for 12h mode, %1 is hour, %2 is am/pm indicator, + //: shown at 12 and 6 + //% "%1 %2" + return qsTrId("calendar_daypage_hour_indicator_12h_pattern").arg(hour.toLocaleString()).arg(amPm) + } else { + return hour.toLocaleString() + } + } else { + // FIXME: pattern not localized + var zero = Qt.locale().zeroDigit + return ((index < 10) ? zero : "") + index.toLocaleString() + ":" + zero + zero + } + } + } + } + } +} diff --git a/usr/share/jolla-calendar/pages/DayTimesFlickable.qml b/usr/share/jolla-calendar/pages/DayTimesFlickable.qml new file mode 100644 index 00000000..b9faed25 --- /dev/null +++ b/usr/share/jolla-calendar/pages/DayTimesFlickable.qml @@ -0,0 +1,87 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import org.nemomobile.calendar 1.0 + +SilicaFlickable { + id: root + + default property alias timesData: events.data + + property date date: new Date + + property date startDate: new Date(1980, 0, 1) + property date endDate: new Date(2199, 11, 31) + property int pageHeaderHeight: Theme.itemSizeSmall + property real previousDayHeight + + property int _maxDay: 1 + Math.floor(Math.max(0, _stripTime(endDate) - _stripTime(startDate)) / 86400000) + + function gotoDate(date) { + var day = QtDate.daysTo(_stripTime(startDate), date); + contentY = day * day1.height + date.getHours() * 2*dayPage.cellHeight + _updateDayBackground() + } + + function _stripTime(date) { + return new Date(date.getFullYear(), date.getMonth(), date.getDate()) + } + + function _updateDayBackground() { + if (!day1.height) return + + var cw = Math.max(0, contentY) + var day = Math.floor(cw / day1.height) + + previousDayHeight = day1.height + day1.y = day * day1.height + day2.y = (day + 1) * day1.height + date = QtDate.addDays(_stripTime(startDate), day) + day1.date = date + day2.date = QtDate.addDays(date, 1) + + if (day + 1 >= _maxDay) { + day2.visible = false + } else { + day2.visible = true + } + } + + function daysForDate(date) + { + return Math.max(0, QtDate.daysTo(_stripTime(startDate), date)) + } + + quickScroll: false + contentHeight: _maxDay * day1.height + onContentYChanged: _updateDayBackground() + Component.onCompleted: gotoDate(date) + + DayTimesBackground { id: day1; pageHeaderHeight: root.pageHeaderHeight } + DayTimesBackground { id: day2; pageHeaderHeight: root.pageHeaderHeight } + + Item { + id: events + anchors.left: day1.right + anchors.right: parent.right + } + + // Update geometry if the cell height changes, do asyncronously + // so positioners have time to calculate correct day height + Connections { + target: dayPage + onCellHeightChanged: lateUpdateDayBackgroundTimer.restart() + } + + Timer { + id: lateUpdateDayBackgroundTimer + interval: 10 + onTriggered: { + if (day1.height !== previousDayHeight) { + // If day height has changed the current contentY has become invalid, fix it + root.contentY = root.contentY*day1.height/previousDayHeight + _updateDayBackground() + } + } + } +} + diff --git a/usr/share/jolla-calendar/pages/DayView.qml b/usr/share/jolla-calendar/pages/DayView.qml new file mode 100644 index 00000000..3d8de9ee --- /dev/null +++ b/usr/share/jolla-calendar/pages/DayView.qml @@ -0,0 +1,345 @@ +import QtQuick 2.4 +import Sailfish.Silica 1.0 +import org.nemomobile.calendar 1.0 +import Nemo.Time 1.0 +import Calendar.hourViewLayouter 1.0 +import "Util.js" as Util + +Item { + id: dayPage + + readonly property alias date: flickable.date + property int cellHeight: Math.max(fontMetrics.height, Theme.itemSizeSmall/2) + property alias flickable: header + + readonly property string title: Util.capitalize(Format.formatDate(date, Formatter.WeekdayNameStandalone)) + readonly property string description: Format.formatDate(date, Formatter.DateLong) + + property Item tabHeader + function attachHeader(tabHeader) { + if (tabHeader) { + tabHeader.parent = tabHeaderContainer + } + dayPage.tabHeader = tabHeader + } + function detachHeader() { + dayPage.tabHeader = null + } + function gotoDate(date) { + flickable.gotoDate(date) + } + + function timeClicked(time) { + pageStack.animatorPush("EventEditPage.qml", { defaultDate: time }) + } + + function timePressAndHold(time) { + return // FIXME: this simply does not work + } + + Connections { + target: tabHeader + onDateClicked: { + var obj = pageStack.animatorPush("Sailfish.Silica.DatePickerDialog") + obj.pageCompleted.connect(function(page) { + page.accepted.connect(function() { + flickable.gotoDate(page.selectedDate) + }) + }) + } + } + + Component { + id: eventDelegate + DayPageEventDelegate { + onPressAndHold: { + var coord = mapToItem(flickable.contentItem, mouse.x, mouse.y) + dayPage.timePressAndHold(coord.x, coord.y) + } + } + } + Component { id: overlapDelegate; DayPageOverlapDelegate {} } + + FontMetrics { + id: fontMetrics + font.pixelSize: Theme.fontSizeMedium + } + + SilicaFlickable { + id: header + width: parent.width + height: topContainer.height + + Column { + id: topContainer + width: parent.width + spacing: isPortrait ? Theme.paddingLarge : Theme.paddingMedium + + Item { + width: parent.width + height: isPortrait ? (allDayList.height + tabHeaderContainer.height) + : Math.max(allDayList.height, tabHeaderContainer.height) + + Item { + id: tabHeaderContainer + width: isPortrait ? parent.width : (parent.width / 2) + height: dayPage.tabHeader ? dayPage.tabHeader.height : 0 + x: isPortrait ? 0 : allDayList.width + } + + ListView { + id: allDayList + height: dayPage.cellHeight + width: isPortrait ? parent.width : (parent.width / 2) + y: isPortrait ? tabHeaderContainer.height : ((parent.height - height) / 2) + interactive: false + layoutDirection: Qt.RightToLeft + orientation: ListView.Horizontal + clip: true // can be removed if Page starts clipping its content, bug 26058 + model: AgendaModel { + filterMode: AgendaModel.FilterNonAllDay + startDate: flickable.date + } + + delegate: DayPageEventDelegate { + width: allDayList.width / Math.min(2, allDayList.model.count) + height: dayPage.cellHeight + // FIXME: long press to show context menu. contextMenuAllDayEvent currently unused + } + } + } + + DayPageHeaderFooterEvent { + id: earlier + currentDate: flickable.date + event: hourViewLayouter.earlierEvent + width: parent.width + onClicked: { + var time = new Date(event.occurrence.startTime.getTime()) + + if (event.event.allDay) { + time.setHours(8) + } else { + time.setHours(time.getHours() - 2) + } + + scrollAnimation.to = hourViewLayouter.timeToPosition(time) + scrollAnimation.start() + } + Image { + anchors.fill: parent + source: "image://theme/graphic-gradient-edge" + rotation: 180 + } + } + } + } + + Item { + id: flickableContainer + + anchors.top: header.bottom + anchors.topMargin: -header.contentY + height: parent.height - header.height + width: parent.width + visible: !dummyFlickable.visible + + Item { + // flickable stays from date label to bottom, this item clips the view to avoid extra items on both ends + height: parent.height - later.height + width: parent.width + clip: true + + DayTimesFlickable { + id: flickable + + y: -parent.y + height: flickableContainer.height + width: flickableContainer.width + + Rectangle { + width: parent.width + height: Math.round(3 * Theme.pixelRatio) + y: { + var dayStartTime = new Date(currentTime.time.getTime()) + dayStartTime.setHours(0, 0, 0, 0) + var dayStartPosition = hourViewLayouter.timeToPosition(dayStartTime) + var dayHeight = 48 * dayPage.cellHeight + var relativePosition = (currentTime.time.getHours()*60 + currentTime.time.getMinutes()) / (24*60) + var dayPosition = Math.min(dayHeight - height, (relativePosition * dayHeight)) + return dayStartPosition + dayPosition + } + color: Theme.secondaryHighlightColor + + WallClock { + id: currentTime + updateFrequency: WallClock.Minute + enabled: Qt.application.active + } + } + + Item { + id: events + width: parent.width - (Screen.sizeCategory > Screen.Medium ? Theme.horizontalPageMargin : 0) + + HourViewLayouter { + id: hourViewLayouter + + model: AgendaModel { + startDate: QtDate.addDays(flickable.date, -7) + endDate: QtDate.addDays(flickable.date, 7) + } + delegate: eventDelegate + overlapDelegate: overlapDelegate + delegateParent: events + visibleY: flickable.contentY + height: flickable.height + width: events.width + cellHeight: dayPage.cellHeight + daySeparatorHeight: flickable.pageHeaderHeight + startDate: flickable.startDate + currentDate: flickable.date + } + } + + NumberAnimation { + id: scrollAnimation + target: flickable + property: "contentY" + duration: 400 + easing.type: Easing.InOutQuad + } + } + } + } + + // This horrible hack exists because we want the animation behavior of a regular context menu, + // but we want to split the day view apart when it appears. Anything else looks very silly. + Flickable { + id: dummyFlickable + y: flickableContainer.y + height: flickable.height + width: parent.width + contentHeight: flickable.contentHeight + visible: contextMenu.height != 0 + + property real splitY + property real initialY + + ShaderEffectSource { + id: dummyFlickableSource + sourceItem: dummyFlickable.visible?flickableContainer:null + hideSource: true + } + + children: [ + Item { + width: parent.width + height: contextMenu.y - dummyFlickable.contentY + + FadeEffect { + anchors.fill: parent + source: dummyFlickableSource + fadeMode: 1 + fade: 0 + sourceOffset: dummyFlickable.splitY - height + sourceHeight: height + } + }, + + Item { + y: (contextMenu.y + contextMenu.height - dummyFlickable.contentY) + width: parent.width + height: parent.height - y + + FadeEffect { + anchors.fill: parent + source: dummyFlickableSource + fadeMode: 2 + fade: 0 + sourceOffset: dummyFlickable.splitY + sourceHeight: height + } + } + ] + + Item { + id: contextMenu + + property Item event + property date date + + height: (contextMenuEvent.parent == contextMenu) ? contextMenuEvent.height : contextMenuBasic.height + } + } + + // We use two ContextMenu's as sometimes the layout (and thus the animation) doesn't work correctly + // if you just set the visibility of the various MenuItems. + ContextMenu { + id: contextMenuBasic + MenuItem { + //% "New event" + text: qsTrId("calendar-day-new_event") + onClicked: pageStack.animatorPush("EventEditPage.qml", { defaultDate: contextMenu.date }) + } + } + ContextMenu { + id: contextMenuEvent + MenuItem { + //% "Edit" + text: qsTrId("calendar-day-edit") + onClicked: pageStack.animatorPush("EventEditPage.qml", { event: contextMenu.event.modelObject }) + } + MenuItem { + //% "Delete" + text: qsTrId("calendar-day-delete") + onClicked: { + var instanceId = contextMenu.event.modelObject.event.instanceId + var startTime = contextMenu.event.modelObject.occurrence.startTime + Remorse.itemAction(contextMenu.event, Remorse.deletedText, // TODO: Migrate DayPageEventDelegate to ListItem + function() { Calendar.remove(instanceId, startTime) }) + } + } + MenuItem { + //% "New event" + text: qsTrId("calendar-day-new_event") + onClicked: pageStack.animatorPush("EventEditPage.qml", { defaultDate: contextMenu.date }) + } + } +/* ContextMenu { + id: contextMenuAllDayEvent + MenuItem { + //% "Edit" + text: qsTrId("calendar-day-edit") + } + MenuItem { + //% "Delete" + text: qsTrId("calendar-day-delete") + } + } */ + DayPageHeaderFooterEvent { + id: later + anchors.bottom: flickableContainer.bottom + currentDate: flickable.date + event: hourViewLayouter.laterEvent + width: parent.width + + onClicked: { + var time = new Date(event.occurrence.startTime.getTime()) + if (event.event.allDay) { + time.setHours(3) + } else { + time.setHours(time.getHours() - 2) + } + + scrollAnimation.to = hourViewLayouter.timeToPosition(time) + scrollAnimation.start() + } + + Image { + anchors.fill: parent + source: "image://theme/graphic-gradient-edge" + } + } +} + diff --git a/usr/share/jolla-calendar/pages/DeletableListDelegate.qml b/usr/share/jolla-calendar/pages/DeletableListDelegate.qml new file mode 100644 index 00000000..761f3eec --- /dev/null +++ b/usr/share/jolla-calendar/pages/DeletableListDelegate.qml @@ -0,0 +1,89 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Sailfish.Calendar 1.0 +import org.nemomobile.calendar 1.0 +import Calendar.syncHelper 1.0 + +CalendarEventListDelegate { + id: root + + property Item _remorse + + onClicked: { + pageStack.animatorPush("EventViewPage.qml", + { instanceId: model.event.instanceId, + startTime: model.occurrence.startTime, + 'remorseParent': root + }) + } + + menu: Component { + ContextMenu { + id: contextMenu + width: root.width + x: 0 + MenuLabel { + visible: model.event.readOnly + //% "This event cannot be modified" + text: qsTrId("calendar-event-event_cannot_be_modified") + } + + MenuItem { + visible: !model.event.readOnly && !model.event.externalInvitation + // "Edit" + text: qsTrId("calendar-day-edit") + onClicked: { + // TODO: should recurrence exception (recurrence id exists) allow to modify main event? + if (model.event.recur != CalendarEvent.RecurOnce) { + pageStack.animatorPush("EventEditRecurringPage.qml", { event: model.event, + occurrence: model.occurrence }) + } else { + pageStack.animatorPush("EventEditPage.qml", { event: model.event }) + } + } + } + MenuItem { + visible: !model.event.readOnly + //% "Delete" + text: qsTrId("calendar-day-delete") + onClicked: { + // TODO: on recurrence exception, this just deletes the exception. Doesn't ask to delete series. + if (model.event.recur != CalendarEvent.RecurOnce) { + pageStack.animatorPush("EventDeletePage.qml", + { event: model.event, + instanceId: model.event.instanceId, + calendarUid: model.event.calendarUid, + startTime: model.occurrence.startTime + }) + } else { + contextMenu.parent.deleteActivated() + } + } + } + } + } + + Connections { + id: dayConnection + ignoreUnknownSignals: true + onStartDateChanged: { + target = null + if (_remorse && _remorse.pending) { + _remorse.cancel() + Calendar.remove(model.event.instanceId) + app.syncHelper.triggerUpdateDelayed(model.event.calendarUid) + } + } + } + + function deleteActivated() { + // Assuming id/property. Need to trigger deletion before day change refreshes content. + // RemorseItem itself would try to execute its command, but model target might be already deleted. + dayConnection.target = view.model + _remorse = remorseDelete(function() { + Calendar.remove(model.event.instanceId) + app.syncHelper.triggerUpdateDelayed(model.event.calendarUid) + }) + } +} + diff --git a/usr/share/jolla-calendar/pages/DstIndicator.qml b/usr/share/jolla-calendar/pages/DstIndicator.qml new file mode 100644 index 00000000..4603acec --- /dev/null +++ b/usr/share/jolla-calendar/pages/DstIndicator.qml @@ -0,0 +1,76 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Calendar.daylightSavingTime 1.0 + +Column { + id: root + property alias referenceDateTime: dst.referenceDateTime + property real textVerticalCenterOffset: text.y + text.height / 2 - height / 2 + property string transitionDescription + + property int transitionTime: -1 + property int transitionDay: -1 + property int transitionMonth: -1 + property int transitionYear: -1 + states: State { + when: dst.nextDaylightSavingTime + && !isNaN(dst.nextDaylightSavingTime.getTime()) + PropertyChanges { + target: root + transitionTime: dst.nextDaylightSavingTime.getHours() + transitionDay: dst.nextDaylightSavingTime.getDate() + transitionMonth: dst.nextDaylightSavingTime.getMonth() + transitionYear: dst.nextDaylightSavingTime.getFullYear() + transitionDescription: { + var beforeDST = dst.nextDaylightSavingTime + beforeDST.setDate(beforeDST.getDate() - 1) + beforeDST.setTime(beforeDST.getTime() - dst.daylightSavingOffset * 1000) + // We just need the time here. But the time of DST will be the + // new time after change, while we need the time before change. + // We work with the day before DST to be able to get the time + // at the moment of the DST. This time does not exist at the DST day. + var timeStr = Format.formatDate(beforeDST, Formatter.TimeValue) + var hourShift = dst.daylightSavingOffset / 3600 + if (dst.daylightSavingOffset < 0) { + //: in most cases %n == 1 and can be translated like 'an hour backward' + //% "At %1, clocks are turned backward %n hour." + return qsTrId("sailfish-calendar_la_dst-move-backward", -hourShift).arg(timeStr) + } else { + //: in most cases %n == 1 and can be translated like 'an hour forward' + //% "At %1, clocks are turned forward %n hour." + return qsTrId("sailfish-calendar_la_dst-move-forward", hourShift).arg(timeStr) + } + } + } + } + + property int _margin: Theme.paddingSmall / 2 + + width: implicitWidth + 2 * _margin + spacing: -_margin + + Icon { + x: root._margin + source: "image://theme/icon-s-time" + color: Theme.secondaryHighlightColor + width: text.width + fillMode: Image.PreserveAspectFit + } + Label { + id: text + x: root._margin + font.pixelSize: Theme.fontSizeTinyBase + text: { + if (dst.daylightSavingOffset < 0) { + return "-" + (-dst.daylightSavingOffset / 3600) + } else { + return "+" + (dst.daylightSavingOffset / 3600) + } + } + color: Theme.secondaryHighlightColor + } + + DaylightSavingTime { + id: dst + } +} diff --git a/usr/share/jolla-calendar/pages/EventDeletePage.qml b/usr/share/jolla-calendar/pages/EventDeletePage.qml new file mode 100644 index 00000000..b3e73705 --- /dev/null +++ b/usr/share/jolla-calendar/pages/EventDeletePage.qml @@ -0,0 +1,86 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import org.nemomobile.calendar 1.0 +import Calendar.syncHelper 1.0 + +Page { + id: root + + property QtObject event + property string instanceId + property string calendarUid + property date startTime + + property bool _smallLandscape: isLandscape && Screen.sizeCategory <= Screen.Medium + + Column { + y: _smallLandscape ? Theme.paddingLarge : Theme.itemSizeExtraLarge + width: parent.width + spacing: _smallLandscape ? Theme.itemSizeExtraSmall : Theme.itemSizeSmall + + Label { + x: Theme.horizontalPageMargin + width: parent.width - 2*Theme.horizontalPageMargin + color: Theme.highlightColor + font.pixelSize: Theme.fontSizeExtraLarge + wrapMode: Text.Wrap + horizontalAlignment: Text.AlignHCenter + //% "This is a recurring event" + text: qsTrId("calendar-event-ph-delete_recurring") + } + + Label { + x: Theme.horizontalPageMargin + width: parent.width - 2*Theme.horizontalPageMargin + color: Theme.highlightColor + font.pixelSize: Theme.fontSizeMedium + wrapMode: Text.Wrap + horizontalAlignment: Text.AlignHCenter + //% "Confirm, if you want to delete this event, later events or all events." + text: qsTrId("calendar-event-delete_confirmation") + } + } + + + ButtonLayout { + anchors.bottom: parent.bottom + anchors.bottomMargin: _smallLandscape ? Theme.itemSizeExtraSmall : Theme.itemSizeMedium + preferredWidth: Theme.buttonWidthMedium + + Button { + //% "Delete this event" + text: qsTrId("calendar-event-delete_occurrence") + onClicked: { + Calendar.remove(instanceId, startTime) + app.syncHelper.triggerUpdateDelayed(calendarUid) + app.showMainPage() + } + } + + Button { + ButtonLayout.newLine: true + //% "Delete this and future events" + text: qsTrId("calendar-event-delete_all_future_occurences") + onClicked: { + var modification = Calendar.createModification(root.event) + // setRecurEndDate() is inclusive. + modification.setRecurEndDate(QtDate.addDays(startTime, -1)) + modification.save() + app.syncHelper.triggerUpdateDelayed(calendarUid) + app.showMainPage() + } + } + + Button { + ButtonLayout.newLine: true + //% "Delete the series" + text: qsTrId("calendar-event-delete_all_occurences") + onClicked: { + Calendar.removeAll(instanceId) + app.syncHelper.triggerUpdateDelayed(calendarUid) + app.showMainPage() + } + } + } +} + diff --git a/usr/share/jolla-calendar/pages/EventEditPage.qml b/usr/share/jolla-calendar/pages/EventEditPage.qml new file mode 100644 index 00000000..2cd2eb2b --- /dev/null +++ b/usr/share/jolla-calendar/pages/EventEditPage.qml @@ -0,0 +1,794 @@ +/**************************************************************************** +** +** Copyright (C) 2015 - 2019 Jolla Ltd. +** Copyright (C) 2020 Open Mobile Platform LLC. +** +****************************************************************************/ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import org.nemomobile.calendar 1.0 +import Sailfish.Calendar 1.0 +import Sailfish.Timezone 1.0 +import Calendar.syncHelper 1.0 +import Nemo.Notifications 1.0 as SystemNotifications +import Nemo.Configuration 1.0 +import org.nemomobile.systemsettings 1.0 +import Sailfish.Silica.private 1.0 as Private + +Dialog { + id: dialog + + property date defaultDate: new Date() + // if set, edit the event, otherwise create a new one + property QtObject event + property bool _isEdit: dialog.event + property QtObject occurrence + property bool _replaceOccurrence: dialog.occurrence + property var newInstanceIdCb + property bool attendeesModified + + canAccept: notebookQuery.isValid && dateSelector.valid + + onAcceptBlocked: { + if (!dateSelector.valid) { + //% "Event start time needs to be before end time" + systemNotification.body = qsTrId("jolla-calendar-event_time_problem_notification") + systemNotification.publish() + } + } + + function stripTime(date) { + return new Date(date.getFullYear(), date.getMonth(), date.getDate()) + } + + function showAttendeePicker() { + var obj = pageStack.animatorPush(Qt.resolvedUrl("AttendeeSelectionPage.qml"), + { + requiredAttendees: requiredAttendees, + optionalAttendees: optionalAttendees + }) + obj.pageCompleted.connect(function(page) { + page.modified.connect(function() { + dialog.attendeesModified = true + }) + }) + } + + Component { + id: recurEndDatePicker + DatePickerDialog { + canAccept: selectedDate.getTime() >= stripTime(dateSelector.startDate) + } + } + + Component { + id: calendarPicker + CalendarPicker { + hideExcludedCalendars: true + onCalendarClicked: { + notebookQuery.targetUid = uid + selectedCalendarUid = uid + pageStack.pop() + } + } + } + + SystemNotifications.Notification { + id: systemNotification + + appIcon: "icon-lock-calendar" + isTransient: true + } + + ConfigurationValue { + id: reminderConfig + + key: "/sailfish/calendar/default_reminder" + defaultValue: -1 + } + + ConfigurationValue { + id: reminderAlldayConfig + + key: "/sailfish/calendar/default_reminder_allday" + defaultValue: -1 + } + + ContactModel { + id: requiredAttendees + } + + ContactModel { + id: optionalAttendees + } + + EventQuery { + property bool initialized + + instanceId: dialog.event ? dialog.event.instanceId : "" + + onAttendeesChanged: { + // only handle once, query status might fluctuate, JB#32993 + if (initialized || dialog.attendeesModified || attendees.length === 0) { + return + } + + for (var i = 0; i < attendees.length; ++i) { + var attendee = attendees[i] + // we should be organizer if editing, list only others + if (attendee.isOrganizer) { + continue + } + + if (attendee.participationRole == Person.RequiredParticipant) { + requiredAttendees.append(attendee.name, attendee.email) + } else { + optionalAttendees.append(attendee.name, attendee.email) + } + } + + initialized = true + } + } + + NotebookQuery { + id: notebookQuery + targetUid: dialog._isEdit ? dialog.event.calendarUid : Calendar.defaultNotebook + } + + SilicaFlickable { + anchors.fill: parent + contentHeight: header.height + col.height + Theme.paddingLarge + + DialogHeader { + id: header + + //% "Save" + acceptText: qsTrId("calendar-ph-event_edit_save") + } + + VerticalScrollDecorator {} + + Column { + id: col + + width: parent.width + anchors.top: header.bottom + + TextField { + id: eventName + + //% "Event name" + placeholderText: qsTrId("calendar-add-event_name") + label: placeholderText + width: parent.width + EnterKey.iconSource: "image://theme/icon-m-enter-next" + EnterKey.onClicked: eventLocation.focus = true + + Private.AutoFill { + id: nameAutoFill + key: "calendar.eventName" + } + } + + TextField { + id: eventLocation + + //% "Event location" + placeholderText: qsTrId("calendar-add-event_location") + label: placeholderText + width: parent.width + EnterKey.iconSource: "image://theme/icon-m-enter-next" + EnterKey.onClicked: eventDescription.focus = true + + Private.AutoFill { + id: locationAutoFill + key: "calendar.eventLocation" + } + } + + TextArea { + id: eventDescription + + //% "Description" + placeholderText: qsTrId("calendar-add-description") + label: placeholderText + width: parent.width + } + + ValueButton { + id: attendeeButton + + // TODO: we should have some property on notebooks telling whether they support + // creating invitations. For the moment let's just disable for local calendars. + enabled: !notebookQuery.localCalendar + label: (requiredAttendees.count + optionalAttendees.count > 0) + ? //% "%n people invited" + qsTrId("calendar-invited_people", requiredAttendees.count + optionalAttendees.count) + : //% "Invite people" + qsTrId("calendar-invite_people") + //% "You cannot invite people to a local calendar event" + description: notebookQuery.localCalendar ? qsTrId("calendar-cannot_invite_people") : "" + value: concatenateAttendees([requiredAttendees, optionalAttendees]) + onClicked: showAttendeePicker() + + function concatenateAttendees(models) { + var result = "" + for (var i = 0; i < models.length; ++i) { + var model = models[i] + + for (var j = 0; j < model.count; ++j) { + var displayName = model.name(j) + if (displayName.length == 0) { + displayName = model.email(j) + } + if (result.length !== 0) { + result += Format.listSeparator + } + result += displayName + } + } + + return result + } + } + + TimeRangeSelector { + id: dateSelector + + readonly property bool valid: dateSelector.allDay ? (stripTime(startDate) <= stripTime(endDate)) + : startDate <= endDate + showError: !valid + allDay: allDay.checked + + function handleStartTimeModification(newStartTime, dateChange) { + var wasValid = valid + var diff = newStartTime.getTime() - startDate.getTime() + setStartDate(newStartTime) + + if (wasValid) { + var newEnd = new Date(dateSelector.endDate.getTime() + diff) + dateSelector.setEndDate(newEnd) + } + + if (!isNaN(recurEnd.recurEndDate.getTime()) && recurEnd.recurEndDate < startDate) { + if (recur.value != CalendarEvent.RecurOnce) { + recurEnd.recurEndDate = startDate + + //: System notification for recurrence end date moved due to user selecting event start date + //: after the earlier value + //% "Recurrence end date moved to event start date" + systemNotification.previewBody = qsTrId("jolla-calendar-recurrence_end_moved_notification") + systemNotification.publish() + } else { + recurEnd.recurEndDate = new Date(NaN) // just clear it, not visible + } + } + } + } + + ValueButton { + id: timezone + property int timespec: Qt.TimeZone + property string name: timeSettings.timezone + function set(spec, zone) { + if (spec == Qt.TimeZone && zone !== undefined) { + name = zone + } else { + name = Qt.binding(function() {return timeSettings.timezone}) + } + timespec = spec + } + visible: opacity > 0. + opacity: allDay.checked ? 0. : 1. + Behavior on opacity { FadeAnimation {} } + //% "Time zone" + label: qsTrId("calendar-choose-timespec") + value: { + switch (timespec) { + //% "None" + case Qt.LocalTime: return qsTrId("calendar-me-clock_time") + //% "Coordinated universal time" + case Qt.UTC: return qsTrId("calendar-me-utc") + //: %1 will be replaced by localized country and %2 with localized city + //% "%1, %2" + case Qt.TimeZone: return qsTrId("calendar-me-localized-timezone").arg(localizer.country).arg(localizer.city) + } + } + onClicked: { + var obj = pageStack.animatorPush("Sailfish.Timezone.TimezonePicker", + {showNoTimezoneOption: true, showUniversalTimeOption: true}) + obj.pageCompleted.connect(function(page) { + page.timezoneClicked.connect(function(zone) { + if (zone == "") { + timezone.set(Qt.LocalTime) + } else if (zone == "UTC") { + timezone.set(Qt.UTC) + } else { + timezone.set(Qt.TimeZone, zone) + } + pageStack.pop() + }) + }) + } + TimezoneLocalizer { + id: localizer + timezone: timezone.name + } + DateTimeSettings { + id: timeSettings + } + } + + Item { + width: 1 + height: Theme.paddingSmall + } + + CalendarSelector { + id: calendar + + // prevent modifying notebook for existing event until qml plugin is fixed to create new uid for event + // ... and always disable for editing single occurrence + enabled: !dialog._isEdit + + //: Shown as placeholder for non-existant notebook, e.g. when default notebook has been deleted + //% "(none)" + name: !notebookQuery.isValid ? qsTrId("calendar-nonexistant_notebook") + : notebookQuery.name + localCalendar: notebookQuery.localCalendar + description: notebookQuery.description + color: notebookQuery.isValid ? notebookQuery.color : "transparent" + accountIcon: notebookQuery.isValid ? notebookQuery.accountIcon : "" + + onClicked: pageStack.animatorPush(calendarPicker, {"selectedCalendarUid": notebookQuery.targetUid}) + } + + TextSwitch { + id: allDay + + //% "All day" + text: qsTrId("calendar-add-all_day") + } + + ComboBox { + id: recur + + property bool showCustom + property int value: currentItem ? currentItem.value : CalendarEvent.RecurOnce + + visible: !dialog._replaceOccurrence && (!dialog.event || !dialog.event.isException) + + //% "Recurring" + label: qsTrId("calendar-add-recurring") + description: value == CalendarEvent.RecurCustom + //% "The recurrence scheme is too complex to be shown." + ? qsTrId("calendar-add-custom-scheme-explanation") : "" + menu: ContextMenu { + MenuItem { + property int value: CalendarEvent.RecurOnce + //% "Once" + text: qsTrId("calendar-add-once") + } + MenuItem { + property int value: CalendarEvent.RecurDaily + //% "Every Day" + text: qsTrId("calendar-add-every_day") + } + MenuItem { + property int value: CalendarEvent.RecurWeeklyByDays + //% "Every Selected Days" + text: qsTrId("calendar-add-every_week_by_days") + } + MenuItem { + property int value: CalendarEvent.RecurWeekly + //% "Every Week" + text: qsTrId("calendar-add-every_week") + } + MenuItem { + property int value: CalendarEvent.RecurBiweekly + //% "Every 2 Weeks" + text: qsTrId("calendar-add-every_2_weeks") + } + MenuItem { + property int value: CalendarEvent.RecurMonthly + //% "Every Month" + text: qsTrId("calendar-add-every_month") + } + MenuItem { + property int value: CalendarEvent.RecurMonthlyByDayOfWeek + text: { + var dayLabel = Format.formatDate(dateSelector.startDate, Format.WeekdayNameStandalone) + var day = dateSelector.startDate.getDate() + if (day < 8) { + //: %1 is replaced with weekday name + //% "First %1 Every Month" + return qsTrId("calendar-add-every_month_by_day_of_week_first").arg(dayLabel) + } else if (day < 15) { + //: %1 is replaced with weekday name + //% "Second %1 Every Month" + return qsTrId("calendar-add-every_month_by_day_of_week_second").arg(dayLabel) + } else if (day < 22) { + //: %1 is replaced with weekday name + //% "Third %1 Every Month" + return qsTrId("calendar-add-every_month_by_day_of_week_third").arg(dayLabel) + } else if (day < 29) { + //: %1 is replaced with weekday name + //% "Fourth %1 Every Month" + return qsTrId("calendar-add-every_month_by_day_of_week_fourth").arg(dayLabel) + } else { + //: %1 is replaced with weekday name + //% "Fifth %1 Every Month" + return qsTrId("calendar-add-every_month_by_day_of_week_fifth").arg(dayLabel) + } + } + } + MenuItem { + property int value: CalendarEvent.RecurMonthlyByLastDayOfWeek + function addDays(date, days) { + var later = new Date(Number(date)) + later.setDate(date.getDate() + days) + return later + } + visible: addDays(dateSelector.startDate, 7).getMonth() != dateSelector.startDate.getMonth() + onVisibleChanged: { + if (!visible && recur.value == value) { + recur.currentIndex = 6 + } + } + text: { + var dayLabel = Format.formatDate(dateSelector.startDate, Format.WeekdayNameStandalone) + //: %1 is replaced with weekday name + //% "Last %1 Every Month" + return qsTrId("calendar-add-every_month_by_day_of_week_last").arg(dayLabel) + } + } + MenuItem { + property int value: CalendarEvent.RecurYearly + //% "Every Year" + text: qsTrId("calendar-add-every_year") + } + MenuItem { + visible: recur.showCustom + property int value: CalendarEvent.RecurCustom + //% "Keep existing scheme" + text: qsTrId("calendar-add-keep-scheme") + } + } + } + + Row { + id: recurringDays + // By default, the day of the event is selected. + property int days: weekModel.model.get((dateSelector.startDate.getDay() + 6) % 7).value + function flipDay(day) { + if (days & day) { + days &= ~day + } else { + days |= day + } + } + + x: Theme.horizontalPageMargin + visible: recur.value == CalendarEvent.RecurWeeklyByDays + + Repeater { + id: weekModel + model: ListModel { + ListElement { value: CalendarEvent.Monday } + ListElement { value: CalendarEvent.Tuesday } + ListElement { value: CalendarEvent.Wednesday } + ListElement { value: CalendarEvent.Thursday } + ListElement { value: CalendarEvent.Friday } + ListElement { value: CalendarEvent.Saturday } + ListElement { value: CalendarEvent.Sunday } + } + MouseArea { + property bool down: (pressed && containsMouse) || (dot.pressed && dot.containsMouse) + + width: (col.width - 2 * Theme.horizontalPageMargin) / 7 + height: childrenRect.height + + onClicked: recurringDays.flipDay(model.value) + + Switch { + id: dot + y: -Theme.paddingLarge + width: parent.width + highlighted: down + automaticCheck: false + checked: recurringDays.days & model.value + down: parent.down + onClicked: recurringDays.flipDay(model.value) + } + Label { + anchors { + horizontalCenter: dot.horizontalCenter + top: dot.bottom + topMargin: -Theme.paddingLarge + } + // 2020 April 20th is a Monday + text: Qt.formatDateTime(new Date(2020, 3, 20 + model.index), "ddd") + font.pixelSize: Theme.fontSizeSmall + color: dot.highlighted ? Theme.highlightColor : Theme.primaryColor + } + } + } + } + + ValueButton { + id: recurEnd + + property date recurEndDate + + visible: recur.value != CalendarEvent.RecurOnce + //: Picker for recurrence end date + //% "Recurrence end" + label: qsTrId("calendar-add-recurrence_end") + value: Qt.formatDate(recurEndDate) + onClicked: { + var defaultDate = recurEnd.recurEndDate + if (isNaN(defaultDate.getTime())) { + defaultDate = dateSelector.endDate + if (recur.value == CalendarEvent.RecurYearly) { + defaultDate.setFullYear(defaultDate.getFullYear() + 1) + } else { + defaultDate.setMonth(defaultDate.getMonth() + 1) + } + } + + var obj = pageStack.animatorPush(recurEndDatePicker, { date: defaultDate }) + obj.pageCompleted.connect(function(dialog) { + dialog.accepted.connect(function() { + recurEnd.recurEndDate = dialog.date + }) + }) + } + } + + ComboBox { + id: reminder + + property int value: currentItem ? currentItem.seconds : -1 + property var dateTime + readonly property bool hasDateTime: dateTime !== undefined && !isNaN(dateTime.getTime()) + property bool followSettings + property bool _applyingSettings + + onFollowSettingsChanged: { + if (followSettings) { + updateFromSettings() + } + } + + onCurrentIndexChanged: { + // modifications stops following settings values + if (!_applyingSettings) + followSettings = false + } + + function updateFromSettings() { + _applyingSettings = true + setFromSeconds(allDay.checked ? reminderAlldayConfig.value + : reminderConfig.value) + _applyingSettings = false + } + + function setFromSeconds(seconds) { + if (seconds < 0 && !hasDateTime) { + currentIndex = 0 // ReminderNone + } else if (seconds < 0 && hasDateTime + && recur.value == CalendarEvent.RecurOnce) { + currentIndex = reminderValues.model.length // A given time + } else if (seconds === 0) { + currentIndex = 1 // ReminderTime + } else { + for (var i = reminderValues.model.length - 1; i >= 2; --i) { + if (seconds == reminderValues.model[i]) { + currentIndex = i + return + } else if (seconds > reminderValues.model[i]) { + var tmp = reminderValues.model + tmp.splice(i + 1, 0, seconds) + reminderValues.model = tmp + currentIndex = i + 1 + return + } + } + } + } + + Connections { + target: allDay + onCheckedChanged: { + if (reminder.followSettings) { + reminder.updateFromSettings() + } + } + } + + //% "Remind me" + label: qsTrId("calendar-add-remind_me") + menu: ContextMenu { + Repeater { + id: reminderValues + readonly property var hourlyModel: [ + -1 // ReminderNone + , 0 // ReminderTime + , 5 * 60 // Reminder5Min + , 15 * 60 // Reminder15Min + , 30 * 60 // Reminder30Min + , 60 * 60 // Reminder1Hour + , 2 * 60 * 60 // Reminder2Hour + , 6 * 60 * 60 // Reminder6Hour + , 12 * 60 * 60 // Reminder12Hour + , 24 * 60 * 60 // Reminder1Day + , 2 * 24 * 60 * 60 // Reminder2Day + ] + readonly property var dailyModel: [ + -1 // ReminderNone + , 16 * 60 * 60 // 8am the day before + , 12 * 60 * 60 // noon the day before + , 6 * 60 * 60 // 6pm the day before + , (16 + 24) * 60 * 60 // 8am two days before + , (12 + 24) * 60 * 60 // noon two days before + , (6 + 24) * 60 * 60 // 6pm two days before + , (12 + 6 * 24) * 60 * 60 // noon the week before + , (12 + 13 * 24) * 60 * 60 // noon two weeks before + ] + model: allDay.checked ? dailyModel : hourlyModel + delegate: ReminderMenuItem { + seconds: modelData + date: allDay.checked ? stripTime(dateSelector.startDate) : undefined + } + } + ReminderMenuItem { + seconds: -2 // Negative value means no relative reminder + text: reminder.hasDateTime + ? //: %1 is replaced by the date in format like Monday 2nd November 2020 + //: %2 is replaced by the time. + //% "%1 at %2" + qsTrId("calendar-item-reminder_date_time") + .arg(Format.formatDate(reminder.dateTime, Format.DateMediumWithoutYear)) + .arg(Format.formatDate(reminder.dateTime, Format.TimeValue)) + //% "Custom reminder" + : qsTrId("calendar-item-reminder_custom") + visible: recur.value == CalendarEvent.RecurOnce + onVisibleChanged: { + if (!visible && reminder.currentIndex == reminderValues.model.length) + reminder.currentIndex = 0 + } + onClicked: { + var dt + if (reminder.hasDateTime) { + dt = reminder.dateTime + } else { + dt = new Date() + if (dt.getMinutes() != 0) { + dt.setMinutes(0) + dt.setHours(dt.getHours() + 1) + } + } + var obj = pageStack.animatorPush("ReminderDateTimeDialog.qml", + {dateTime: dt}) + obj.pageCompleted.connect(function(dtDialog) { + dtDialog.acceptDestinationAction = PageStackAction.Pop + dtDialog.acceptDestination = dialog + dtDialog.accepted.connect(function() { + reminder.dateTime = dtDialog.dateTime + }) + }) + } + } + } + } + } + } + + Component.onCompleted: { + if (event) { + eventName.text = event.displayLabel + eventDescription.text = event.description + eventLocation.text = event.location + + if (!dialog._replaceOccurrence) { + switch (event.recur) { + case CalendarEvent.RecurOnce: recur.currentIndex = 0; break; + case CalendarEvent.RecurDaily: recur.currentIndex = 1; break; + case CalendarEvent.RecurWeeklyByDays: recur.currentIndex = 2; recurringDays.days = event.recurWeeklyDays; break; + case CalendarEvent.RecurWeekly: recur.currentIndex = 3; break; + case CalendarEvent.RecurBiweekly: recur.currentIndex = 4; break; + case CalendarEvent.RecurMonthly: recur.currentIndex = 5; break; + case CalendarEvent.RecurMonthlyByDayOfWeek: recur.currentIndex = 6; break; + case CalendarEvent.RecurMonthlyByLastDayOfWeek: recur.currentIndex = 7; break; + case CalendarEvent.RecurYearly: recur.currentIndex = 8; break; + case CalendarEvent.RecurCustom: recur.currentIndex = 9; recur.showCustom = true; break; + } + } + + reminder.dateTime = event.reminderDateTime + reminder.setFromSeconds(event.reminder) + recurEnd.recurEndDate = event.recurEndDate + + if (dialog._replaceOccurrence) { + dateSelector.setStartDate(dialog.occurrence.startTimeInTz) + dateSelector.setEndDate(dialog.occurrence.endTimeInTz) + } else { + dateSelector.setStartDate(event.startTime) + dateSelector.setEndDate(event.endTime) + } + timezone.set(event.startTimeSpec, event.startTimeZone) + + allDay.checked = event.allDay + } else { + eventName.focus = true + + var date = defaultDate + dateSelector.setStartDate(date) + date.setHours(date.getHours() + 1) + dateSelector.setEndDate(date) + reminder.followSettings = true + } + } + + onAccepted: { + var modification = dialog._isEdit ? Calendar.createModification(dialog.event, dialog.occurrence) + : Calendar.createNewEvent() + if (dialog._isEdit && modification.instanceId == "") { + console.warn("Unable to dissociate event " + dialog.event.instanceId + " at date " + dialog.occurrence.startTime) + return + } + modification.displayLabel = eventName.text + modification.location = eventLocation.text + modification.description = eventDescription.text + modification.recur = recur.value + modification.recurWeeklyDays = recurringDays.days + + if (recur.value == CalendarEvent.RecurOnce) { + modification.unsetRecurEndDate() + } else { + modification.setRecurEndDate(recurEnd.recurEndDate) + } + + modification.reminder = reminder.value + if (reminder.hasDateTime && reminder.value == -2) { + modification.reminderDateTime = reminder.dateTime + } + + if (allDay.checked) { + modification.setStartTime(stripTime(dateSelector.startDate), Qt.LocalTime) + modification.setEndTime(stripTime(dateSelector.endDate), Qt.LocalTime) + modification.allDay = true + } else { + modification.setStartTime(dateSelector.startDate, timezone.timespec, timezone.name) + modification.setEndTime(dateSelector.endDate, timezone.timespec, timezone.name) + modification.allDay = false + } + + modification.calendarUid = notebookQuery.targetUid + + if (dialog.attendeesModified && attendeeButton.enabled) { + modification.setAttendees(requiredAttendees, optionalAttendees) + } + + modification.save() + if (_replaceOccurrence && newInstanceIdCb) { + newInstanceIdCb(modification.instanceId) + } + nameAutoFill.save() + locationAutoFill.save() + + // When new event is created to calendar mark that calendar as default, and save reminder + if (!dialog._isEdit) { + Calendar.defaultNotebook = notebookQuery.targetUid + if (modification.allDay) { + reminderAlldayConfig.value = reminder.value + } else { + reminderConfig.value = reminder.value + } + } + + app.syncHelper.triggerUpdateDelayed(modification.calendarUid) + } +} diff --git a/usr/share/jolla-calendar/pages/EventEditRecurringPage.qml b/usr/share/jolla-calendar/pages/EventEditRecurringPage.qml new file mode 100644 index 00000000..0e086526 --- /dev/null +++ b/usr/share/jolla-calendar/pages/EventEditRecurringPage.qml @@ -0,0 +1,67 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import org.nemomobile.calendar 1.0 + +Page { + id: root + + property QtObject event + property QtObject occurrence + property var newInstanceIdCb + + property bool _smallLandscape: isLandscape && Screen.sizeCategory <= Screen.Medium + + Column { + y: _smallLandscape ? Theme.paddingLarge : Theme.itemSizeExtraLarge + width: parent.width + spacing: _smallLandscape ? Theme.itemSizeExtraSmall : Theme.itemSizeSmall + + Label { + x: Theme.horizontalPageMargin + width: parent.width - 2*Theme.horizontalPageMargin + color: Theme.highlightColor + font.pixelSize: Theme.fontSizeExtraLarge + wrapMode: Text.Wrap + horizontalAlignment: Text.AlignHCenter + //% "This is a recurring event" + text: qsTrId("calendar-event-he-edit_recurring") + } + + Label { + x: Theme.horizontalPageMargin + width: parent.width - 2*Theme.horizontalPageMargin + color: Theme.highlightColor + font.pixelSize: Theme.fontSizeMedium + wrapMode: Text.Wrap + horizontalAlignment: Text.AlignHCenter + //% "Do you want to edit this event or the series" + text: qsTrId("calendar-event-edit_recurring_confirmation") + } + } + + + ButtonLayout { + anchors.bottom: parent.bottom + anchors.bottomMargin: _smallLandscape ? Theme.itemSizeExtraSmall : Theme.itemSizeMedium + preferredWidth: Theme.buttonWidthMedium + + Button { + //% "Edit this event" + text: qsTrId("calendar-event-edit_occurrence") + onClicked: { + pageStack.animatorReplace("EventEditPage.qml", { event: root.event, + occurrence: root.occurrence, + newInstanceIdCb: root.newInstanceIdCb }) + } + } + + Button { + ButtonLayout.newLine: true + //% "Edit the series" + text: qsTrId("calendar-event-edit_all_occurrences") + onClicked: { + pageStack.animatorReplace("EventEditPage.qml", { event: root.event }) + } + } + } +} diff --git a/usr/share/jolla-calendar/pages/EventListSectionDelegate.qml b/usr/share/jolla-calendar/pages/EventListSectionDelegate.qml new file mode 100644 index 00000000..69d57421 --- /dev/null +++ b/usr/share/jolla-calendar/pages/EventListSectionDelegate.qml @@ -0,0 +1,29 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import "Util.js" as Util + +Item { + id: root + + signal clicked + + height: label.height + anchors.right: parent.right + + BackgroundItem { + id: backgroundItem + anchors.centerIn: label + + width: label.width + 2 * Theme.paddingMedium + height: Math.min(label.height + Theme.paddingSmall, Theme.itemSizeExtraSmall) + onClicked: root.clicked() + } + Label { + id: label + anchors.right: parent.right + anchors.rightMargin: Theme.paddingLarge + text: Util.formatDateWeekday(section) + color: backgroundItem.highlighted ? Theme.highlightColor : Theme.primaryColor + font.pixelSize: Theme.fontSizeLarge + } +} diff --git a/usr/share/jolla-calendar/pages/EventViewPage.qml b/usr/share/jolla-calendar/pages/EventViewPage.qml new file mode 100644 index 00000000..cf55098e --- /dev/null +++ b/usr/share/jolla-calendar/pages/EventViewPage.qml @@ -0,0 +1,160 @@ +/**************************************************************************** +** +** Copyright (C) 2015 - 2021 Jolla Ltd. +** Copyright (C) 2021 Open Mobile Platform LLC. +** +****************************************************************************/ +import QtQuick 2.6 +import Sailfish.Silica 1.0 +import Sailfish.Calendar 1.0 +import Sailfish.Share 1.0 +import org.nemomobile.calendar 1.0 +import Calendar.syncHelper 1.0 +import "Util.js" as Util + +Page { + id: root + + property alias instanceId: query.instanceId + property alias startTime: query.startTime + + property Item remorseParent + + function doDelete(action) { + if (root.remorseParent) { + pageStack.pop() + Remorse.itemAction(root.remorseParent, Remorse.deletedText, action) + } else { + Remorse.popupAction(pageStack.previousPage(root), Remorse.deletedText, + function() { action() }) + pageStack.pop() + } + } + + function newInstanceId(instanceId) { + root.instanceId = instanceId + root.startTime = undefined + } + + function iCalendarName(calendarEntry) { + // Return a name for this icalendar that can be used as a filename + + // Remove any whitespace + var noWhitespace = calendarEntry.displayLabel.replace(/\s/g, '') + + // Convert to 7-bit ASCII + var sevenBit = Format.formatText(noWhitespace, Formatter.Ascii7Bit) + if (sevenBit.length < noWhitespace.length) { + // This event's name is not representable in ASCII + sevenBit = "calendarevent" + } + + // Remove any characters that are not part of the portable filename character set + return Format.formatText(sevenBit, Formatter.PortableFilename) + '.ics' + } + + objectName: "EventViewPage" + + EventQuery { id: query } + + SilicaFlickable { + anchors.fill: parent + contentHeight: col.height + Theme.paddingLarge + + PullDownMenu { + visible: query.event && !query.event.readOnly + + MenuItem { + //% "Delete" + text: qsTrId("calendar-event-delete") + onClicked: { + if (query.event.recur != CalendarEvent.RecurOnce) { + pageStack.animatorPush("EventDeletePage.qml", + { event: query.event, + instanceId: query.instanceId, + calendarUid: query.event.calendarUid, + startTime: query.startTime}) + } else { + var instanceId = root.instanceId + var calendarUid = query.event.calendarUid + var remove = Calendar.remove + var helper = app.syncHelper + // no time passed, assuming deleting the event + root.doDelete(function() { + remove(instanceId) + helper.triggerUpdateDelayed(calendarUid) + }) + } + } + } + MenuItem { + //% "Share" + text: qsTrId("calendar-event-share") + onClicked: { + var content = { + "data": query.event.iCalendar(), + "name": root.iCalendarName(query.event), + "type": "text/calendar" + } + shareAction.resources = [content] + shareAction.trigger() + } + ShareAction { + id: shareAction + //% "Share event" + title: qsTrId("jolla-calendar-he-share-event") + } + } + MenuItem { + visible: query.event && !query.event.externalInvitation + //% "Edit" + text: qsTrId("calendar-event-edit") + onClicked: { + if (query.event.recur != CalendarEvent.RecurOnce) { + pageStack.animatorPush("EventEditRecurringPage.qml", { event: query.event, + occurrence: query.occurrence, + newInstanceIdCb: root.newInstanceId }) + } else { + pageStack.animatorPush("EventEditPage.qml", { event: query.event }) + } + } + } + } + + Column { + id: col + + width: parent.width + spacing: Theme.paddingMedium + + PageHeader { + width: parent.width + title: CalendarTexts.ensureEventTitle(query.event ? query.event.displayLabel : "") + wrapMode: Text.Wrap + } + + CalendarEventView { + id: eventDetails + + event: query.event + occurrence: query.occurrence + showHeader: false + + Connections { + target: query + onAttendeesChanged: { + eventDetails.setAttendees(query.attendees) + } + } + } + } + VerticalScrollDecorator {} + + ViewPlaceholder { + id: eventErrorPlaceholder + enabled: query.eventError + //% "Event could not be loaded, it may no longer exist" + text: qsTrId("calendar-la-event_could_not_be_loaded") + } + } +} diff --git a/usr/share/jolla-calendar/pages/FadeEffect.qml b/usr/share/jolla-calendar/pages/FadeEffect.qml new file mode 100644 index 00000000..7ad1521b --- /dev/null +++ b/usr/share/jolla-calendar/pages/FadeEffect.qml @@ -0,0 +1,63 @@ +import QtQuick 2.0 + +ShaderEffect { + id: root + + property ShaderEffectSource source + property int sourceOffset: 0 + property int sourceHeight: source.sourceItem.height + + // 0 - top and bottom, 1 - top, 2 - bottom + property int fadeMode + + mesh: Qt.size(1, (fadeMode == 0)?3:2) + + property real fade: 0.05 + + property real _sourceTextureHeight: source.sourceItem?source.sourceItem.height:1 + property real _sourceOffset: sourceOffset / _sourceTextureHeight + property real _sourceScale: sourceHeight / _sourceTextureHeight + + vertexShader: + "uniform highp mat4 qt_Matrix; + uniform lowp float fade; + uniform lowp float height; + uniform int fadeMode; + uniform lowp float qt_Opacity; + uniform highp float _sourceScale; + uniform highp float _sourceOffset; + attribute highp vec4 qt_Vertex; + attribute highp vec2 qt_MultiTexCoord0; + varying highp vec2 qt_TexCoord0; + varying lowp float opacity; + void main() { + + highp float y = qt_MultiTexCoord0.y; + if (y > 0. && (y < 0.5 || (fadeMode == 1 && y < 1.))) { + y = fade; + opacity = qt_Opacity; + } else if (y < 1. && (y > 0.5 || (fadeMode == 2 && y > 0.))) { + y = 1. - fade; + opacity = qt_Opacity; + } else if (y == 0.) { + if (fadeMode == 2) opacity = qt_Opacity; + else opacity = 0.; + } else { + if (fadeMode == 1) opacity = qt_Opacity; + else opacity = 0.; + } + + qt_TexCoord0 = vec2(qt_MultiTexCoord0.x, _sourceOffset + y * _sourceScale); + gl_Position = qt_Matrix * vec4(qt_Vertex.x, y * height, qt_Vertex.zw); + }" + + fragmentShader: + "varying highp vec2 qt_TexCoord0; + varying lowp float opacity; + uniform sampler2D source; + void main() { + gl_FragColor = texture2D(source, qt_TexCoord0) * opacity; + }" + +} + diff --git a/usr/share/jolla-calendar/pages/FlippingLabel.qml b/usr/share/jolla-calendar/pages/FlippingLabel.qml new file mode 100644 index 00000000..d21c7a67 --- /dev/null +++ b/usr/share/jolla-calendar/pages/FlippingLabel.qml @@ -0,0 +1,60 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 + +Item { + id: root + + property string text + property real fontSize: Theme.fontSizeSmall + property string color: Theme.secondaryHighlightColor + property bool animate: true + + property real _target: 0 + property real _rotation: _target + + Behavior on _rotation { SmoothedAnimation { velocity: 4 } } + + width: label2.label2visible() ? label2.width : label1.width + height: label2.label2visible() ? label2.height : label1.height + + Text { + id: label1 + visible: !label2.visible + color: root.color + font.pixelSize: root.fontSize + + transform: Rotation { + origin { x: label1.width / 2; y: label1.height / 2 } + axis { x: 1; y: 0; z: 0 } + angle: (root._rotation % 2) * 180 + } + } + + Text { + id: label2 + function label2visible() { return r.angle > -90 && r.angle < 90 } + visible: label2visible() + color: root.color + font.pixelSize: root.fontSize + + transform: Rotation { + id: r + origin { x: label2.width / 2; y: label2.height / 2 } + axis { x: 1; y: 0; z: 0 } + angle: -180 * (1 - (root._rotation % 2)) + } + } + + onTextChanged: { + if (animate) { + if (!label2.label2visible()) label2.text = text + else label1.text = text + if (_target - _rotation < 0.5) _target++ + } else { + if (!label2.label2visible()) label1.text = text + else label2.text = text + } + } + + Component.onCompleted: label1.text = root.text +} diff --git a/usr/share/jolla-calendar/pages/ImportEventDate.qml b/usr/share/jolla-calendar/pages/ImportEventDate.qml new file mode 100644 index 00000000..e7e72e18 --- /dev/null +++ b/usr/share/jolla-calendar/pages/ImportEventDate.qml @@ -0,0 +1,46 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 + +Label { + property date startDate + property date endDate + property bool allDay + property bool multiDay: (startDate && endDate) + && (startDate.getFullYear() !== endDate.getFullYear() + || startDate.getMonth() !== endDate.getMonth() + || startDate.getDate() !== endDate.getDate()) + + text: { + var d = startDate + var result + if (d.getFullYear() != (new Date).getFullYear()) { + result = Format.formatDate(d, Format.DateLong) + } else { + //% "d MMMM" + result = Qt.formatDate(d, qsTrId("calendar-date_pattern_date_month")) + } + + if (!allDay) { + result += " " + Format.formatDate(startDate, Formatter.TimeValue) + } + + if (multiDay || !allDay) { + result += " -" + } + + if (multiDay) { + if (d.getFullYear() != (new Date).getFullYear()) { + result += " " + Format.formatDate(d, Format.DateLong) + } else { + //% "d MMMM" + result += " " + Qt.formatDate(d, qsTrId("calendar-date_pattern_date_month")) + } + } + + if (!allDay) { + result += " " + Format.formatDate(endDate, Formatter.TimeValue) + } + + return result + } +} diff --git a/usr/share/jolla-calendar/pages/ImportEventViewPage.qml b/usr/share/jolla-calendar/pages/ImportEventViewPage.qml new file mode 100644 index 00000000..c50befd3 --- /dev/null +++ b/usr/share/jolla-calendar/pages/ImportEventViewPage.qml @@ -0,0 +1,41 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Sailfish.Calendar 1.0 +import org.nemomobile.calendar 1.0 + +Page { + property alias event: eventDetails.event + property alias occurrence: eventDetails.occurrence + + SilicaFlickable { + anchors.fill: parent + contentHeight: column.height + Theme.paddingLarge + + Column { + id: column + + width: parent.width + spacing: Theme.paddingMedium + + PageHeader { + width: parent.width + title: CalendarTexts.ensureEventTitle(eventDetails.event ? eventDetails.event.displayLabel : "") + wrapMode: Text.Wrap + } + + CalendarEventView { + id: eventDetails + showHeader: false + showSelector: false + + onEventChanged: { + if (event) { + setAttendees(event.attendees) + } + } + } + } + + VerticalScrollDecorator {} + } +} diff --git a/usr/share/jolla-calendar/pages/ImportPage.qml b/usr/share/jolla-calendar/pages/ImportPage.qml new file mode 100644 index 00000000..22f7e820 --- /dev/null +++ b/usr/share/jolla-calendar/pages/ImportPage.qml @@ -0,0 +1,230 @@ +/**************************************************************************** +** +** Copyright (C) 2015 - 2019 Jolla Ltd. +** Copyright (C) 2020 Open Mobile Platform LLC. +** +****************************************************************************/ + +import QtQuick 2.0 +import org.nemomobile.calendar 1.0 +import Sailfish.Silica 1.0 +import Sailfish.Calendar 1.0 +import Nemo.Notifications 1.0 as SystemNotifications + +Dialog { + // Set one of fileName or icsString, but not both + property alias fileName: importModel.fileName + property alias icsString: importModel.icsString + property bool _dropInvitation + + width: parent.width + height: parent.height + objectName: "ImportPage" + canAccept: !importModel.error + onAccepted: { + var importSuccess = importModel.save(_dropInvitation) + systemNotification.body = importSuccess + ? //% "Import successful" + qsTrId("jolla-calendar-import-successfull") + : //% "Import failed" + qsTrId("jolla-calendar-import-failed") + systemNotification.publish() + } + + SystemNotifications.Notification { + id: systemNotification + + appIcon: "icon-lock-calendar" + isTransient: true + } + + ImportModel { + id: importModel + targetNotebook: query.targetUid + } + + NotebookQuery { + id: query + + targetUid: Calendar.defaultNotebook + } + + Component { + id: calendarPicker + + CalendarPicker { + hideExcludedCalendars: true + onCalendarClicked: { + query.targetUid = uid + selectedCalendarUid = uid + pageStack.pop() + } + } + } + + DialogHeader { + id: dialogHeader + + acceptText: importModel.hasDuplicates + //% "Overwrite" + ? qsTrId("calendar-ph-event_edit_overwrite") + //% "Import" + : qsTrId("calendar-ph-event_edit_import") + spacing: 0 + } + + SilicaListView { + id: listView + + property string color: query.isValid ? query.color : "transparent" + + anchors { + top: dialogHeader.bottom + left: parent.left + right: parent.right + bottom: parent.bottom + } + clip: true + + header: Item { + width: listView.width + height: (importModel.error ? errorLabel.height : (calendarSelector.height + (dropInvitationSwitch.visible ? dropInvitationSwitch.height : 0))) + Theme.paddingLarge + onHeightChanged: listView.contentY = -height + + CalendarSelector { + id: calendarSelector + + anchors.top: parent.top + anchors.topMargin: Theme.paddingLarge + visible: !importModel.error + //: Shown as placeholder for non-existant notebook, e.g. when default notebook has been deleted + //% "(none)" + name: !query.isValid ? qsTrId("calendar-nonexistant_notebook") + : query.name + localCalendar: query.localCalendar + description: query.isValid ? query.description : "" + color: listView.color + + onClicked: pageStack.animatorPush(calendarPicker, {"selectedCalendarUid": query.targetUid}) + } + TextSwitch { + id: dropInvitationSwitch + + anchors.bottom: parent.bottom + visible: importModel.hasInvitations && !importModel.error + //% "Remove attendees" + text: qsTrId("calendar-drop_invitation") + //% "Invitations with attendees are owned by the organizer and cannot be modified on the device" + description: qsTrId("calendar-detail_external_invitation") + onCheckedChanged: _dropInvitation = checked + } + Label { + id: errorLabel + + visible: importModel.error + anchors.bottom: parent.bottom + text: fileName !== "" + //% "Error importing calendar file: %1" + ? qsTrId("calendar-error_importing_file").arg(fileName) + //% "Error importing calendar data" + : qsTrId("calendar-error_importing_data") + color: Theme.highlightColor + x: Theme.horizontalPageMargin + width: parent.width - 2*x + wrapMode: Text.Wrap + } + } + + model: importModel + + delegate: BackgroundItem { + id: root + + property QtObject event: importModel.getEvent(index) + property QtObject occurrence: event ? event.nextOccurrence() : null + + height: Math.max(Theme.itemSizeSmall, content.height + 2*Theme.paddingSmall) + width: parent.width + + Row { + id: content + x: Theme.paddingMedium + height: column.height + spacing: Theme.paddingMedium + anchors.verticalCenter: parent.verticalCenter + + Rectangle { + id: colorBar + + width: Theme.paddingSmall + radius: Math.round(width/3) + color: listView.color + height: parent.height + } + + Column { + id: column + anchors.verticalCenter: parent.verticalCenter + width: root.width - 3*Theme.paddingMedium - colorBar.width + + ImportEventDate { + startDate: root.occurrence ? root.occurrence.startTime : new Date(-1) + endDate: root.occurrence ? root.occurrence.endTime : new Date(-1) + allDay: root.event && root.event.allDay + width: parent.width + font.pixelSize: Theme.fontSizeLarge + truncationMode: TruncationMode.Fade + color: root.highlighted ? Theme.secondaryHighlightColor : Theme.secondaryColor + } + + Label { + width: parent.width + text: CalendarTexts.ensureEventTitle(root.event ? root.event.displayLabel : "") + font.pixelSize: Theme.fontSizeMedium + truncationMode: TruncationMode.Fade + color: root.highlighted ? Theme.highlightColor : Theme.primaryColor + } + + Row { + height: Math.max(iconWarning.height, textWarning.height) + 2 * Theme.paddingSmall + visible: duplicate || invitation + spacing: Theme.paddingMedium + HighlightImage { + id: iconWarning + anchors.verticalCenter: parent.verticalCenter + highlighted: root.highlighted + source: "image://theme/icon-s-warning" + } + Column { + id: textWarning + anchors.verticalCenter: parent.verticalCenter + width: root.width - iconWarning.width + Label { + visible: duplicate + width: parent.width + //% "Event already exists" + text: qsTrId("calendar-error_importing-duplicate") + wrapMode: Text.Wrap + } + Label { + visible: invitation + width: parent.width + //% "Event is an invitation" + text: qsTrId("calendar-error_importing-invitation") + wrapMode: Text.Wrap + } + } + } + } + } + + onClicked: { + event.color = listView.color + pageStack.animatorPush("ImportEventViewPage.qml", + { "event": event, "occurrence": occurrence }) + } + } + + VerticalScrollDecorator {} + } +} diff --git a/usr/share/jolla-calendar/pages/MonthView.qml b/usr/share/jolla-calendar/pages/MonthView.qml new file mode 100644 index 00000000..4c0f0286 --- /dev/null +++ b/usr/share/jolla-calendar/pages/MonthView.qml @@ -0,0 +1,176 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Sailfish.Silica.private 1.0 as Private +import org.nemomobile.calendar 1.0 +import "Util.js" as Util + +SilicaListView { + id: view + + readonly property date date: headerItem ? headerItem.date : new Date + readonly property string title: Util.capitalize(Format.formatDate(date, Formatter.MonthNameStandalone)) + readonly property string description: { + var now = new Date + return now.getFullYear() != date.getFullYear() ? date.getFullYear() : "" + } + property int _eventX: headerItem && Screen.sizeCategory <= Screen.Medium ? headerItem.x : 0 + property alias flickable: view + + property Item tabHeader + function attachHeader(tabHeader) { + if (tabHeader) { + tabHeader.parent = headerItem + } + view.tabHeader = tabHeader + } + function detachHeader() { + view.tabHeader = null + } + function gotoDate(date) { + if (headerItem) { + headerItem.gotoDate(date) + } + } + + header: Item { + property int _tabHeight: view.tabHeader ? view.tabHeader.height : 0 + property date date: datePicker.date + function gotoDate(date) { + datePicker.date = date + } + + x: isPortrait ? 0 : datePicker.width + width: view.width - x + height: { + var h = _tabHeight + if (isPortrait) { + if (Screen.sizeCategory > Screen.Medium) { + h += Math.max(datePicker.height, additionalInformation.height) + } else { + h += datePicker.height + additionalInformation.height + } + } else { + h += additionalInformation.height + if (Screen.sizeCategory > Screen.Medium) { + h = Math.max(datePicker.height, h) + } + } + return h + Theme.paddingLarge + } + + Connections { + target: tabHeader + onDateClicked: { + var obj = pageStack.animatorPush(yearMonthDialog) + obj.pageCompleted.connect(function(page) { + page.monthActivated.connect(function(month, year) { + var date = datePicker.date + date.setFullYear(year) + date.setMonth(month - 1) + datePicker.date = date + pageStack.pop() + }) + }) + } + } + + Component { + id: yearMonthDialog + Page { + signal monthActivated(int month, int year) + Private.YearMonthMenu { + onMonthActivated: parent.monthActivated(month, year) + } + } + } + + DatePickerPanel { + id: datePicker + anchors.right: isPortrait ? parent.right : parent.left + anchors.top: isPortrait && tabHeader ? tabHeader.bottom : parent.top + width: isPortrait ? view.width : (view.width*0.5) + } + + Binding { + target: agendaModel + property: "startDate" + value: datePicker.date + when: !datePicker.viewMoving + } + + Column { + id: additionalInformation + width: parent.width - Theme.horizontalPageMargin + anchors.top: isPortrait && Screen.sizeCategory <= Screen.Medium + ? datePicker.bottom : tabHeader ? tabHeader.bottom : parent.top + + Label { + visible: Screen.sizeCategory > Screen.Medium + anchors.right: parent.right + font.pixelSize: Theme.fontSizeHuge * 4.5 + renderType: Text.NativeRendering + text: date.getDate() + color: Theme.highlightColor + height: implicitHeight - 2 * Theme.paddingLarge + Label { + anchors.top: parent.top + anchors.right: parent.right + font.pixelSize: Theme.fontSizeHuge + text: Util.capitalize(Format.formatDate(date, Format.WeekdayNameStandalone)) + color: Theme.highlightColor + } + } + + InfoLabel { + font.pixelSize: Theme.fontSizeMedium + color: Theme.highlightColor + text: datePicker.dstIndication + visible: text.length > 0 + horizontalAlignment: Text.AlignLeft + verticalAlignment: Text.AlignVCenter + height: implicitHeight + Theme.paddingLarge + } + } + + // Placeholder + Item { + width: parent.width + height: view.height - y + y: parent.height + + visible: view.count === 0 && !agendaModel.loading + + InfoLabel { + y: parent.height / 3 - height / 2 + //% "Your schedule is free" + text: qsTrId("calendar-me-schedule_is_free") + } + } + } + + model: AgendaModel { + id: agendaModel + property bool loading: true + onStartDateChanged: loading = true + onUpdated: loading = false + } + + delegate: DeletableListDelegate { + // Update activeDay after the contents of agendaModel changes (after the initial update) + // to prevent delegates from recalculating time labels before agendaModel responds to + // changes in datePicker.date + + x: view._eventX + Theme.paddingSmall + width: view.width - x + + Component.onCompleted: activeDay = agendaModel.startDate + + Connections { + target: agendaModel + onUpdated: activeDay = agendaModel.startDate + } + } + + VerticalScrollDecorator {} +} + diff --git a/usr/share/jolla-calendar/pages/ReminderDateTimeDialog.qml b/usr/share/jolla-calendar/pages/ReminderDateTimeDialog.qml new file mode 100644 index 00000000..1b6bfd2f --- /dev/null +++ b/usr/share/jolla-calendar/pages/ReminderDateTimeDialog.qml @@ -0,0 +1,91 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 + +Dialog { + id: root + property var dateTime + + Column { + width: parent.width + + DialogHeader {} + Label { + x: Theme.horizontalPageMargin + width: parent.width - 2 * x + //% "Select the date and time for your custom reminder" + text: qsTrId("calendar-reminder-lbl-date_time") + color: Theme.highlightColor + wrapMode: Text.Wrap + } + Item { + width: parent.width + height: Theme.paddingMedium + } + BackgroundItem { + height: Math.max(Theme.itemSizeMedium, dateLabel.height + 2 * Theme.paddingSmall) + Image { + id: dateIcon + anchors { + left: parent.left + leftMargin: Theme.horizontalPageMargin + verticalCenter: parent.verticalCenter + } + source: "image://theme/icon-m-date" + } + Label { + id: dateLabel + anchors { + left: dateIcon.right + leftMargin: Theme.paddingMedium + right: parent.right + rightMargin: Theme.horizontalPageMargin + verticalCenter: parent.verticalCenter + } + text: Format.formatDate(root.dateTime, Format.DateFull) + wrapMode: Text.Wrap + } + onClicked: { + var obj = pageStack.animatorPush("Sailfish.Silica.DatePickerDialog", + {date: root.dateTime}) + obj.pageCompleted.connect(function(datePicker) { + datePicker.accepted.connect(function() { + root.dateTime = new Date(datePicker.year, datePicker.month - 1, + datePicker.day, root.dateTime.getHours(), + root.dateTime.getMinutes()) + }) + }) + } + } + BackgroundItem { + height: Theme.itemSizeMedium + Image { + id: timeIcon + anchors { + left: parent.left + leftMargin: Theme.horizontalPageMargin + verticalCenter: parent.verticalCenter + } + source: "image://theme/icon-m-clock" + } + Label { + anchors { + left: timeIcon.right + leftMargin: Theme.paddingMedium + verticalCenter: parent.verticalCenter + } + text: Format.formatDate(root.dateTime, Format.TimeValue) + } + onClicked: { + var obj = pageStack.animatorPush("Sailfish.Silica.TimePickerDialog", + {hour: root.dateTime.getHours(), minute: root.dateTime.getMinutes()}) + obj.pageCompleted.connect(function(timePicker) { + timePicker.accepted.connect(function() { + root.dateTime = new Date(root.dateTime.getFullYear(), + root.dateTime.getMonth(), root.dateTime.getDate(), + timePicker.hour, timePicker.minute) + }) + }) + } + } + } +} diff --git a/usr/share/jolla-calendar/pages/ReminderMenuItem.qml b/usr/share/jolla-calendar/pages/ReminderMenuItem.qml new file mode 100644 index 00000000..0fe27fe7 --- /dev/null +++ b/usr/share/jolla-calendar/pages/ReminderMenuItem.qml @@ -0,0 +1,9 @@ +import QtQuick 2.6 +import Sailfish.Silica 1.0 +import Sailfish.Calendar 1.0 + +MenuItem { + property int seconds: -1 // ReminderNone + property var date // When defined, assume reminder should be applied in reference to + text: CalendarTexts.getReminderText(seconds, date) +} diff --git a/usr/share/jolla-calendar/pages/SearchPage.qml b/usr/share/jolla-calendar/pages/SearchPage.qml new file mode 100644 index 00000000..5e967ddf --- /dev/null +++ b/usr/share/jolla-calendar/pages/SearchPage.qml @@ -0,0 +1,78 @@ +import QtQuick 2.6 +import Sailfish.Silica 1.0 +import Sailfish.Calendar 1.0 +import org.nemomobile.calendar 1.0 + +Page { + SilicaListView { + id: view + + anchors.fill: parent + + header: Column { + width: parent.width + PageHeader { + //% "Search" + title: qsTrId("jolla-calendar-he-search") + } + SearchField { + width: parent.width - x + x: Theme.horizontalPageMargin + enabled: !view.model.loading + //% "Search calendars" + placeholderText: qsTrId("jolla-calendar-la-search_notebooks") + EnterKey.onClicked: { + view.model.searchString = text + focus = false + } + onTextChanged: { + if (text.length == 0) { + view.model.searchString = "" + forceActiveFocus() + } + } + Component.onCompleted: forceActiveFocus() + } + } + + model: EventSearchModel { + limit: 200 + } + + section { + property: "year" + delegate: Label { + width: parent.width - Theme.paddingLarge + height: Theme.itemSizeSmall + color: Theme.highlightColor + text: section + horizontalAlignment: Text.AlignRight + verticalAlignment: Text.AlignVCenter + } + } + + delegate: DeletableListDelegate { + width: parent.width + timeText: { + var label = Format.formatDate(model.occurrence.startTime, Formatter.DateMediumWithoutYear) + if (!model.event.allDay) { + label += " " + Format.formatDate(model.occurrence.startTime, Formatter.TimeValue) + } + return label + } + } + + ViewPlaceholder { + enabled: view.model.count == 0 && view.model.searchString.length > 0 && !view.model.loading + //% "No search results" + text: qsTrId("jolla-calendar-la-search_no_result") + } + + VerticalScrollDecorator {} + } + BusyIndicator { + anchors.centerIn: parent + size: BusyIndicatorSize.Large + running: view.model.loading + } +} diff --git a/usr/share/jolla-calendar/pages/SettingsPage.qml b/usr/share/jolla-calendar/pages/SettingsPage.qml new file mode 100644 index 00000000..cf347c65 --- /dev/null +++ b/usr/share/jolla-calendar/pages/SettingsPage.qml @@ -0,0 +1,152 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import org.nemomobile.calendar 1.0 +import Calendar.sortFilterModel 1.0 +import Sailfish.Calendar 1.0 + +Page { + property var excluded: new Array + property bool excludedLoaded + + onStatusChanged: { + if (status == PageStatus.Deactivating) { + saveCalendar() + } + } + + Connections { + target: Qt.application + onActiveChanged: if (!Qt.application.active) saveCalendar() + } + + function saveCalendar() { + loadExcluded() + Calendar.excludedNotebooks = excluded + } + + function loadExcluded() { + if (excludedLoaded) + return + + var a = Calendar.excludedNotebooks + for (var ii = 0; ii < a.length; ++ii) + excluded.push(a[ii]) + + excludedLoaded = true + } + + function isNotebookExcluded(notebook) { + loadExcluded() + + for (var ii = 0; ii < excluded.length; ++ii) { + if (excluded[ii] == notebook) + return true + } + return false + } + + function setExcludeNotebook(notebook, exclude) { + loadExcluded() + + var current = isNotebookExcluded(notebook) + + if (exclude && !current) { + excluded.push(notebook); + } else if (!exclude && current) { + for (var ii = 0; ii < excluded.length; ++ii) { + if (excluded[ii] == notebook) { + excluded.splice(ii, 1) + return + } + } + } + } + + SilicaFlickable { + anchors.fill: parent + contentHeight: column.height + + Column { + id: column + width: parent.width + + PageHeader { + width: parent.width + //% "Settings" + title: qsTrId("calendar-settings-settings") + } + + Repeater { + model: SortFilterModel { + model: NotebookModel { } + sortRole: "name" + } + + delegate: BackgroundItem { + id: backgroundItem + + height: Math.max(calendarDelegate.height + 2*Theme.paddingSmall, Theme.itemSizeMedium) + highlighted: down || enabledSwitch.down + + onClicked: enabledSwitch.checked = !enabledSwitch.checked + + Switch { + id: enabledSwitch + + down: backgroundItem.down || (pressed && containsMouse) + anchors.left: parent.left + anchors.leftMargin: Theme.horizontalPageMargin - Theme.paddingLarge + anchors.verticalCenter: parent.verticalCenter + Component.onCompleted: checked = !isNotebookExcluded(uid) + onCheckedChanged: setExcludeNotebook(uid, !checked) + } + + CalendarSelectorDelegate { + id: calendarDelegate + accountIcon: model.accountIcon + calendarName: localCalendar ? CalendarTexts.getLocalCalendarName() : model.name + calendarDescription: model.description + + anchors.left: enabledSwitch.right + anchors.leftMargin: Theme.paddingMedium + anchors.verticalCenter: parent.verticalCenter + anchors.right: notebookColor.left + anchors.rightMargin: Theme.paddingMedium + } + + Component { + id: colorPicker + ColorPickerPage { + onColorClicked: { + model.color = color + pageStack.pop() + } + } + } + + Rectangle { + id: notebookColor + + opacity: enabledSwitch.checked ? 1.0 : Theme.opacityLow + anchors { + right: parent.right + rightMargin: Theme.horizontalPageMargin + verticalCenter: parent.verticalCenter + } + height: Theme.itemSizeExtraSmall + width: Theme.itemSizeExtraSmall + radius: Theme.paddingSmall/2 + color: model.color + + MouseArea { + enabled: enabledSwitch.checked + anchors { margins: -Theme.paddingLarge; fill: parent } + onClicked: pageStack.animatorPush(colorPicker) + } + } + } + } + } + VerticalScrollDecorator {} + } +} diff --git a/usr/share/jolla-calendar/pages/TabHeader.qml b/usr/share/jolla-calendar/pages/TabHeader.qml new file mode 100644 index 00000000..4a6633b6 --- /dev/null +++ b/usr/share/jolla-calendar/pages/TabHeader.qml @@ -0,0 +1,90 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Sailfish.Silica.private 1.0 + +Item { + id: root + + property date date: new Date + property int currentIndex + property string currentView: tabs.model.get(currentIndex).view + property alias title: titleLabel.text + property alias description: descriptionLabel.text + property bool animated: true + property alias model: tabs.model + + signal dateClicked() + + height: Screen.sizeCategory > Screen.Medium ? Theme.itemSizeLarge : Theme.itemSizeMedium + + Item { + height: parent.height + x: Theme.horizontalPageMargin + Row { + id: tabRow + height: parent.height + Repeater { + id: tabs + SilicaMouseArea { + width: icon.width + Theme.paddingLarge + height: icon.height + anchors.verticalCenter: parent.verticalCenter + HighlightImage { + id: icon + source: model.icon + anchors.centerIn: parent + highlighted: parent.highlighted || root.currentIndex == model.index + } + onClicked: root.currentIndex = model.index + } + } + } + Rectangle { + parent: root.currentIndex < tabs.count ? tabs.itemAt(root.currentIndex) : null + width: parent ? parent.width : 0 + height: Theme._lineWidth + anchors { + bottom: parent ? parent.bottom : undefined + bottomMargin: -Theme.paddingSmall + } + color: Theme.highlightColor + } + } + + BackgroundItem { + id: dateItem + anchors.right: root.right + height: parent.height + width: root.width - Theme.horizontalPageMargin - tabRow.width - Theme.paddingLarge + onClicked: root.dateClicked() + FlippingLabel { + id: titleLabel + animate: root.animated + color: parent.highlighted ? Theme.highlightColor : Theme.primaryColor + fontSize: Screen.sizeCategory > Screen.Medium ? Theme.fontSizeExtraLarge : Theme.fontSizeLarge + transformOrigin: Item.Right + scale: Math.min(1., (dateItem.width - anchors.rightMargin - Theme.paddingLarge) / width) + anchors { + right: parent.right + rightMargin: Theme.horizontalPageMargin + bottom: parent.verticalCenter + bottomMargin: descriptionLabel.text.length > 0 ? -Theme.paddingSmall : -height / 2 + } + Behavior on anchors.bottomMargin { SmoothedAnimation { duration: 1000 } } + } + FlippingLabel { + id: descriptionLabel + animate: root.animated + color: parent.highlighted ? Theme.secondaryHighlightColor : Theme.secondaryColor + fontSize: Screen.sizeCategory > Screen.Medium ? Theme.fontSizeMedium : Theme.fontSizeSmall + transformOrigin: Item.Right + scale: Math.min(1., (dateItem.width - anchors.rightMargin - Theme.paddingLarge) / width) + anchors { + topMargin: Theme.paddingSmall + top: parent.verticalCenter + right: parent.right + rightMargin: Theme.horizontalPageMargin + } + } + } +} diff --git a/usr/share/jolla-calendar/pages/Util.js b/usr/share/jolla-calendar/pages/Util.js new file mode 100644 index 00000000..82164f08 --- /dev/null +++ b/usr/share/jolla-calendar/pages/Util.js @@ -0,0 +1,38 @@ +.pragma library +.import Sailfish.Silica 1.0 as S +.import org.nemomobile.calendar 1.0 as C + +function formatDateWeekday(d) { + var t = new Date + var t2 = new Date(d) + var today = new Date(t.getFullYear(), t.getMonth(), t.getDate()) + var day = new Date(t2.getFullYear(), t2.getMonth(), t2.getDate()) + + var tcol = (t.getDay() + 6) % 7 + var t2col = (t2.getDay() + 6) % 7 + + var delta = (day - today) / 86400000 + + if (delta == 0) { + //% "Today" + return qsTrId("calendar-today") + } else if (delta == -1) { + //% "Yesterday" + return qsTrId("calendar-yesterday") + } else if (delta == 1) { + //% "Tomorrow" + return qsTrId("calendar-tomorrow") + } else if (delta <= -7 || delta >= 7 || + (delta < 0 && t2col > tcol) || + (delta > 0 && tcol > t2col)) { + //: Long date pattern without year. Used e.g. in month view. + //% "d MMMM" + return capitalize(Qt.formatDate(d, qsTrId("calendar-date_pattern_date_month"))) + } else { + return capitalize(S.Format.formatDate(d, S.Format.WeekdayNameStandalone)) + } +} + +function capitalize(string) { + return string.charAt(0).toUpperCase() + string.substr(1) +} diff --git a/usr/share/jolla-calendar/pages/WeekLayout.qml b/usr/share/jolla-calendar/pages/WeekLayout.qml new file mode 100644 index 00000000..839a8701 --- /dev/null +++ b/usr/share/jolla-calendar/pages/WeekLayout.qml @@ -0,0 +1,409 @@ +import QtQuick 2.6 +import Sailfish.Silica 1.0 +import org.nemomobile.calendar 1.0 +import Calendar.hourViewLayouter 1.0 + +Item { + id: root + property int fromHour: 0 + property int toHour: 24 + property string weekText: "week %1" + property string timeFormat: "24" + property int minHourHeight: Math.max(2*fontDef.height, Theme.itemSizeSmall) + property date oneDate: new Date() + property int oneDateShift: -1 + property int highlightedDay: -1 + readonly property var highlightedDate: { + if (highlightedDay >= 0) { + var dt = new Date(_firstDay) + dt.setDate(dt.getDate() + highlightedDay) + return dt + } else { + return undefined + } + } + property alias contentY: content.contentY + property int contentHeight: header.height + days.height + property alias headerHeight: header.height + property real initialContentY: days.oneHourHeight * (8 - fromHour) + property date _firstDay + + signal daySelected(int day) + + onOneDateChanged: { + if (oneDate.getFullYear() < 0) + return + var dt = new Date(oneDate) + dt.setHours(12, 0, 0, 0) + if (dt.getDay() < Qt.locale().firstDayOfWeek) { + oneDateShift = 7 + dt.getDay() - Qt.locale().firstDayOfWeek + } else { + oneDateShift = dt.getDay() - Qt.locale().firstDayOfWeek + } + dt.setDate(dt.getDate() - oneDateShift) + _firstDay = dt + } + + Column { + id: header + spacing: Theme.paddingSmall + x: background.horizontalShift + width: days.width + + Row { + height: daysHeader.count > 0 ? daysHeader.itemAt(0).height : 0 + Repeater { + id: daysHeader + model: 7 + delegate: Column { + id: dayColumn + property date date: { + var dt = new Date(root._firstDay) + dt.setDate(dt.getDate() + modelData) + dt.setHours(12, 0) + return dt + } + property bool isToday: date.getDate() === wallClock.time.getDate() + && date.getMonth() === wallClock.time.getMonth() + && date.getFullYear() === wallClock.time.getFullYear() + width: days.dayWidth + Label { + text: Qt.formatDateTime(dayColumn.date, "ddd") + color: modelData == root.highlightedDay ? Theme.highlightColor : Theme.secondaryHighlightColor + font.pixelSize: Theme.fontSizeExtraSmall + font.bold: isToday + anchors.horizontalCenter: parent.horizontalCenter + } + Label { + text: dayColumn.date.getDate() + color: modelData == root.highlightedDay ? Theme.highlightColor : Theme.secondaryHighlightColor + font.pixelSize: Theme.fontSizeSmall + font.bold: isToday + anchors.horizontalCenter: parent.horizontalCenter + } + } + } + } + + Item { + id: fullDays + property int maxConcurrentFullDays: height / Theme.paddingMedium + property int nConcurrentFullDays: 1 + property real itemHeight: height / nConcurrentFullDays + property var lastFreeDay: { + var arr = new Array(maxConcurrentFullDays) + reset(arr) + return arr + } + function reset(arr) { + if (arr) { + for (var i = 0; i < arr.length; i++) { + arr[i] = -1 + } + } + } + function relayout() { + reset(fullDays.lastFreeDay) + var n = 0 + for (var it = 0; it < fullDaysModel.count; it++) { + var item = fullDaysItems.itemAt(it) + item.lane = -1 + for (var i = 0; i < fullDays.lastFreeDay.length; i++) { + if (fullDays.lastFreeDay[i] <= item.startDay) { + fullDays.lastFreeDay[i] = item.startDay + item.duration + item.lane = i + n = Math.max(n, i) + break + } + } + if (item.x < 0) console.warn("not enough space to accomodate full day.") + } + nConcurrentFullDays = n + 1 + } + // Ensure we have at least two concurrent full day event with text + height: Math.max(Theme.itemSizeSmall, + 2 * (Theme.paddingSmall + fullDaysItems.barHeight + fontDef.height)) + width: parent.width + Repeater { + id: fullDaysItems + property int barHeight: Theme.paddingSmall + model: AgendaModel { + id: fullDaysModel + startDate: root._firstDay + endDate: QtDate.addDays(root._firstDay, 6) + filterMode: AgendaModel.FilterNonAllDay + onUpdated: fullDays.relayout() + } + delegate: BackgroundItem { + property int startDay: { + var st = Math.max(fullDaysModel.startDate.getTime(), + model.occurrence.startTime.getTime()) + return (st - fullDaysModel.startDate.getTime()) / 86400000 + } + property int duration: { + var st = Math.max(fullDaysModel.startDate.getTime(), + model.occurrence.startTime.getTime()) + var et = Math.min(fullDaysModel.endDate.getTime(), + model.occurrence.endTime.getTime()) + return (et - st) / 86400000 + 1 + } + property int lane: -1 + + x: days.dayWidth * startDay + y: lane * fullDays.itemHeight + width: days.dayWidth * duration + height: fullDays.itemHeight + visible: lane >= 0 && duration > 0 + Rectangle { + id: fullDayBar + color: model.event.color + y: Math.round(0.5 * Theme.paddingSmall) + x: Theme.paddingSmall + width: parent.width - 2 * Theme.paddingSmall + height: fullDaysItems.barHeight + radius: height / 3 + } + Label { + id: fullDayLabel + width: parent.width + visible: height >= fontDef.height + y: fullDayBar.y + fullDayBar.height + height: parent.height - y + wrapMode: Text.Wrap + clip: true + text: model.event.displayLabel + font.pixelSize: textRef.font.pixelSize + font.strikeout: model.event.status == CalendarEvent.StatusCancelled + } + OpacityRampEffect { + enabled: fullDayLabel.implicitHeight > fullDayLabel.height + direction: OpacityRamp.TopToBottom + sourceItem: fullDayLabel + slope: Math.max(1, fullDayLabel.height / Theme.paddingLarge) + offset: 1 - 1 / slope + } + onClicked: { + pageStack.animatorPush("EventViewPage.qml", + { instanceId: model.event.instanceId, + startTime: model.occurrence.startTime, + 'remorseParent': root + }) + } + } + } + } + } + + Item { + id: content + width: parent.width + height: parent.height - header.height + + property real contentY: root.initialContentY + anchors.top: header.bottom + clip: true + + Item { + id: background + property real sidePanelPadding: Theme.paddingSmall + property real horizontalShift: Theme.paddingSmall + hourRef.width + sidePanelPadding + width: parent.width + y: Math.max(-content.contentY, root.height - root.contentHeight) + + Repeater { + model: root.toHour - root.fromHour + delegate: Item { + Rectangle { + id: hourRectangle + y: modelData * days.oneHourHeight + width: background.width + height: days.oneHourHeight + color: Theme.primaryColor + opacity: 0.05 + visible: modelData & 1 + } + Label { + width: hourRef.width + text: { + var dt = new Date + dt.setHours(root.fromHour + modelData, 0) + if (root.timeFormat === "24") { + return Format.formatDate(dt, Format.TimeValueTwentyFourHours) + } else { + return Format.formatDate(dt, Format.TimeValueTwelveHours) + } + } + anchors { + left: hourRectangle.left + leftMargin: Theme.paddingSmall + top: hourRectangle.top + } + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + font.pixelSize: Theme.fontSizeSmall + color: Theme.highlightColor + opacity: modelData & 1 ? Theme.opacityHigh : Theme.opacityLow + } + } + } + TextMetrics { + id: hourRef + text: { + var dt = new Date + dt.setHours(23, 0) + return Format.formatDate(dt, Format.TimeValueTwentyFourHours) + } + font.pixelSize: Theme.fontSizeSmall + } + Rectangle { + width: parent.width + height: Theme.paddingSmall / 2 + color: Theme.secondaryHighlightColor + opacity: 0.5 + x: background.horizontalShift + y: Math.max(0, days.oneHourHeight * (wallClock.time.getHours() + + wallClock.time.getMinutes() / 60 - root.fromHour) - height / 2) + visible: dayItems.count > 0 + && wallClock.time.getHours() >= root.fromHour + && wallClock.time.getHours() < root.toHour + && wallClock.time >= dayItems.itemAt(0).fromDate + && wallClock.time < dayItems.itemAt(6).toDate + } + } + + Row { + id: days + property real dayWidth: width / 7 + property real oneHourHeight: Math.max(root.minHourHeight, (root.height - content.y) / (toHour - fromHour)) + x: background.horizontalShift + y: background.y + width: parent.width - x + height: (root.toHour - root.fromHour) * oneHourHeight + + Repeater { + id: dayItems + model: 7 + delegate: Item { + id: agenda + property date fromDate: { + var dt = new Date(root._firstDay) + dt.setDate(dt.getDate() + modelData) + dt.setHours(root.fromHour, 0) + return dt + } + property date toDate: { + var dt = new Date(fromDate) + dt.setHours(root.toHour, 59, 59) + return dt + } + property bool isToday: fromDate.getDate() === wallClock.time.getDate() + && fromDate.getMonth() === wallClock.time.getMonth() + && fromDate.getFullYear() === wallClock.time.getFullYear() + width: days.dayWidth + height: (root.toHour - root.fromHour) * days.oneHourHeight + + MouseArea { + anchors.fill: parent + onClicked: root.daySelected(modelData) + } + + Rectangle { + height: parent.height + width: Math.round(Theme.pixelRatio) + color: Theme.secondaryHighlightColor + visible: modelData > 0 + } + + Rectangle { + width: days.dayWidth - 2 * Theme.paddingSmall + height: Theme.paddingSmall / 2 + color: Theme.highlightColor + radius: height / 2 + visible: agenda.isToday + y: Math.max(0, days.oneHourHeight * (wallClock.time.getHours() + wallClock.time.getMinutes() / 60 - root.fromHour) - height / 2) + x: Theme.paddingSmall + z: 0 + } + + HourViewLayouter { + model: AgendaModel { + filterMode: AgendaModel.FilterAllDay + startDate: agenda.fromDate + } + width: agenda.width + height: agenda.height + cellHeight: days.oneHourHeight / 2 + delegate: eventDelegate + overlapDelegate: overflowDelegate + delegateParent: agenda + startDate: agenda.fromDate + currentDate: agenda.fromDate + maximumConcurrency: Math.max(2, days.dayWidth / (3 * Theme.paddingSmall + textRef.width)) + } + } + } + } + } + + Label { + id: weekLabel + property date thursday: { + var date = new Date(root._firstDay) + date.setHours(0, 0, 0, 0) + // Thursday in current week decides the year. + date.setDate(date.getDate() + 3 - (date.getDay() + 6) % 7) + return date + } + anchors { + top: content.top + topMargin: days.oneHourHeight - height / 2 + right: content.left + rightMargin: Theme.paddingSmall + } + text: { + // Source: https://weeknumber.com/how-to/javascript + // January 4 is always in week 1. + var week1 = new Date(thursday.getFullYear(), 0, 4) + // Adjust to Thursday in week 1 and count number of weeks from date to week1. + var weekId = 1 + Math.round(((thursday.getTime() - week1.getTime()) / 86400000 + - 3 + (week1.getDay() + 6) % 7) / 7) + return root.weekText.arg(weekId) + } + color: Theme.secondaryHighlightColor + } + Label { + anchors { + top: content.top + topMargin: 3 * days.oneHourHeight - height / 2 + right: weekLabel.right + } + text: weekLabel.thursday.getFullYear() + color: Theme.secondaryHighlightColor + } + + TextMetrics { + id: textRef + text: "m" + font.pixelSize: Theme.fontSizeExtraSmall + } + FontMetrics { + id: fontDef + font.pixelSize: Theme.fontSizeExtraSmall + } + Component { + id: eventDelegate + DayPageEventDelegate { + fontSize: Theme.fontSizeExtraSmall + oneLiner: false + onClicked: root.daySelected((7 + date.getDay() - Qt.locale().firstDayOfWeek) % 7) + } + } + Component { + id: overflowDelegate + DayPageOverlapDelegate { + fontSize: Theme.fontSizeExtraSmall + oneLiner: false + onClicked: root.daySelected((7 + date.getDay() - Qt.locale().firstDayOfWeek) % 7) + } + } +} diff --git a/usr/share/jolla-calendar/pages/WeekPanel.qml b/usr/share/jolla-calendar/pages/WeekPanel.qml new file mode 100644 index 00000000..b403736b --- /dev/null +++ b/usr/share/jolla-calendar/pages/WeekPanel.qml @@ -0,0 +1,107 @@ +import QtQuick 2.6 +import Sailfish.Silica 1.0 +import Nemo.Time 1.0 +import org.nemomobile.calendar 1.0 +import Nemo.Configuration 1.0 + +SlideshowView { + id: view + property date date: new Date() + property var currentDate: date + property int highlightedDay: -1 + property real contentY + property int contentHeight + property int headerHeight + property real initialContentY + + onCurrentItemChanged: { + if (currentItem) { + if (currentItem.highlightedDate !== undefined) { + view.currentDate = currentItem.highlightedDate + } + contentHeight = currentItem.contentHeight + headerHeight = currentItem.headerHeight + initialContentY = currentItem.initialContentY + } + } + Connections { + target: currentItem + onHighlightedDateChanged: { + if (currentItem.highlightedDate !== undefined) { + view.currentDate = currentItem.highlightedDate + } + } + } + + readonly property date refDate: { // This will correspond to model / 2, see weekToDate() + var dt = new Date() + dt.setHours(0, 0, 0, 0) + var oneDateShift + if (dt.getDay() < Qt.locale().firstDayOfWeek) { + oneDateShift = 7 + dt.getDay() - Qt.locale().firstDayOfWeek + } else { + oneDateShift = dt.getDay() - Qt.locale().firstDayOfWeek + } + dt.setDate(dt.getDate() - oneDateShift) + return dt + } + onDateChanged: { + currentIndex = dateToWeek(date) + var id = 6 + var dt = QtDate.addDays(weekToDate(currentIndex), 6) + while (date < dt) { + id -= 1 + dt.setDate(dt.getDate() - 1) + } + highlightedDay = id + } + + clip: true + itemWidth: width + Theme.paddingSmall + weekMetric.width + Theme.paddingLarge + itemHeight: height + cacheItemCount: 3 + + WallClock { + id: wallClock + updateFrequency: WallClock.Minute + enabled: Qt.application.active + } + + TextMetrics { + id: weekMetric + //% "week %1" + property string label: qsTrId("calendar-lbl-weekview_week_number") + text: label.arg(56) + font.pixelSize: Theme.fontSizeMedium + } + + model: 10000 + currentIndex: dateToWeek(date) + function weekToDate(weekId) { + var dt = new Date(refDate) + dt.setDate(dt.getDate() + 7 * (weekId - model / 2)) + return dt + } + function dateToWeek(dt) { + return model / 2 + QtDate.daysTo(refDate, dt) / 7 + } + + delegate: WeekLayout { + readonly property bool active: PathView.isCurrentItem + oneDate: weekToDate(model.index) + onDaySelected: view.highlightedDay = day + width: view.width + height: view.height + weekText: weekMetric.label + timeFormat: timeFormatConfig.value + highlightedDay: view.highlightedDay + Binding on contentY { + when: active || moving + value: view.contentY + } + } + ConfigurationValue { + id: timeFormatConfig + key: "/sailfish/i18n/lc_timeformat24h" + } +} diff --git a/usr/share/jolla-calendar/pages/WeekView.qml b/usr/share/jolla-calendar/pages/WeekView.qml new file mode 100644 index 00000000..c94280bb --- /dev/null +++ b/usr/share/jolla-calendar/pages/WeekView.qml @@ -0,0 +1,98 @@ +import QtQuick 2.6 +import Sailfish.Silica 1.0 +import "Util.js" as Util + +Item { + id: root + + readonly property alias date: weekPanel.currentDate + readonly property string title: Util.capitalize(Format.formatDate(weekPanel.currentDate, Formatter.MonthNameStandalone)) + readonly property string description: { + var now = new Date + return now.getFullYear() != weekPanel.currentDate.getFullYear() ? weekPanel.currentDate.getFullYear() : "" + } + property alias flickable: flickable + + property Item tabHeader + function attachHeader(tabHeader) { + if (tabHeader) { + tabHeader.parent = tabPlaceholder + } + root.tabHeader = tabHeader + } + function detachHeader() { + root.tabHeader = null + } + function gotoDate(date) { + weekPanel.date = date + } + + SilicaFlickable { + id: flickable + + anchors.fill: parent + + contentWidth: width + contentHeight: tabPlaceholder.height + weekPanel.contentHeight + contentY: weekPanel.initialContentY + quickScroll: false + + property real pullDownMenuOrigin + MouseArea { + id: headerArea + property bool within + parent: flickable + anchors.fill: parent + z: 100 + onPressed: { + within = mouse.y < tabPlaceholder.height + weekPanel.headerHeight + mouse.accepted = false + } + } + onDraggingVerticallyChanged: { + if (draggingVertically && headerArea.within) { + pullDownMenuOrigin = weekPanel.contentY + } + } + Connections { + target: pullDownMenu + onActiveChanged: if (!pullDownMenu.active && !flickable.dragging) flickable.pullDownMenuOrigin = 0 + } + onMovementEnded: if (pullDownMenu && !pullDownMenu.active) pullDownMenuOrigin = 0 + onTopMarginChanged: topMargin = pullDownMenu && pullDownMenu.active ? pullDownMenu.height - flickable.pullDownMenuOrigin : 0 + + Column { + id: content + width: parent.width + y: Math.max(flickable.contentY, flickable.pullDownMenuOrigin) + Item { + id: tabPlaceholder + width: isPortrait ? parent.width : (parent.width / 2) + x: isPortrait ? 0 : (parent.width / 2) + height: (root.tabHeader ? root.tabHeader.height : 0) + } + + Connections { + target: tabHeader + onDateClicked: { + var obj = pageStack.animatorPush("Sailfish.Silica.DatePickerDialog") + obj.pageCompleted.connect(function(page) { + page.accepted.connect(function() { + weekPanel.date = page.selectedDate + }) + }) + } + } + + WeekPanel { + id: weekPanel + width: parent.width + height: root.height - tabPlaceholder.height + Binding on contentY { + when: !flickable.pullDownMenu || (!flickable.pullDownMenu.active && flickable.pullDownMenuOrigin == 0) + value: flickable.contentY + } + } + } + } +} diff --git a/usr/share/jolla-camera/cover/CameraCover.qml b/usr/share/jolla-camera/cover/CameraCover.qml index 64246fe7..0263951d 100644 --- a/usr/share/jolla-camera/cover/CameraCover.qml +++ b/usr/share/jolla-camera/cover/CameraCover.qml @@ -2,7 +2,7 @@ import QtQuick 2.4 import QtMultimedia 5.0 import Sailfish.Silica 1.0 import com.jolla.camera 1.0 -import org.nemomobile.thumbnailer 1.0 +import Nemo.Thumbnailer 1.0 CoverBackground { id: cover diff --git a/usr/share/jolla-camera/pages/MainCameraPage.qml b/usr/share/jolla-camera/pages/MainCameraPage.qml index ccf7c77c..516611c4 100644 --- a/usr/share/jolla-camera/pages/MainCameraPage.qml +++ b/usr/share/jolla-camera/pages/MainCameraPage.qml @@ -1,7 +1,7 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 import com.jolla.camera 1.0 -import org.nemomobile.dbus 2.0 +import Nemo.DBus 2.0 CameraPage { id: page diff --git a/usr/share/jolla-clock/clock.qml b/usr/share/jolla-clock/clock.qml index 6df9990a..2d2a4d2b 100644 --- a/usr/share/jolla-clock/clock.qml +++ b/usr/share/jolla-clock/clock.qml @@ -7,8 +7,8 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 -import org.nemomobile.alarms 1.0 -import org.nemomobile.dbus 2.0 +import Nemo.Alarms 1.0 +import Nemo.DBus 2.0 import "pages" import "cover" diff --git a/usr/share/jolla-clock/common/ClockItem.qml b/usr/share/jolla-clock/common/ClockItem.qml index 91541797..c5fed08d 100644 --- a/usr/share/jolla-clock/common/ClockItem.qml +++ b/usr/share/jolla-clock/common/ClockItem.qml @@ -1,7 +1,7 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 -import org.nemomobile.time 1.0 -import org.nemomobile.configuration 1.0 +import Nemo.Time 1.0 +import Nemo.Configuration 1.0 Row { id: root diff --git a/usr/share/jolla-clock/common/DateUtils.js b/usr/share/jolla-clock/common/DateUtils.js index 5f052c5f..0b8eef8f 100644 --- a/usr/share/jolla-clock/common/DateUtils.js +++ b/usr/share/jolla-clock/common/DateUtils.js @@ -56,15 +56,15 @@ function daysTo(hour, minute, weekdays, currentDate) { } function days(time) { - return Math.floor(time/oneday) + return Math.floor(time / oneday) } function hours(time) { - return Math.floor((time % oneday)/onehour) + return Math.floor((time % oneday) / onehour) } function minutes(time) { - return Math.round((time % onehour)/oneminute) + return Math.round((time % onehour) / oneminute) } function formatDuration(duration) { diff --git a/usr/share/jolla-clock/common/TimerClock.qml b/usr/share/jolla-clock/common/TimerClock.qml index 6b3a240d..9fa9e221 100644 --- a/usr/share/jolla-clock/common/TimerClock.qml +++ b/usr/share/jolla-clock/common/TimerClock.qml @@ -1,5 +1,5 @@ import QtQuick 2.0 -import org.nemomobile.time 1.0 +import Nemo.Time 1.0 WallClock { id: timer diff --git a/usr/share/jolla-clock/cover/ClockCover.qml b/usr/share/jolla-clock/cover/ClockCover.qml index 32bf86c1..be38c0f9 100644 --- a/usr/share/jolla-clock/cover/ClockCover.qml +++ b/usr/share/jolla-clock/cover/ClockCover.qml @@ -1,8 +1,8 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 import Sailfish.Timezone 1.0 -import org.nemomobile.time 1.0 -import org.nemomobile.alarms 1.0 +import Nemo.Time 1.0 +import Nemo.Alarms 1.0 import com.jolla.clock.private 1.0 import "../common" @@ -66,7 +66,7 @@ CoverBackground { onLayoutDataChanged: _calculatePaddings() - // TODO: only display enabled alarms once org.nemomobile.time alarm model supports filtering + // TODO: only display enabled alarms once Nemo.Time alarm model supports filtering model: enabledAlarmsModel maximumCount: _stopwatchMode ? 0 : (_maximumItems - timersView.visualCount) diff --git a/usr/share/jolla-clock/pages/AlarmView.qml b/usr/share/jolla-clock/pages/AlarmView.qml index f244f87a..33b8459b 100644 --- a/usr/share/jolla-clock/pages/AlarmView.qml +++ b/usr/share/jolla-clock/pages/AlarmView.qml @@ -1,9 +1,9 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 import Sailfish.Silica.private 1.0 -import org.nemomobile.alarms 1.0 -import org.nemomobile.time 1.0 -import org.nemomobile.configuration 1.0 +import Nemo.Alarms 1.0 +import Nemo.Time 1.0 +import Nemo.Configuration 1.0 import "main" TabItem { diff --git a/usr/share/jolla-clock/pages/MainPage.qml b/usr/share/jolla-clock/pages/MainPage.qml index 77289741..f25c0521 100644 --- a/usr/share/jolla-clock/pages/MainPage.qml +++ b/usr/share/jolla-clock/pages/MainPage.qml @@ -63,8 +63,7 @@ Page { if (days > 0) { //: E.g. Expiring in 2 days, 3 hours and 1 minute, time measurements are localized separately //% "Expiring in %0, %1 and %2" - text = - qsTrId("clock-la-expiring_in_days_hours_minutes").arg(daysText).arg(hoursText).arg(minutesText) + text = qsTrId("clock-la-expiring_in_days_hours_minutes").arg(daysText).arg(hoursText).arg(minutesText) } else if (hours > 0) { //: E.g. Expiring in 1 hour and 13 minutes, time measurements are localized separately //% "Expiring in %0 and %1" diff --git a/usr/share/jolla-clock/pages/TimerView.qml b/usr/share/jolla-clock/pages/TimerView.qml index 898c11c1..d788648b 100644 --- a/usr/share/jolla-clock/pages/TimerView.qml +++ b/usr/share/jolla-clock/pages/TimerView.qml @@ -1,10 +1,10 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 import Sailfish.Silica.private 1.0 -import org.nemomobile.alarms 1.0 -import org.nemomobile.notifications 1.0 -import org.nemomobile.time 1.0 -import org.nemomobile.configuration 1.0 +import Nemo.Alarms 1.0 +import Nemo.Notifications 1.0 +import Nemo.Time 1.0 +import Nemo.Configuration 1.0 import "main" TabItem { diff --git a/usr/share/jolla-clock/pages/main/Clock.qml b/usr/share/jolla-clock/pages/main/Clock.qml index c5751ea3..2b9f69a1 100644 --- a/usr/share/jolla-clock/pages/main/Clock.qml +++ b/usr/share/jolla-clock/pages/main/Clock.qml @@ -1,6 +1,6 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 -import org.nemomobile.time 1.0 +import Nemo.Time 1.0 import "../../common" ClockItem { diff --git a/usr/share/jolla-contacts/pages/ContactImportWizardPage.qml b/usr/share/jolla-contacts/pages/ContactImportWizardPage.qml index 692f995f..b2d52203 100644 --- a/usr/share/jolla-contacts/pages/ContactImportWizardPage.qml +++ b/usr/share/jolla-contacts/pages/ContactImportWizardPage.qml @@ -1,10 +1,9 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 import Sailfish.Accounts 1.0 -import org.nemomobile.configuration 1.0 +import Nemo.Configuration 1.0 import org.nemomobile.contacts 1.0 import com.jolla.settings.accounts 1.0 -import com.jolla.signonuiservice 1.0 Dialog { id: root @@ -22,7 +21,6 @@ Dialog { } function createAccount(providerName) { - jolla_signon_ui_service.inProcessParent = root accountCreator.endDestination = pageStack.currentPage accountCreator.endDestinationAction = PageStackAction.Pop accountCreator.startAccountCreationForProvider(providerName, {}, PageStackAction.Push) @@ -136,12 +134,6 @@ Dialog { AccountCreationManager { id: accountCreator } - SignonUiService { - // Note: this ID is required to have this name: - id: jolla_signon_ui_service - inProcessServiceName: "com.jolla.people" - inProcessObjectPath: "/JollaPeopleSignonUi" - } Component { id: importFromServices diff --git a/usr/share/jolla-email/cover/CoverLabel.qml b/usr/share/jolla-email/cover/CoverLabel.qml new file mode 100644 index 00000000..191eaf7d --- /dev/null +++ b/usr/share/jolla-email/cover/CoverLabel.qml @@ -0,0 +1,16 @@ +/* + * Copyright (c) 2013 – 2019 Jolla Ltd. + * + * License: Proprietary + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 + +Label { + x: Theme.paddingLarge + width: parent.width - Theme.paddingLarge*2 + color: Theme.highlightColor + elide: Text.ElideRight + wrapMode: Text.Wrap +} diff --git a/usr/share/jolla-email/cover/EmailCover.qml b/usr/share/jolla-email/cover/EmailCover.qml new file mode 100644 index 00000000..754f47b4 --- /dev/null +++ b/usr/share/jolla-email/cover/EmailCover.qml @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2013 – 2019 Jolla Ltd. + * + * License: Proprietary + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 + +CoverBackground { + id: emailCover + + Image { + visible: app.numberOfAccounts > 0 && !app.accountsManagerActive + anchors.horizontalCenter: parent.horizontalCenter + width: parent.width + height: sourceSize.height * width / sourceSize.width + source: "image://theme/graphic-cover-email-background" + opacity: 0.1 + } + + CoverPlaceholder { + id: placeholder + + //% "Create account" + text: qsTrId("email-la-create_account") + icon.source: "image://theme/icon-launcher-email" + visible: app.numberOfAccounts === 0 || app.accountsManagerActive + } + + Loader { + id: coverLoader + anchors.fill: parent + asynchronous: true + source: { + if (placeholder.visible) + return "" + + switch (app.coverMode) { + case "mainView": + return "MainViewCover.qml" + case "mailViewer": + return "MailViewerCover.qml" + case "mailEditor": + return "MailEditorCover.qml" + default: + console.warn("Invalid cover mode", app.coverMode) + return "" + } + } + + onStatusChanged: { + if (status == Loader.Error && sourceComponent) { + console.log(sourceComponent.errorString()) + } + } + } +} diff --git a/usr/share/jolla-email/cover/MailEditorCover.qml b/usr/share/jolla-email/cover/MailEditorCover.qml new file mode 100644 index 00000000..0e2a2808 --- /dev/null +++ b/usr/share/jolla-email/cover/MailEditorCover.qml @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2013 – 2019 Jolla Ltd. + * + * License: Proprietary + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 + +Item { + anchors.fill: parent + + CoverLabel { + id: toLabel + + y: Theme.paddingLarge + //: 'To: ' recipient cover label + //% "To: %1" + text: qsTrId("jolla-email-la-to_cover").arg(app.editorTo) + font.pixelSize: Theme.fontSizeSmall + maximumLineCount: 2 + } + CoverLabel { + property int lineHeight: toLabel.height/toLabel.lineCount + + text: app.editorBody + color: Theme.primaryColor + font.pixelSize: Theme.fontSizeSmall + maximumLineCount: Math.round(height/lineHeight) + anchors { + top: toLabel.bottom + bottom: parent.bottom + topMargin: Theme.paddingLarge + bottomMargin: Theme.paddingMedium + } + } +} diff --git a/usr/share/jolla-email/cover/MailViewerCover.qml b/usr/share/jolla-email/cover/MailViewerCover.qml new file mode 100644 index 00000000..d2b45abc --- /dev/null +++ b/usr/share/jolla-email/cover/MailViewerCover.qml @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2013 – 2019 Jolla Ltd. + * + * License: Proprietary + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 + +Item { + anchors.fill: parent + + CoverLabel { + anchors { + top: parent.top + bottom: senderLabel.top + topMargin: Theme.paddingLarge + bottomMargin: Theme.paddingMedium + } + text: "\"" + app.viewerSubject + "\"" + color: Theme.primaryColor + maximumLineCount: Math.round(height/lineHeight) + property int lineHeight: senderLabel.height/senderLabel.lineCount + } + + CoverLabel { + id: senderLabel + text: app.viewerSender + maximumLineCount: 2 + anchors { bottom: parent.bottom; bottomMargin: Theme.paddingLarge } + } +} diff --git a/usr/share/jolla-email/cover/MainViewCover.qml b/usr/share/jolla-email/cover/MainViewCover.qml new file mode 100644 index 00000000..d4420a14 --- /dev/null +++ b/usr/share/jolla-email/cover/MainViewCover.qml @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2013 – 2019 Jolla Ltd. + * + * License: Proprietary + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 + +Item { + id: root + + property int unreadMailCount: app.numberOfAccounts === 1 ? app.inboxUnreadCount + : app.combinedInboxUnreadCount + + anchors.fill: parent + + Behavior on opacity { FadeAnimation { duration: 500 } } + Label { + id: unreadCount + text: unreadMailCount + x: Theme.paddingLarge + y: Theme.paddingMedium + visible: !app.syncInProgress + font.pixelSize: Theme.fontSizeHuge + } + Label { + id: unreadLabel + + //: Unread label. Code requires exact line break tag "
". + //% "Unread
email(s)" + text: qsTrId("jolla-email-la-unread-emails", unreadMailCount).replace("
", "\n") + font.pixelSize: Theme.fontSizeExtraSmall + visible: !app.syncInProgress + maximumLineCount: 2 + wrapMode: Text.Wrap + fontSizeMode: Text.HorizontalFit + lineHeight: 0.8 + height: implicitHeight/0.8 + verticalAlignment: Text.AlignVCenter + anchors { + right: parent.right + left: unreadCount.right + leftMargin: Theme.paddingMedium + baseline: unreadCount.baseline + baselineOffset: lineCount > 1 ? -implicitHeight/2 : -(height-implicitHeight)/2 + } + } + OpacityRampEffect { + offset: 0.5 + sourceItem: unreadLabel + enabled: unreadLabel.implicitWidth > Math.ceil(unreadLabel.width) + } + + CoverLabel { + id: statusLabel + + //: Updating label + //% "Updating..." + text: app.syncInProgress ? qsTrId("jolla-email-la-updating") : + app.errorOccurred ? app.lastErrorText : app.lastAccountUpdate + anchors { top: unreadCount.baseline; topMargin: Theme.paddingLarge } + height: parent.height - coverActionArea.height - statusLabel.y - Theme.paddingMedium + fontSizeMode: Text.VerticalFit + font.pixelSize: Theme.fontSizeLarge + wrapMode: app.syncInProgress ? Text.NoWrap : Text.Wrap + width: parent.width - Theme.paddingLarge + color: Theme.highlightColor + elide: Text.ElideNone + maximumLineCount: 3 + Timer { + property bool keepVisible + + repeat: true + interval: 500 + running: app.syncInProgress && emailCover.status === Cover.Active + onRunningChanged: if (!running) root.opacity = 1.0 + onTriggered: { + if (keepVisible) { + keepVisible = false + } else { + keepVisible = root.opacity < Theme.opacityLow + root.opacity = (root.opacity > Theme.opacityLow ? 0.0 : 1.0) + } + } + } + } + OpacityRampEffect { + offset: 0.5 + sourceItem: statusLabel + enabled: statusLabel.implicitWidth > statusLabel.width - Theme.paddingLarge + } + CoverActionList { + enabled: app.numberOfAccounts > 0 + CoverAction { + iconSource: "image://theme/icon-cover-sync" + onTriggered: { + emailAgent.accountsSyncInbox() + } + } + } +} diff --git a/usr/share/jolla-email/email.qml b/usr/share/jolla-email/email.qml new file mode 100644 index 00000000..b6a9998c --- /dev/null +++ b/usr/share/jolla-email/email.qml @@ -0,0 +1,374 @@ +/* + * Copyright (c) 2012 – 2019 Jolla Ltd. + * Copyright (c) 2019 Open Mobile Platform LLC. + * + * License: Proprietary + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Sailfish.WebEngine 1.0 +import Nemo.Email 0.1 +import Nemo.Notifications 1.0 +import com.jolla.email 1.1 +import com.jolla.settings.accounts 1.0 +import com.jolla.signonuiservice 1.0 +import Nemo.Time 1.0 +import Nemo.Connectivity 1.0 +import Nemo.DBus 2.0 +import "pages" +import "pages/utils.js" as Utils + +ApplicationWindow { + id: app + + allowedOrientations: defaultAllowedOrientations + _defaultPageOrientations: Orientation.All + _defaultLabelFormat: Text.PlainText + + property alias numberOfAccounts: mailAccountListModel.numberOfAccounts + property alias syncInProgress: emailAgent.synchronizing + property bool refreshSyncTime + readonly property string lastAccountUpdate: { + // Cheating a bit as needsUpdate is always true but property changes are + // of our interest here. + var needsUpdate = Qt.application.active || refreshSyncTime || true + if (mailAccountListModel.persistentConnectionActive) { + return qsTrId("email-la_up_to_date") + } else if (needsUpdate) { + return Utils.lastSyncTime(mailAccountListModel.lastUpdateTime) + } + } + property bool accountsManagerActive + readonly property int defaultMessageListLimit: 100 + + // State for cover + property string coverMode: "mainView" + property bool errorOccurred + property string lastErrorText + property string viewerSender + property string viewerSubject + property string editorTo + property string editorBody + property int inboxUnreadCount + property int combinedInboxUnreadCount + + property Page _mainPage + property Component _messageViewerComponent + property bool _hasCombinedInbox + property var today: new Date() + + signal movingToMainPage() + + cover: Qt.resolvedUrl("cover/EmailCover.qml") + + Component.onCompleted: { + _updateMainPage() + } + + WallClock { + id: wallClock + + enabled: Qt.application.state === Qt.ApplicationActive + updateFrequency: WallClock.Minute + + onTimeChanged: { + var now = wallClock.time + if (now.getDate() !== app.today.getDate() + || now.getMonth() !== app.today.getMonth() + || now.getYear() !== app.today.getYear()) { + app.today = now + } + } + } + + DBusAdaptor { + service: "com.jolla.email.ui" + path: "/share" + iface: "org.sailfishos.share" + + function share(shareActionConfiguration) { + _navigateToMainPage() + if (app.numberOfAccounts) { + pageStack.animatorPush(Qt.resolvedUrl("pages/ShareComposerPage.qml"), + { "shareActionConfiguration": shareActionConfiguration }, + PageStackAction.Immediate) + } + app.activate() + } + } + + Connections { + target: EmailService + + onShowCompose: { + _navigateToMainPage() + if (app.numberOfAccounts) { + var obj = pageStack.animatorPush(Qt.resolvedUrl("pages/ComposerPage.qml"), { + emailTo: emailTo, + emailSubject: emailSubject, + emailCc: emailCc, + emailBcc: emailBcc, + emailBody: emailBody}, + PageStackAction.Immediate) + obj.pageCompleted.connect(function(page) { + for (var attachment in attachments) { + page.attachmentsModel.append({ + "url": attachments[attachment]["url"], + "title": attachments[attachment]["name"], + "mimeType": attachments[attachment]["mime"], + "FromOriginalMessage": "false" + }) + } + }) + } + app.activate() + } + + onShowCombinedInbox: { + _navigateToMainPage() + app.activate() + } + + onShowInbox: { + _navigateToAccountInbox(accountId) + app.activate() + } + + onShowMessage: { + if (emailAgent.isMessageValid(messageId)) { + _navigateToAccountInbox(emailAgent.accountIdForMessage(messageId)) + pageStack.animatorPush(app.getMessageViewerComponent(), + { + "messageId": messageId, + "removeCallback": pageStack.currentPage.removeMessage, + "messageAction": messageAction, + }, + PageStackAction.Immediate) + } else { + _navigateToMainPage() + console.log("Message is not valid:", messageId) + } + app.activate() + } + + onShowWindow: app.activate() + } + + function _navigateToMainPage() { + movingToMainPage() + if (pageStack.currentPage != _mainPage) { + pageStack.pop(_mainPage, PageStackAction.Immediate) + } + } + + function _navigateToAccountInbox(accountId) { + _navigateToMainPage() + if (emailAgent.isAccountValid(accountId)) { + if (numberOfAccounts > 1) { + pushAccountInbox(accountId, true) + } + } else { + console.log("Account is not valid:", accountId) + } + } + + function _updateMainPage() { + if (numberOfAccounts === 0) { + _navigateToMainPage() + _mainPage = pageStack.replace(Qt.resolvedUrl("pages/NoAccountsPage.qml"), {}, PageStackAction.Immediate) + } else if (numberOfAccounts === 1) { + _navigateToMainPage() + var accountId = mailAccountListModel.accountId(0) + var inbox = emailAgent.inboxFolderId(accountId) + + if (inbox > 0) { + var accessor = emailAgent.accessorFromFolderId(inbox) + _mainPage = pageStack.replace(Qt.resolvedUrl("pages/MessageListPage.qml"), { folderAccessor: accessor }, + PageStackAction.Immediate) + } else { + _mainPage = pageStack.replace(Qt.resolvedUrl("pages/PendingInboxPage.qml"), { accountId: accountId }, + PageStackAction.Immediate) + emailAgent.synchronizeInbox(accountId) + } + + _hasCombinedInbox = false // remove this fellow + } else if (!_hasCombinedInbox && numberOfAccounts > 1) { + _navigateToMainPage() + _mainPage = pageStack.replace(Qt.resolvedUrl("pages/CombinedInbox.qml"), {}, PageStackAction.Immediate) + _hasCombinedInbox = true + } + } + + function pushAccountInbox(accountId, immediate) { + var inbox = emailAgent.inboxFolderId(accountId) + if (inbox > 0) { + var accessor = emailAgent.accessorFromFolderId(inbox) + pageStack.animatorPush(Qt.resolvedUrl("pages/MessageListPage.qml"), { folderAccessor: accessor }, + immediate ? PageStackAction.Immediate : PageStackAction.Animated) + } else { + pageStack.animatorPush(Qt.resolvedUrl("pages/PendingInboxPage.qml"), { accountId: accountId }, + immediate ? PageStackAction.Immediate : PageStackAction.Animated) + emailAgent.synchronizeInbox(accountId) + } + } + + function showAccountsCreationDialog() { + app.accountsManagerActive = true + accountCreationLoader.setSource(pageStack.resolveImportPage("com.jolla.email.AccountCreation"), { endDestination: app._mainPage }) + accountCreationLoader.active = true + } + + function getMessageViewerComponent() { + if (componentCompiler.running) { + componentCompiler.triggered() + } + + return _messageViewerComponent + } + + function showSingleLineNotification(text) { + if (text.length > 0) { + var n = notificationComponent.createObject(null, { 'summary': text, + 'appIcon': "image://theme/icon-system-warning" }) + n.publish() + } + } + + Component { + id: notificationComponent + Notification { + isTransient: true + } + } + + EmailAgent { + id: emailAgent + + onSynchronizingChanged: { + if (synchronizing) { + errorOccurred = false + } + } + + onNetworkConnectionRequested: { + connectionHelper.attemptToConnectNetwork() + } + + // Global status used for the cover + onError: { + errorOccurred = true + lastErrorText = Utils.syncErrorText(syncError) + } + + onCalendarInvitationResponded: { + if (!success) { + var text = "" + switch (response) { + case EmailAgent.InvitationResponseAccept: + //: Failed to send invitation response (accept) + //% "Failed to accept invitation" + text = qsTrId("jolla-email-la-response_failed_body_accept") + break + case EmailAgent.InvitationResponseTentative: + //: Failed to send invitation response (tentative) + //% "Failed to tentatively accept invitation" + text = qsTrId("jolla-email-la-response_failed_body_tentative") + break + case EmailAgent.InvitationResponseDecline: + //: Failed to send invitation response (decline) + //% "Failed to decline invitation" + text = qsTrId("jolla-email-la-response_failed_body_decline") + break + default: + break + } + showSingleLineNotification(text) + } + } + + onOnlineFolderActionCompleted: { + if (!success) { + var text = "" + switch (action) { + case EmailAgent.ActionOnlineCreateFolder: + //% "Folder creation failed" + text = qsTrId("jolla-email-la-fa_failed_body_create") + break + case EmailAgent.ActionOnlineDeleteFolder: + //% "Folder deletion failed" + text = qsTrId("jolla-email-la-fa_failed_body_delete") + break + case EmailAgent.ActionOnlineRenameFolder: + //% "Folder rename failed" + text = qsTrId("jolla-email-la-fa_failed_body_rename") + break + case EmailAgent.ActionOnlineMoveFolder: + //% "Folder move failed" + text = qsTrId("jolla-email-la-fa_failed_body_move") + break + default: + break + } + showSingleLineNotification(text) + } + } + } + + EmailAccountListModel { + id: mailAccountListModel + + onAccountsAdded: { + // Don't try to modify pages while accounts configuration manager is running + if (!accountsManagerActive) { + _updateMainPage() + } + } + onAccountsRemoved: { + if (!accountsManagerActive) { + _updateMainPage() + } + } + } + + Timer { + id: lastAccountUpdateRefreshTimer + interval: 60000 + running: !syncInProgress + repeat: true + onTriggered: refreshSyncTime = !refreshSyncTime + } + + ConnectionHelper { + id: connectionHelper + } + + Loader { + id: accountCreationLoader + active: false + anchors.fill: parent + + Connections { + target: accountCreationLoader.item + ignoreUnknownSignals: true + + onCreationCompleted: { + _updateMainPage() + app.accountsManagerActive = false + accountCreationLoader.active = false + } + } + } + + Timer { + // do some compilation ahead of time, but avoid delaying startup + id: componentCompiler + running: true + interval: 500 + onTriggered: { + running = false + _messageViewerComponent = Qt.createComponent(Qt.resolvedUrl("pages/MessageView.qml")) + Qt.createComponent(Qt.resolvedUrl("pages/ComposerPage.qml"), Component.Asynchronous) + } + } +} diff --git a/usr/share/jolla-email/pages/AccountList.qml b/usr/share/jolla-email/pages/AccountList.qml new file mode 100644 index 00000000..e822a944 --- /dev/null +++ b/usr/share/jolla-email/pages/AccountList.qml @@ -0,0 +1,166 @@ +/* + * Copyright (c) 2013 – 2019 Jolla Ltd. + * + * License: Proprietary + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Nemo.Email 0.1 +import "utils.js" as Utils + +Column { + id: root + + width: parent.width + + Repeater { + model: mailAccountListModel + + delegate: ListItem { + id: accountItem + property bool errorOccurred + property string lastErrorText + property bool updating + + width: root.width + contentHeight: Theme.itemSizeExtraLarge + menu: Component { + ContextMenu { + id: contextMenu + MenuItem { + //: Update account + //% "Sync" + text: qsTrId("jolla-email-me-sync") + onClicked: emailAgent.synchronize(mailAccountId) + } + } + } + + Label { + id: unreadCountLabel + color: highlighted ? Theme.highlightColor : Theme.primaryColor + text: unreadCount ? unreadCount : "" + font.pixelSize: Theme.fontSizeLarge + anchors { + left: accountItem.contentItem.left + leftMargin: Screen.sizeCategory >= Screen.Large ? Theme.horizontalPageMargin : 0 + right: accountIcon.left + rightMargin: Theme.paddingLarge + verticalCenter: parent.verticalCenter + } + horizontalAlignment: Text.AlignRight + } + + Image { + id: accountIcon + + property string fixedIconPath: iconPath + + x: Screen.sizeCategory >= Screen.Large + ? 6 * Theme.paddingLarge + : 5 * Theme.paddingLarge + width: Screen.sizeCategory >= Screen.Large + ? 90 * Theme.pixelRatio + : Screen.width / 5 + height: width + sourceSize.width: width + sourceSize.height: height + anchors.verticalCenter: parent.verticalCenter + source: fixedIconPath !== "" ? fixedIconPath : "image://theme/graphic-service-generic-mail" + + onStatusChanged: if (accountIcon.status == Image.Error) fixedIconPath = "image://theme/graphic-service-generic-mail" + } + + Column { + anchors { + left: accountIcon.right + leftMargin: Theme.paddingLarge + right: parent.right + rightMargin: Theme.horizontalPageMargin + verticalCenter: parent.verticalCenter + verticalCenterOffset: -Math.round(Theme.paddingSmall/2) + } + + Label { + width: parent.width + text: displayName !== "" ? displayName : emailAddress + font.pixelSize: accountItem.errorOccurred ? Theme.fontSizeMedium : Theme.fontSizeLarge + color: unreadCountLabel.text !== "" ? (highlighted ? Theme.highlightColor : Theme.primaryColor) + : (highlighted ? Theme.secondaryHighlightColor : Theme.secondaryColor) + truncationMode: TruncationMode.Fade + } + + Item { + width: parent.width + height: statusLabel.height + + Icon { + id: errorIcon + + y: (statusLabel.firstLineHeight - height) / 2 + + visible: accountItem.errorOccurred + source: "image://theme/icon-s-warning" + } + + Label { + id: statusLabel + + property real firstLineHeight + + x: accountItem.errorOccurred ? errorIcon.width + Theme.paddingSmall : 0 + width: parent.width - x + font.pixelSize: Theme.fontSizeExtraSmall + color: highlighted ? Theme.secondaryHighlightColor : Theme.secondaryColor + wrapMode: Text.Wrap + text: { + // Cheating a bit as needsUpdate is always true but property changes are + // of our interest here. + var needsUpdate = Qt.application.active || accountItem.visible || app.refreshSyncTime || true + if (accountItem.updating) { + //: Updating account label + //% "Updating account..." + return qsTrId("jolla-email-la-updating_account") + } else if (accountItem.errorOccurred) { + return lastErrorText + } else if (needsUpdate) { + return hasPersistentConnection ? qsTrId("email-la_up_to_date") : Utils.lastSyncTime(lastSynchronized) + } + return "" + } + + onLineLaidOut: { + if (line.number === 0) { + firstLineHeight = line.height + } + } + } + } + } + onClicked: { + app.pushAccountInbox(mailAccountId, false) + } + + Connections { + target: emailAgent + + onCurrentSynchronizingAccountIdChanged: { + if (emailAgent.currentSynchronizingAccountId === mailAccountId) { + accountItem.updating = true + accountItem.errorOccurred = false + } else { + accountItem.updating = false + } + } + + onError: { + if (accountId === 0 || accountId === mailAccountId) { + accountItem.errorOccurred = true + accountItem.lastErrorText = Utils.syncErrorText(syncError) + } + } + } + } + } +} diff --git a/usr/share/jolla-email/pages/AttachmentDelegate.qml b/usr/share/jolla-email/pages/AttachmentDelegate.qml new file mode 100644 index 00000000..c143bc9b --- /dev/null +++ b/usr/share/jolla-email/pages/AttachmentDelegate.qml @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2013 – 2019 Jolla Ltd. + * Copyright (c) 2019 Open Mobile Platform LLC. + * + * License: Proprietary + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Nemo.Thumbnailer 1.0 +import Nemo.Email 0.1 +import Nemo.FileManager 1.0 + +BackgroundItem { + id: attachmentItem + + readonly property bool activated: statusInfo === EmailAgent.Downloading || statusInfo === EmailAgent.Queued + readonly property bool downloaded: statusInfo === EmailAgent.Downloaded && url !== "" + readonly property int downloadedSize: progressInfo * size + property bool openOnDownload + property real attachmentNameFontSize: Theme.fontSizeMedium + property real attachmentStatusFontSize: Theme.fontSizeSmall + + property real leftMargin: Theme.horizontalPageMargin + property real rightMargin: Theme.horizontalPageMargin + + function triggerAction(url) { + // Attachment is considered as downloaded when attachment body has been + // downloaded. The url is only valid when attachment exists also in file system. + if (url && statusInfo === EmailAgent.Downloaded) { + if (mimeType.toLowerCase() == "message/rfc822") { + pageStack.animatorPush(app.getMessageViewerComponent(), { "pathToLoad": FileEngine.urlToPath(url) }) + } else { + Qt.openUrlExternally(url) + } + return true + } else { + return false + } + } + + onClicked: { + if (activated) { + openOnDownload = false + emailAgent.cancelAttachmentDownload(contentLocation) + } else if (!triggerAction(url)) { + // maybe downloaded but not saved as file + var saved = emailAgent.downloadAttachment(messageId, contentLocation) + if (saved) { + triggerAction(url) + } else { + openOnDownload = true + } + } + } + + onDownloadedChanged: { + if (downloaded && openOnDownload) { + openOnDownload = false + + triggerAction(url) + } + } + + Thumbnail { + id: icon + x: attachmentItem.leftMargin + y: (attachmentItem.contentHeight - height) / 2 + height: Theme.iconSizeMedium + width: Theme.iconSizeMedium + + sourceSize.width: width + sourceSize.height: height + source: url + mimeType: mimeType + } + + Icon { + visible: icon.status !== Thumbnail.Ready + anchors.centerIn: icon + source: activated ? "image://theme/icon-m-clear" : Theme.iconForMimeType(mimeType) + + Loader { + active: attachmentItem.activated + anchors.centerIn: parent + sourceComponent: ProgressCircle { + value: statusInfo === EmailAgent.Downloading ? progressInfo : 0 + height: attachmentItem.contentHeight - 2*Theme.paddingSmall + width: height + progressColor: Theme.highlightDimmerColor + backgroundColor: attachmentItem.pressed ? Theme.secondaryHighlightColor : Theme.highlightColor + } + } + } + + Label { + id: attachmentName + + x: icon.x + icon.width + Theme.paddingMedium + y: Math.max(Theme.paddingLarge, (attachmentItem.contentHeight - height) / 2) + width: sizeLabel.x - x - Theme.paddingMedium + + font.pixelSize: attachmentItem.attachmentNameFontSize + + text: type === AttachmentListModel.Email + //: Attached email with unknown title => use placeholder name + //% "Forwarded email" + ? title || qsTrId("jolla-email-la-forwarded_email") + : displayName + truncationMode: TruncationMode.Fade + } + + Label { + id: sizeLabel + x: attachmentItem.width - width - attachmentItem.rightMargin + y: Math.max(Theme.paddingLarge, (attachmentItem.contentHeight - height) / 2) + + text: statusInfo === EmailAgent.Downloading + ? (Format.formatFileSize(downloadedSize, 2) + + "/" + + Format.formatFileSize(size, 2)) + : Format.formatFileSize(size, 2) + font.pixelSize: attachmentStatusFontSize + } +} diff --git a/usr/share/jolla-email/pages/AttachmentListPage.qml b/usr/share/jolla-email/pages/AttachmentListPage.qml new file mode 100644 index 00000000..e79b42db --- /dev/null +++ b/usr/share/jolla-email/pages/AttachmentListPage.qml @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2013 – 2019 Jolla Ltd. + * + * License: Proprietary + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 + +Page { + property int messageId + property alias attachmentsModel: attachmentListView.model + + objectName: "attachmentsListPage" + + SilicaListView { + id: attachmentListView + anchors.fill: parent + + header: PageHeader { + //% "Attachments" + title: qsTrId("jolla-email-he-attachments_list_page") + } + + delegate: AttachmentDelegate { } + VerticalScrollDecorator {} + } +} diff --git a/usr/share/jolla-email/pages/AttachmentRow.qml b/usr/share/jolla-email/pages/AttachmentRow.qml new file mode 100644 index 00000000..b90f9f73 --- /dev/null +++ b/usr/share/jolla-email/pages/AttachmentRow.qml @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2018 – 2019 Jolla Ltd. + * Copyright (c) 2020 Open Mobile Platform LLC. + * + * License: Proprietary + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Nemo.Email 0.1 +import Nemo.Thumbnailer 1.0 + +Row { + id: root + + property var emailMessage + property AttachmentListModel attachmentsModel + + readonly property int _maximumVisibleAttachments: { + if (Screen.sizeCategory < Screen.Large) { + return 2 + } else if (portrait) { + return 3 + } else { + return 4 + } + } + + height: Theme.itemSizeExtraSmall + + Repeater { + model: emailMessage && emailMessage.numberOfAttachments > 0 && + emailMessage.numberOfAttachments <= _maximumVisibleAttachments + ? attachmentsModel + : null + delegate: AttachmentDelegate { + id: attachmentItem + + leftMargin: attachmentItem.Positioner.isFirstItem + ? Theme.horizontalPageMargin + : Theme.paddingMedium + rightMargin: attachmentItem.Positioner.isLastItem + ? Theme.horizontalPageMargin + : Theme.paddingMedium + + width: ((root.width - (2 * (Theme.horizontalPageMargin - Theme.paddingMedium))) / emailMessage.numberOfAttachments) + + (leftMargin - Theme.paddingMedium) + + (rightMargin - Theme.paddingMedium) + + contentHeight: Theme.itemSizeExtraSmall + + attachmentNameFontSize: Theme.fontSizeExtraSmall + attachmentStatusFontSize: Theme.fontSizeExtraSmall + } + } + + BackgroundItem { + id: attachmentsLink + visible: emailMessage && emailMessage.numberOfAttachments > root._maximumVisibleAttachments + contentHeight: Theme.itemSizeExtraSmall + + onClicked: { + pageStack.animatorPush("AttachmentListPage.qml", { + messageId: emailMessage.messageId, + attachmentsModel: root.attachmentsModel + }) + } + + Label { + //: Number of email attachments (only used when number of attachments greater than 2) + //% "%n attachments" + text: qsTrId("jolla-email-la-attachments_summary", emailMessage ? emailMessage.numberOfAttachments : 0) + anchors { + verticalCenter: parent.verticalCenter + left: parent.left + right: parent.right + margins: Theme.horizontalPageMargin + } + } + } +} diff --git a/usr/share/jolla-email/pages/CalendarDelegate.qml b/usr/share/jolla-email/pages/CalendarDelegate.qml new file mode 100644 index 00000000..841ef0d0 --- /dev/null +++ b/usr/share/jolla-email/pages/CalendarDelegate.qml @@ -0,0 +1,155 @@ +/* + * Copyright (c) 2015 - 2019 Jolla Ltd. + * Copyright (c) 2020 - 2021 Open Mobile Platform LLC. + * + * License: Proprietary + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Nemo.Email 0.1 +import org.nemomobile.calendar 1.0 + +BackgroundItem { + id: root + + property EmailMessage email + property real leftMargin: Theme.paddingMedium + property real rightMargin: Theme.paddingMedium + property real iconSize: Theme.iconSizeMedium + + contentHeight: Theme.itemSizeExtraSmall + height: contentItem.height + + InvitationQuery { + id: invitationQuery + invitationFile: !!email ? email.calendarInvitationUrl : "" + property bool triggerInvitationWhenFinished + onQueryFinished: { + if (triggerInvitationWhenFinished) { + triggerInvitation() + } + } + function triggerInvitation() { + invitationQuery.triggerInvitationWhenFinished = false + if (invitationQuery.instanceId.length > 0 && invitationQuery.startTime.length > 0) { + // the invitation has already been synced or imported. + var obj = pageStack.animatorPush(Qt.resolvedUrl("CalendarEventPage.qml"), { + instanceId: invitationQuery.instanceId, + startTime: invitationQuery.startTime, + cancellation: email.hasCalendarCancellation + }) + obj.pageCompleted.connect(function(page) { + page.eventRemovePressed.connect(function() { + invitationQuery.query() + }) + }) + } else { + // the invitation doesn't yet exist in the calendar. + pageStack.animatorPush(Qt.resolvedUrl("CalendarEventPreviewPage.qml"), { + icsString: email.calendarInvitationBody, + cancellation: email.hasCalendarCancellation + }) + } + } + } + + onClicked: { + if ((email.calendarInvitationStatus != EmailMessage.Downloading + && email.calendarInvitationStatus != EmailMessage.Downloaded + && email.calendarInvitationStatus != EmailMessage.Saved) + || email.calendarInvitationUrl === "") { + email.getCalendarInvitation() + } else if (invitationQuery.busy) { + invitationQuery.triggerInvitationWhenFinished = true + } else { + invitationQuery.triggerInvitation() + } + } + + Icon { + id: defaultIcon + x: leftMargin + height: iconSize + width: height + anchors.verticalCenter: parent.verticalCenter + sourceSize.width: width + sourceSize.height: height + source: email.hasCalendarInvitation + ? "image://theme/icon-l-date" + : "image://theme/icon-l-calendar-cancelled" + } + + Item { + anchors { + verticalCenter: parent.verticalCenter + left: defaultIcon.right + leftMargin: Theme.paddingMedium + right: parent.right + rightMargin: root.rightMargin + } + + height: calendarInvitationLabel.height + statusLabel.height + + Label { + id: calendarInvitationLabel + width: parent.width + font.pixelSize: Theme.fontSizeExtraSmall + text: email.hasCalendarInvitation + //: Calendar invitation label + //% "Calendar invitation" + ? qsTrId("jolla-email-la-calendar_invitation") + //: Calendar cancellation label + //% "Calendar cancellation" + : qsTrId("jolla-email-la-calendar_cancellation") + truncationMode: TruncationMode.Fade + } + + Label { + id: statusLabel + visible: email && email.calendarInvitationStatus != EmailMessage.Downloading + width: parent.width + anchors.top: calendarInvitationLabel.bottom + font.pixelSize: Theme.fontSizeTiny + + text: email ? statusText(email.calendarInvitationStatus) : "" + truncationMode: TruncationMode.Fade + } + + ProgressBar { + visible: email && email.calendarInvitationStatus === EmailMessage.Downloading + indeterminate: true + width: parent.width + leftMargin: Theme.paddingMedium + rightMargin: Theme.paddingMedium + anchors.top: calendarInvitationLabel.bottom + highlighted: root.highlighted + } + } + + function statusText(status) { + if (status === EmailMessage.Unknown) { + //: Calendar invitation download state - Not Downloaded + //% "Not Downloaded" + return qsTrId("jolla-email-la-calendar_invitation_not_downloaded") + } else if (status === EmailMessage.Downloaded || status === EmailMessage.Saved) { + //: Calendar invitation download state - Downloaded + //% "Downloaded" + return qsTrId("jolla-email-la-calendar_invitation_downloaded") + } else if (status === EmailMessage.Downloading) { + //: Calendar invitation download state - Downloading + //% "Downloading" + return qsTrId("jolla-email-la-calendar_invitation_downloading") + } else if (status === EmailMessage.Failed) { + //: Calendar invitation download state - Failed + //% "Failed" + return qsTrId("jolla-email-la-calendar_invitation_failed") + } else if (status === EmailMessage.FailedToSave) { + //: Calendar invitation - Failed to save file + //% "Failed to save file" + return qsTrId("jolla-email-la-calendar_invitation_failed_save") + } else { + return "" + } + } +} diff --git a/usr/share/jolla-email/pages/CalendarEventPage.qml b/usr/share/jolla-email/pages/CalendarEventPage.qml new file mode 100644 index 00000000..caad6b01 --- /dev/null +++ b/usr/share/jolla-email/pages/CalendarEventPage.qml @@ -0,0 +1,13 @@ +/* + * Copyright (c) 2017 – 2019 Jolla Ltd. + * + * License: Proprietary + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Sailfish.Calendar 1.0 + +CalendarEventViewPage { + +} diff --git a/usr/share/jolla-email/pages/CalendarEventPreviewPage.qml b/usr/share/jolla-email/pages/CalendarEventPreviewPage.qml new file mode 100644 index 00000000..fe3bd50c --- /dev/null +++ b/usr/share/jolla-email/pages/CalendarEventPreviewPage.qml @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2017 - 2019 Jolla Ltd. + * Copyright (c) 2020 - 2021 Open Mobile Platform LLC. + * + * License: Proprietary + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Sailfish.Calendar 1.0 +import org.nemomobile.calendar 1.0 +import Nemo.DBus 2.0 + +Page { + id: root + property alias icsString: importModel.icsString + property alias event: eventDetails.event + property alias occurrence: eventDetails.occurrence + property alias cancellation: eventDetails.cancellation + + ImportModel { + id: importModel + onCountChanged: { + if (count > 0) { + eventDetails.event = getEvent(0) + eventDetails.occurrence = eventDetails.event ? eventDetails.event.nextOccurrence() : null + } else { + eventDetails.event = null + eventDetails.occurrence = null + } + } + } + + DBusInterface { + id: calendarDBusInterface + service: "com.jolla.calendar.ui" + path: "/com/jolla/calendar/ui" + iface: "com.jolla.calendar.ui" + } + + SilicaFlickable { + anchors.fill: parent + contentHeight: column.height + Theme.paddingLarge + + PullDownMenu { + visible: icsString !== "" && !cancellation + MenuItem { + //% "Import into Calendar" + text: qsTrId("sailfish_calendar-me-import_event_in_calendar") + onClicked: { + calendarDBusInterface.call("importIcsData", [root.icsString]) + pageStack.pop() + } + } + } + + Column { + id: column + + width: parent.width + spacing: Theme.paddingMedium + + PageHeader { + width: parent.width + title: eventDetails.event ? eventDetails.event.displayLabel : "" + wrapMode: Text.Wrap + } + + CalendarEventView { + id: eventDetails + showHeader: false + showSelector: false + } + } + VerticalScrollDecorator {} + } +} diff --git a/usr/share/jolla-email/pages/CalendarInvite.qml b/usr/share/jolla-email/pages/CalendarInvite.qml new file mode 100644 index 00000000..79864633 --- /dev/null +++ b/usr/share/jolla-email/pages/CalendarInvite.qml @@ -0,0 +1,130 @@ +/* + * Copyright (c) 2020 Open Mobile Platform LLC. + * + * License: Proprietary + */ + +import QtQuick 2.6 +import Sailfish.Silica 1.0 +import Sailfish.Calendar 1.0 +import Nemo.Email 0.1 +import org.nemomobile.calendar 1.0 + +SilicaControl { + id: invite + + property QtObject event + property QtObject occurrence + property alias preferredButtonWidth: responseButtons.preferredButtonWidth + + property int pixelSize: Theme.fontSizeSmall + + readonly property bool twoLineDates: !startDate.fitsOneLine || (multiDay && !endDate.fitsOneLine) + readonly property bool multiDay: { + if (!invite.occurrence) { + return false + } + + var start = invite.occurrence.startTime + var end = invite.occurrence.endTime + return start.getFullYear() !== end.getFullYear() + || start.getMonth() !== end.getMonth() + || start.getDate() !== end.getDate() + } + + + implicitHeight: timesFlow.height + responseButtons.implicitHeight + Theme.paddingLarge + palette.colorScheme: Theme.DarkOnLight + + Rectangle { + width: invite.width + height: invite.height + + color: "#f3f0f0" + } + + // Ideally put these side by side aligned to each end but if there's not enough space break over + // two lines and left align. + Flow { + id: timesFlow + + x: Theme.horizontalPageMargin + + width: invite.width - (2 * x) + + spacing: Math.max(Theme.paddingSmall, width - startDate.width - endDateOrDuration.width) + + CalendarEventDate { + id: startDate + + eventDate: invite.occurrence ? invite.occurrence.startTime : new Date(-1) + showTime: invite.multiDay && (invite.event && !invite.event.allDay) + timeContinued: invite.multiDay + useTwoLines: invite.twoLineDates + color: invite.palette.highlightColor + font.pixelSize: invite.pixelSize + maximumWidth: timesFlow.width / (invite.multiDay ? 2 : 1) + } + + Row { + id: endDateOrDuration + + CalendarEventDate { + id: endDate + + visible: invite.multiDay + eventDate: invite.occurrence ? invite.occurrence.endTime : new Date(-1) + showTime: invite.event && !invite.event.allDay + useTwoLines: invite.twoLineDates + color: invite.palette.highlightColor + font.pixelSize: invite.pixelSize + maximumWidth: timesFlow.width / 2 + } + + Text { + height: startDate.height + + color: invite.palette.highlightColor + font.pixelSize: invite.pixelSize + visible: !invite.multiDay + verticalAlignment: Text.AlignVCenter + + //% "All day" + text: !invite.event ? "" : (invite.event.allDay + ? qsTrId("jolla-email-la-all_day") + : (Format.formatDate(invite.occurrence.startTime, Formatter.TimeValue) + + " - " + + Format.formatDate(invite.occurrence.endTime, Formatter.TimeValue))) + } + } + } + + InvitationResponseButtons { + id: responseButtons + + y: timesFlow.height + Math.min(Theme.paddingLarge, (invite.height - timesFlow.height - height - Theme.paddingMedium) / 2) + + subject: invite.event ? invite.event.displayLabel : "" + width: parent.width + labelFont.pixelSize: invite.pixelSize + _backgroundColor: "white" + onCalendarInvitationResponded: { + var emailResponse = EmailAgent.InvitationResponseUnspecified + switch (response) { + case CalendarEvent.ResponseAccept: + emailResponse = EmailAgent.InvitationResponseAccept + break + case CalendarEvent.ResponseTentative: + emailResponse = EmailAgent.InvitationResponseTentative + break + case CalendarEvent.ResponseDecline: + emailResponse = EmailAgent.InvitationResponseDecline + break + default: + return + } + emailAgent.respondToCalendarInvitation(email.messageId, emailResponse, responseSubject) + pageStack.pop() + } + } +} diff --git a/usr/share/jolla-email/pages/CombinedInbox.qml b/usr/share/jolla-email/pages/CombinedInbox.qml new file mode 100644 index 00000000..97691579 --- /dev/null +++ b/usr/share/jolla-email/pages/CombinedInbox.qml @@ -0,0 +1,177 @@ +/* + * Copyright (c) 2013 – 2019 Jolla Ltd. + * Copyright (c) 2019 Open Mobile Platform LLC. + * + * License: Proprietary + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Nemo.Email 0.1 +import com.jolla.email 1.1 +import "utils.js" as Utils + +Page { + id: root + + onStatusChanged: { + if (status === PageStatus.Active) { + app.coverMode = "mainView" + } + } + + EmailMessageListModel { + id: combinedInboxModel + + folderAccessor: emailAgent.combinedInboxAccessor() + } + + Binding { + target: app + property: "combinedInboxUnreadCount" + value: combinedInboxModel.count + } + + RemorsePopup { + id: removeSingleRemorse + + function startRemoveSingle(messageId) { + //% "Deleting mail" + execute(qsTrId("jolla-email-me-deleting-mail"), function() { + emailAgent.deleteMessage(messageId) + }) + } + } + + MessageRemorsePopup { + id: multiItemRemoveRemorse + } + + SilicaListView { + id: messageListView + + anchors.fill: parent + + PullDownMenu { + busy: app.syncInProgress + + MenuItem { + // Defined in message list page + text: qsTrId("jolla-email-me-select_messages") + visible: messageListView.count > 0 + onClicked: { + pageStack.animatorPush(Qt.resolvedUrl("MultiSelectionPage.qml"), { + removeRemorse: multiItemRemoveRemorse, + selectionModel: combinedInboxModel, + deletionModel: deletionModel + }) + } + } + + MenuItem { + //: Synchronize inbox of all enabled accounts + //% "Synchronize all" + text: qsTrId("jolla-email-me-sync_all") + onClicked: { + emailAgent.accountsSyncAllFolders() + } + } + + MenuItem { + //: New message menu item + //% "New Message" + text: qsTrId("jolla-email-me-new_message") + onClicked: { + pageStack.animatorPush(Qt.resolvedUrl("ComposerPage.qml")) + } + } + } + + header: Column { + width: parent.width + PageHeader { + id: pageHeader + //: Email page header + //% "Mail" + title: qsTrId("email-he-email") + } + AccountList { + id: accountList + } + Item { + height: Theme.itemSizeLarge + width: parent.width + visible: messageListView.count > 0 + Label { + //: Shows overall number of unread messages in the Inboxes of all accounts. + //: Takes number of unread messages as a parameter. + //% "Inboxes (%1)" + text: qsTrId("email-la_unread_messages_in_inboxes").arg(messageListView.count) + color: Theme.highlightColor + font.pixelSize: Theme.fontSizeMedium + + anchors { + baseline: parent.bottom + // If possible drop this bottom margin when first item is section header + baselineOffset: -Theme.paddingLarge + horizontalCenter: parent.horizontalCenter + } + } + } + Item { + height: Math.max(emptyStateText.height + Theme.paddingLarge, + messageListView.height - accountList.height - pageHeader.height - Theme.paddingLarge) + width: parent.width + visible: messageListView.count === 0 + Text { + id: emptyStateText + width: parent.width - 2 * Theme.horizontalPageMargin + anchors.centerIn: parent + horizontalAlignment: Text.AlignHCenter + wrapMode: Text.Wrap + //: Empty state string for the combined Inboxes list view. + //: Shown when none of the Inboxes contain unread messages. + //% "No unread emails in Inboxes" + text: qsTrId("email-la_no_unread_messages_in_inboxes") + font { + pixelSize: Theme.fontSizeExtraLarge + family: Theme.fontFamilyHeading + } + color: Theme.secondaryHighlightColor + } + } + } + + footer: Item { + width: messageListView.width + height: Theme.paddingLarge + } + + section { + property: 'timeSection' + + delegate: SectionHeader { + text: Format.formatDate(section, Formatter.TimepointSectionRelative) + height: text === "" ? 0 : Theme.itemSizeExtraSmall + horizontalAlignment: Text.AlignHCenter + } + } + + model: DeletionDelegateModel { + id: deletionModel + + model: combinedInboxModel + + delegate: MessageItem { + onEmailViewerRequested: { + pageStack.animatorPush(app.getMessageViewerComponent(), { + "messageId": messageId, + "removeCallback": removeSingleRemorse.startRemoveSingle + }) + } + } + } + + VerticalScrollDecorator {} + } +} diff --git a/usr/share/jolla-email/pages/ComposerPage.qml b/usr/share/jolla-email/pages/ComposerPage.qml new file mode 100644 index 00000000..b7762248 --- /dev/null +++ b/usr/share/jolla-email/pages/ComposerPage.qml @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2017 – 2019 Jolla Ltd. + * + * License: Proprietary + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import com.jolla.email 1.1 + +Page { + id: composerPage + + property alias attachmentsModel: composer.attachmentsModel + property alias emailSubject: composer.emailSubject + property alias emailTo: composer.emailTo + property alias emailCc: composer.emailCc + property alias emailBcc: composer.emailBcc + property alias emailBody: composer.emailBody + property alias messageId: composer.messageId + property alias maximumAttachmentsSize: composer.maximumAttachmentsSize + + property alias action: composer.action + property alias originalMessageId: composer.originalMessageId + property alias accountId: composer.accountId + + property alias popDestination: composer.popDestination + property alias draft: composer.draft + property var draftRemoveCallback + + highContrast: true + + // Lazy load cover + onStatusChanged: { + if (status === PageStatus.Active) { + app.coverMode = "mailEditor" + // Check if all content is available for FWD + if (composer.action === 'forward' && !composer.discardUndownloadedAttachments) { + composer.forwardContentAvailable() + } + } + } + + Connections { + target: app + onMovingToMainPage: { + if (composer.messageContentModified()) { + composer.saveDraft() + } + } + } + + Binding { + target: app + property: "editorTo" + value: composer._toSummary + } + + Binding { + target: app + property: "editorBody" + value: composer._bodyText + } + + EmailComposer { + id: composer + + autoSaveDraft: true + onRequestDraftRemoval: { + if (composerPage.draftRemoveCallback) { + composerPage.draftRemoveCallback() + } + } + } +} diff --git a/usr/share/jolla-email/pages/DecryptItem.qml b/usr/share/jolla-email/pages/DecryptItem.qml new file mode 100644 index 00000000..d3c70442 --- /dev/null +++ b/usr/share/jolla-email/pages/DecryptItem.qml @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2023 Jolla Ltd. + * + * License: Proprietary + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Nemo.Email 0.1 + +// Can become a BackgroundItem when decrypt capability will be available +Item { + property EmailMessage email + readonly property int encryptionStatus: email ? email.encryptionStatus : EmailMessage.NoDigitalEncryption + + height: Theme.itemSizeExtraSmall + visible: encryptionStatus != EmailMessage.NoDigitalEncryption + + Icon { + id: icon + x: Theme.horizontalPageMargin + anchors.verticalCenter: parent.verticalCenter + source: "image://theme/icon-m-device-lock" + } + + Label { + anchors { + left: icon.right + right: parent.right + leftMargin: Theme.paddingMedium + rightMargin: Theme.horizontalPageMargin + } + height: parent.height + + //% "Encrypted content" + text: qsTrId("jolla-email-la-encrypted_content") + font.pixelSize: Theme.fontSizeSmall + verticalAlignment: Text.AlignVCenter + truncationMode: TruncationMode.Fade + } +} diff --git a/usr/share/jolla-email/pages/DeletionDelegateModel.qml b/usr/share/jolla-email/pages/DeletionDelegateModel.qml new file mode 100644 index 00000000..62d4f8e2 --- /dev/null +++ b/usr/share/jolla-email/pages/DeletionDelegateModel.qml @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2013 – 2019 Jolla Ltd. + * Copyright (c) 2019 Open Mobile Platform LLC. + * + * License: Proprietary + */ + +import QtQml.Models 2.1 + +DelegateModel { + property alias allItems: allItemsGroup + property alias selectedItems: selectedItemsGroup + property alias hiddenItems: hiddenItemsGroup + + function hideSelected() { + if (selectedItemsGroup.count > 0) { + selectedItemsGroup.setGroups(0, selectedItemsGroup.count, ["hidden", "all"]) + } + } + + function selectAll() { + if (allItems.count > 0) { + allItems.addGroups(0, allItems.count, ["selected"]) + } + } + + function clearSelected() { + if (selectedItemsGroup.count > 0) { + selectedItemsGroup.remove(0, selectedItemsGroup.count) + } + } + + function clearHidden() { + if (hiddenItemsGroup.count > 0) { + hiddenItemsGroup.setGroups(0, hiddenItemsGroup.count, ["items", "all"]) + } + } + + function selectItem(index) { + allItems.addGroups(index, 1, ["selected"]) + } + + function deselectItem(index) { + allItems.removeGroups(index, 1, ["selected"]) + } + + groups: [ + DelegateModelGroup { + id: allItemsGroup + + name: "all" + includeByDefault: true + }, + DelegateModelGroup { + id: selectedItemsGroup + + name: "selected" + }, + DelegateModelGroup { + id: hiddenItemsGroup + + name: "hidden" + } + ] +} diff --git a/usr/share/jolla-email/pages/FolderAccessHint.qml b/usr/share/jolla-email/pages/FolderAccessHint.qml new file mode 100644 index 00000000..cfc7750f --- /dev/null +++ b/usr/share/jolla-email/pages/FolderAccessHint.qml @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2014 – 2019 Jolla Ltd. + * + * License: Proprietary + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 + +Loader { + id: root + + property bool pageActive + + anchors.fill: parent + active: counter.active && app.numberOfAccounts > 0 + sourceComponent: Component { + Item { + anchors.fill: parent + + Connections { + target: root + onPageActiveChanged: { + if (root.pageActive) { + touchInteractionHint.restart() + counter.increase() + root.pageActive = false + } + } + } + + InteractionHintLabel { + //: Swipe left to access your Email folders + //% "Swipe left to access your Email folders" + text: qsTrId("email-la-folder_access_hint") + anchors.bottom: parent.bottom + opacity: touchInteractionHint.running ? 1.0 : 0.0 + Behavior on opacity { FadeAnimation { duration: 1000 } } + } + TouchInteractionHint { + id: touchInteractionHint + + direction: TouchInteraction.Left + anchors.verticalCenter: parent.verticalCenter + } + } + } + FirstTimeUseCounter { + id: counter + limit: 3 + defaultValue: 1 // display hint twice for existing users + key: "/sailfish/email/folder_access_hint_count" + } +} diff --git a/usr/share/jolla-email/pages/FolderItem.qml b/usr/share/jolla-email/pages/FolderItem.qml new file mode 100644 index 00000000..561d2703 --- /dev/null +++ b/usr/share/jolla-email/pages/FolderItem.qml @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2013 – 2019 Jolla Ltd. + * + * License: Proprietary + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Nemo.Email 0.1 +import "utils.js" as Utils + +ListItem { + id: folderItem + + property bool enableFolderActions + property bool showUnreadCount + property string folderDisplayName: (typeof(isRoot) === 'undefined' || !isRoot) + ? Utils.standardFolderName(folderType, folderName) + : //: No parent folder + //% "None" + qsTrId("jolla-email-la-none_folder") + property bool isCurrentItem + + opacity: enabled ? 1 : (isCurrentItem ? Theme.opacityHigh : Theme.opacityLow) + contentHeight: Screen.sizeCategory >= Screen.Large ? Theme.itemSizeMedium : Theme.itemSizeSmall + + Label { + anchors { + left: parent.left + leftMargin: Theme.horizontalPageMargin + Theme.paddingLarge * folderNestingLevel + right: folderItemUnreadCount.left + rightMargin: Theme.paddingMedium + verticalCenter: parent.verticalCenter + } + text: folderDisplayName + font.pixelSize: Theme.fontSizeMedium + color: (highlighted || isCurrentItem) + ? Theme.highlightColor + : Theme.primaryColor + truncationMode: TruncationMode.Fade + } + + Label { + id: folderItemUnreadCount + visible: folderUnreadCount && showUnreadCount + anchors { + right: parent.right + rightMargin: Theme.horizontalPageMargin + verticalCenter: parent.verticalCenter + } + text: folderUnreadCount + font.pixelSize: Theme.fontSizeLarge + color: Theme.highlightColor + } +} diff --git a/usr/share/jolla-email/pages/FolderListPage.qml b/usr/share/jolla-email/pages/FolderListPage.qml new file mode 100644 index 00000000..9ee6768e --- /dev/null +++ b/usr/share/jolla-email/pages/FolderListPage.qml @@ -0,0 +1,140 @@ +/* + * Copyright (c) 2013 – 2019 Jolla Ltd. + * + * License: Proprietary + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Nemo.Email 0.1 + +Page { + id: folderListPage + + property alias accountKey: folderModel.accountKey + + signal folderClicked(var accessor) + signal deletingFolder(int id) + + FolderListModel { + id: folderModel + + onResyncNeeded: { + emailAgent.retrieveFolderList(accountKey) + } + } + + Connections { + target: emailAgent + onOnlineFolderActionCompleted: { + if (!success) { + emailAgent.retrieveFolderList(accountKey) // refresh folders list in case of error + } + } + } + + SilicaListView { + anchors.fill: parent + model: folderModel + header: PageHeader { + //: Folder List page title + //% "Folders" + title: qsTrId("jolla-email-he-folder_list_title") + } + + delegate: FolderItem { + enabled: canHaveMessages + showUnreadCount: true + onClicked: folderListPage.folderClicked(folderModel.folderAccessor(index)) + menu: ((canCreateChild || canRename || canMove || canDelete) && !preDeletionTimer.running) + ? contextMenuComponent : null + visible: !preDeletionTimer.running + Component { + id: contextMenuComponent + ContextMenu { + MenuItem { + visible: canCreateChild + //% "New subfolder" + text: qsTrId("jolla-email-fi-new_subfolder") + onClicked: { + pageStack.animatorPush(Qt.resolvedUrl("NewFolderDialog.qml"), { + parentFolderId: folderId, + parentFolderName: folderDisplayName, + folderModel: folderModel + }) + } + } + MenuItem { + visible: canRename + //% "Rename" + text: qsTrId("jolla-email-fi-rename_folder") + onClicked: { + pageStack.animatorPush(Qt.resolvedUrl("RenameFolderDialog.qml"), { + folderName: folderName, + folderId: folderId + }) + } + } + MenuItem { + visible: canMove + //% "Move" + text: qsTrId("jolla-email-fi-move_folder") + onClicked: { + pageStack.animatorPush(Qt.resolvedUrl("MoveFolderPage.qml"), { + folderModel: folderModel, + folderName: model.folderName, + folderId: model.folderId, + parentFolderId: model.parentFolderId + }) + } + } + MenuItem { + visible: canDelete + //% "Delete" + text: qsTrId("jolla-email-fi-delete_folder") + onClicked: _remove() + } + } + } + Timer { + id: preDeletionTimer + interval: 3000 // to avoid delegate flicking when remove is executed pretty fast + repeat: false + } + + function _remove() { + remorseDelete(function() { + folderListPage.deletingFolder(folderId) + preDeletionTimer.start() + emailAgent.deleteFolder(folderId) + }) + } + } + + PullDownMenu { + busy: emailAgent.currentSynchronizingAccountId === folderModel.accountKey + visible: folderModel.supportsFolderActions + MenuItem { + //% "Refresh folder list" + text: qsTrId("jolla-email-folder_list_refresh") + onClicked: emailAgent.retrieveFolderList(folderModel.accountKey) + } + MenuItem { + //% "New folder" + text: qsTrId("jolla-email-folder_new") + visible: folderModel.canCreateTopLevelFolders + onClicked: { + pageStack.animatorPush(Qt.resolvedUrl("NewFolderDialog.qml"), { + parentFolderId: 0, + //: No parent folder + //% "None" + parentFolderName: qsTrId("jolla-email-la-none_folder"), + folderModel: folderModel + }) + } + } + } + + VerticalScrollDecorator {} + } +} diff --git a/usr/share/jolla-email/pages/HtmlLoader.qml b/usr/share/jolla-email/pages/HtmlLoader.qml new file mode 100644 index 00000000..fcbe6c5e --- /dev/null +++ b/usr/share/jolla-email/pages/HtmlLoader.qml @@ -0,0 +1,199 @@ +/* + * Copyright (c) 2014 – 2019 Jolla Ltd. + * Copyright (c) 2019 Open Mobile Platform LLC. + * + * License: Proprietary + */ + +import QtQuick 2.2 +import Sailfish.Silica 1.0 +import Sailfish.Silica.private 1.0 +import Nemo.Email 0.1 +import Sailfish.WebView 1.0 +import Sailfish.WebEngine 1.0 +import Nemo.Configuration 1.0 + +Loader { + id: htmlLoader + + property bool showLoadProgress: true + readonly property int pageStatus: messageViewPage.status + property bool portrait + property bool isOutgoing + property bool isLocalFile + property string initialAction + readonly property bool showImages: app.accountsManagerActive || downloadImagesConfig.value + + property bool _wasLoaded + property var _html + property var _email + property AttachmentListModel attachmentsModel + + signal removeRequested + signal needToSendReadReceipt + + function load(email) { + if (email.contentType === EmailMessage.Plain) { + parser.text = email.body + _html = Qt.binding(plainTextAsHtml) + } else { + _html = email.htmlBody + parser.text = "" + } + + _email = email + + if (initialAction) { + _openComposer(initialAction, email, true) + initialAction = "" + } + + _finishLoad() + } + + function _finishLoad() { + if (!item) { + // Show progress indicator when we don't have an item. + showLoadProgress = true + messageViewPage.loaded = false + active = true + } else { + if (!_html.length) + return + + _wasLoaded = true + loadingTimer.restart() + } + } + + function markAsRead() { + if (_email) { + if (!_email.read && _email.requestReadReceipt) { + needToSendReadReceipt() + } + _email.read = true + } + } + + function resurrect() { + // Don't resurrect if item was not never loaded or we already + // have an item (nothing was released). + if (!_wasLoaded || item) { + return + } + + if (!item) { + active = true + } + + _finishLoad() + } + + function plainTextAsHtml() { + return "
" + parser.linkedText + "
" + } + + function _openComposer(action, immediately) { + pageStack.animatorPush( + Qt.resolvedUrl("ComposerPage.qml"), + { popDestination: previousPage, action: action, originalMessageId: _email.messageId }, + immediately ? PageStackAction.Immediate : PageStackAction.Animated) + } + + Component.onCompleted: { + WebEngineSettings.autoLoadImages = Qt.binding(function() { + return showImages + }) + } + + sourceComponent: HtmlViewer { + anchors.fill: parent + interactive: messageViewPage.loaded + portrait: htmlLoader.portrait + attachmentsModel: htmlLoader.attachmentsModel + isOutgoing: htmlLoader.isOutgoing + isLocalFile: htmlLoader.isLocalFile + email: htmlLoader._email + htmlBody: htmlLoader._html + showImages: htmlLoader.showImages + + onVisuallyCommittedChanged: { + if (visuallyCommitted) { + showLoadProgress = false + if (pageStatus == PageStatus.Active && (!loadingTimer.running || loaded)) { + messageViewPage.loaded = true + loadingTimer.stop() + } + } + } + + onComposerRequested: htmlLoader._openComposer(action, false) + onRemoveRequested: htmlLoader.removeRequested() + } + + // Activated from load() + active: false + onActiveChanged: { + if (!active) { + messageViewPage.loaded = false + } + } + + onItemChanged: { + if (item) { + _finishLoad() + } + } + + asynchronous: true + + + LinkParser { + id: parser + } + + ConfigurationValue { + id: downloadImagesConfig + key: "/apps/jolla-email/settings/downloadImages" + defaultValue: false + } + + Timer { + id: loadingTimer + interval: 800 + onTriggered: { + if (pageStatus == PageStatus.Active) { + messageViewPage.loaded = true + } + } + } + + Timer { + id: backgroundTimer + + readonly property bool canRelease: !Qt.application.active && htmlLoader.item + + interval: 1000 * 10 // 10sec + onTriggered: { + if (canRelease) { + htmlLoader.active = false + } + } + + onCanReleaseChanged: { + if (canRelease) { + restart() + } + } + } + + Connections { + target: Qt.application + onActiveChanged: { + if (Qt.application.active) { + backgroundTimer.stop() + htmlLoader.resurrect() + } + } + } +} diff --git a/usr/share/jolla-email/pages/HtmlViewer.qml b/usr/share/jolla-email/pages/HtmlViewer.qml new file mode 100644 index 00000000..ea262470 --- /dev/null +++ b/usr/share/jolla-email/pages/HtmlViewer.qml @@ -0,0 +1,273 @@ +/* + * Copyright (c) 2013 – 2019 Jolla Ltd. + * Copyright (c) 2019 - 2020 Open Mobile Platform LLC. + * + * License: Proprietary + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Sailfish.Silica.private 1.0 +import Sailfish.TextLinking 1.0 +import Sailfish.WebEngine 1.0 +import Sailfish.WebView 1.0 +import Nemo.Email 0.1 + +Item { + id: view + + property EmailMessage email + property string htmlBody + + // Avoid blocking webview content during accounts creation + property bool showImages + property bool showImagesButton + property bool portrait + property AttachmentListModel attachmentsModel + property bool isOutgoing + property bool isLocalFile + property bool hasImages + property bool orientationTransitionRunning: flickable.webView.webViewPage + && flickable.webView.webViewPage.orientationTransitionRunning + + property bool pageActive: flickable.webView.webViewPage + && flickable.webView.webViewPage.status === PageStatus.Active + + property alias interactive: flickable.interactive + + property bool visuallyCommitted + property bool complete + + signal removeRequested + signal composerRequested(string action) + + Component.onCompleted: { + complete = true + } + + onEmailChanged: showImagesButton = false + + onHtmlBodyChanged: { + if (!htmlBody) { + return + } + visuallyCommitted = false + view.showImagesButton = false + view.hasImages = false + + // Change the preference before loading new html body + flickable.webView.loadHtml(htmlBody) + } + + onPageActiveChanged: { + if (!pageActive) { + flickable.webView.clearSelection() + } + } + + WebViewFlickable { + id: flickable + + width: view.width + height: view.height + // Don't resize the web view when the keyboard is open. + contentHeight: view.portrait ? Screen.height : Screen.width + + webView { + x: view.width - webView.width + width: view.width - landscapeInviteContainer.implicitWidth + + footerMargin: footer.open && !view.isLocalFile ? footer.height : 0 + + // Hide the web view while the width changes as there's a lag time between when the + // QQuickItem resizes and the view refreshes and in the interim the content will appear + // stretched. + Behavior on width { + enabled: view.complete && !view.orientationTransitionRunning + SequentialAnimation { + id: widthChangeAnimation + FadeAnimation { + target: resizeWhiteout + to: 1 + duration: 100 + } + PropertyAction {} + PauseAnimation { + duration: 200 + } + FadeAnimation { + target: resizeWhiteout + to: 0 + duration: 100 + } + } + } + + Behavior on footerMargin { + enabled: view.complete && !view.orientationTransitionRunning + + NumberAnimation { + id: footerAnimation + + easing.type: Easing.InOutQuad + duration: 100 + } + } + + onFirstPaint: visuallyCommitted = true + + onViewInitialized: { + webView.loadFrameScript(Qt.resolvedUrl("webviewframescript.js")); + webView.addMessageListener("JollaEmail:DocumentHasImages") + webView.addMessageListener("embed:OpenLink") + } + + onChromeChanged: { + if (webView.chrome === footer.open) { + // Ignore the change if the values are equal. + } else if (webView.chrome) { + footer.open = true + } else { + footer.open = false + } + } + + + onTextSelectionActiveChanged: { + if (webView.textSelectionActive) { + footer.open = true + } + } + + onRecvAsyncMessage: { + switch (message) { + case "JollaEmail:DocumentHasImages": + if (!view.hasImages) { + view.hasImages = true + view.showImagesButton = !view.showImages + } + break + case "embed:OpenLink": + linkHandler.handleLink(data.uri) + break + default: + break + } + } + } + + header: MessageViewHeader { + id: messageHeader + + width: view.width + email: view.email + + contentX: landscapeInviteContainer.width + + isOutgoing: view.isOutgoing + attachmentsModel: view.attachmentsModel + showLoadImages: view.showImagesButton + heightBehaviorEnabled: view.complete && !view.orientationTransitionRunning + + onClicked: pageStack.navigateForward() + + onLoadImagesClicked: { + if (showLoadImages) { + view.showImagesButton = false + WebEngineSettings.autoLoadImages = true + flickable.webView.reload() + } + } + + onLoadImagesCloseClicked: { + view.showImagesButton = false + } + + Loader { + id: inviteLoader + + parent: view.portrait ? messageHeader.contentItem : landscapeInviteContainer + + active: messageHeader.inlineInvitation + + sourceComponent: CalendarInvite { + width: view.portrait ? view.width : Theme.buttonWidthLarge + height: view.portrait ? undefined : view.height - messageHeader.contentY + + preferredButtonWidth: view.portrait + ? Theme.buttonWidthExtraSmall + : Theme.buttonWidthSmall + + event: messageHeader.event + occurrence: messageHeader.occurrence + } + } + } + + VerticalScrollDecorator { color: Theme.highlightBackgroundColor } + HorizontalScrollDecorator { color: Theme.highlightBackgroundColor } + + Column { + id: landscapeInviteContainer + + y: flickable.webView.y + } + + Rectangle { + x: landscapeInviteContainer.x + y: flickable.webView.y + z: -100 + width: view.width - x + height: view.height - y + + color: flickable.webView.backgroundColor + visible: widthChangeAnimation.running + } + + Rectangle { + id: resizeWhiteout + + x: landscapeInviteContainer.width + y: flickable.webView.y + width: view.width - x + height: flickable.webView.height + + color: flickable.webView.backgroundColor + + opacity: 0 + } + + LinkHandler { + id: linkHandler + } + + MessageViewFooter { + id: footer + + x: landscapeInviteContainer.width + y: flickable.contentHeight - flickable.webView.footerMargin + portrait: view.portrait + + width: view.width - x + + textSelectionController: flickable.webView.textSelectionController + showReplyAll: view.email.multipleRecipients + + open: true + + onOpenChanged: { + if (flickable.webView.chrome !== open) { + flickable.webView.chrome = open + } + } + + onForward: view.composerRequested('forward') + onReplyAll: view.composerRequested('replyAll') + onReply: view.composerRequested('reply') + onDeleteEmail: { + pageStack.pop() + view.removeRequested() + } + } + } +} diff --git a/usr/share/jolla-email/pages/LoadImagesItem.qml b/usr/share/jolla-email/pages/LoadImagesItem.qml new file mode 100644 index 00000000..fde28778 --- /dev/null +++ b/usr/share/jolla-email/pages/LoadImagesItem.qml @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2018 – 2019 Jolla Ltd. + * Copyright (c) 2020 Open Mobile Platform LLC. + * + * License: Proprietary + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 + +BackgroundItem { + id: loadImagesArea + + signal closeClicked() + + height: Theme.itemSizeExtraSmall + + Icon { + id: fileImage + x: Theme.horizontalPageMargin + anchors.verticalCenter: parent.verticalCenter + source: "image://theme/icon-m-file-image" + } + + Label { + anchors { + left: fileImage.right + right: closeButton.left + margins: Theme.paddingMedium + } + height: loadImagesArea.height + + //% "Load images" + text: qsTrId("jolla-email-la-load_images") + font.pixelSize: Theme.fontSizeSmall + verticalAlignment: Text.AlignVCenter + truncationMode: TruncationMode.Fade + } + + IconButton { + id: closeButton + + icon.source: "image://theme/icon-m-input-remove" + + width: loadImagesArea.height + height: loadImagesArea.height + + x: loadImagesArea.width - width - Theme.horizontalPageMargin + + onClicked: loadImagesArea.closeClicked() + } +} diff --git a/usr/share/jolla-email/pages/MessageInfo.qml b/usr/share/jolla-email/pages/MessageInfo.qml new file mode 100644 index 00000000..d8c4f414 --- /dev/null +++ b/usr/share/jolla-email/pages/MessageInfo.qml @@ -0,0 +1,236 @@ +/* + * Copyright (c) 2013 – 2019 Jolla Ltd. + * Copyright (c) 2019 Open Mobile Platform LLC. + * + * License: Proprietary + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Nemo.Email 0.1 +import org.nemomobile.calendar 1.0 +import Sailfish.TextLinking 1.0 + +Page { + id: root + + property EmailMessage message + readonly property var messageToAddresses: message ? message.to : [] + readonly property var messageCcAddresses: message ? message.cc : [] + property QtObject calendarEvent + property bool isLocalFile + + ImportModel { + icsString: message && message.calendarInvitationSupportsEmailResponses + ? message.calendarInvitationBody : "" + onCountChanged: { + if (count > 0) { + root.calendarEvent = getEvent(0) + } else { + root.calendarEvent = null + } + } + } + + SilicaFlickable { + anchors.fill: parent + contentHeight: content.height + Theme.paddingLarge + + Column { + id: content + + width: parent.width + spacing: Theme.paddingLarge + + PageHeader { + //: Message info header + //% "Message info" + title: qsTrId("jolla-email-he-message_info") + } + + MessageInfoLabel { + text: message ? message.subject : "" + font.pixelSize: Theme.fontSizeMedium + maximumLineCount: 10 + } + + Column { + width: parent.width + + LinkedText { + x: Theme.horizontalPageMargin + width: parent.width - 2*x + plainText: message ? (message.fromAddress != "" ? message.fromDisplayName + " <" + message.fromAddress + ">" + : message.fromDisplayName) : "" + font.pixelSize: Theme.fontSizeSmall + elide: Text.ElideRight + wrapMode: Text.Wrap + } + + MessageInfoLabel { + text: message ? Format.formatDate(message.date, Formatter.Timepoint) : "" + } + } + + MessageInfoSection { + visible: calendarEvent !== null + //: Start and end time of a meeting + //% "When:" + headerText: qsTrId("jolla-email-la-cal-when") + bodyText: calendarEvent ? (Format.formatDate( + calendarEvent.startTime, Formatter.Timepoint) + + " - " + + Format.formatDate( + calendarEvent.endTime, Formatter.Timepoint)) : "" + } + + MessageInfoLinkedText { + visible: calendarEvent && calendarEvent.organizer != "" + //: Meeting invitation organizer address + //% "Organizer:" + headerText: qsTrId("jolla-email-la-cal-organizer") + plainText: calendarEvent ? calendarEvent.organizer : "" + } + + MessageInfoLinkedText { + visible: message && message.replyTo != "" + //: Reply to address + //% "Reply to:" + headerText: qsTrId("jolla-email-la-replyTo") + plainText: message ? message.replyTo : "" + } + + MessageInfoRepeater { + visible: messageToAddresses != "" + headerText: { + if (calendarEvent) { + //: 'Mandatory: ' recipients label for calendar invitation + //% "Mandatory:" + return qsTrId("jolla-email-la-cal-mandatory_info") + } else { + //: 'To: ' recipients label + //% "To:" + return qsTrId("jolla-email-la-to_info") + } + } + model: messageToAddresses + } + + MessageInfoRepeater { + visible: messageCcAddresses != "" + headerText: { + if (calendarEvent) { + //: 'Optional: ' recipients label for calendar invitation + //% "Optional:" + return qsTrId("jolla-email-la-cal-optional_info") + } else { + //: 'Cc: ' recipients label + //% "Cc:" + return qsTrId("jolla-email-la-cc_info") + } + } + model: messageCcAddresses + } + + MessageInfoSection { + //: 'Importance: ' label + //% "Importance:" + headerText: qsTrId("jolla-email-la-importance") + bodyText: message ? priorityText(message.priority) : "" + function priorityText(priority) { + if (priority === EmailMessageListModel.HighPriority) { + //: Message priority high + //% "High" + return qsTrId("jolla-email-la-priority_high") + } else if (priority === EmailMessageListModel.LowPriority) { + //: Message priority low + //% "Low" + return qsTrId("jolla-email-la-priority_low") + } else { + //: Message priority normal + //% "Normal" + return qsTrId("jolla-email-la-priority_Normal") + } + } + } + + MessageInfoSection { + visible: calendarEvent + //: 'Secrecy: ' label for calendar invitation + //% "Secrecy:" + headerText: qsTrId("jolla-email-la-cal-secrecy") + bodyText: secrecyText(calendarEvent ? calendarEvent.secrecy : CalendarEvent.SecrecyPublic) + function secrecyText(secrecy) { + switch (secrecy) { + case CalendarEvent.SecrecyPrivate: + //: Invitation secrecy private + //% "Private" + return qsTrId("jolla-email-la-cal-secrecy_private") + case CalendarEvent.SecrecyConfidential: + //: Invitation secrecy confidential + //% "Confidential" + return qsTrId("jolla-email-la-cal-secrecy_confidential") + default: + //: Invitation secrecy public + //% "Public" + return qsTrId("jolla-email-la-cal-secrecy_public") + } + } + } + + MessageInfoSection { + visible: calendarEvent + //: 'Repeat: ' label for calendar + //% "Repeat:" + headerText: qsTrId("jolla-email-la-cal-repeat") + bodyText: recurText(calendarEvent ? calendarEvent.recur : CalendarEvent.RecurOnce) + + function recurText(recur) { + if (calendarEvent) { + switch (calendarEvent.recur) { + case CalendarEvent.RecurDaily: + //% "Every Day" + return qsTrId("jolla-email-la-cal-recurrence-every_day") + case CalendarEvent.RecurWeekly: + //% "Every Week" + return qsTrId("jolla-email-la-cal-recurrence-every_week") + case CalendarEvent.RecurBiweekly: + //% "Every 2 Weeks" + return qsTrId("jolla-email-la-cal-recurrence-every_2_weeks") + case CalendarEvent.RecurMonthly: + //% "Every Month" + return qsTrId("jolla-email-la-cal-recurrence-every_month") + case CalendarEvent.RecurYearly: + //% "Every Year" + return qsTrId("jolla-email-la-cal-recurrence-every_year") + case CalendarEvent.RecurCustom: + //% "Custom" + return qsTrId("jolla-email-la-cal-recurrence-custom") + } + } + //: Recurrence - not set (once) text + //% "Once" + return qsTrId("jolla-email-la-cal-recurrence-once") + } + } + + MessageInfoSection { + //: 'Account: ' label + //% "Account:" + headerText: qsTrId("jolla-email-la-account") + bodyText: message ? mailAccountListModel.displayNameFromAccountId(message.accountId) : "" + visible: message && mailAccountListModel.indexFromAccountId(message.accountId) >= 0 + } + + MessageInfoSection { + //: Message 'Size: ' label + //% "Size:" + headerText: qsTrId("jolla-email-la-message_size") + bodyText: Format.formatFileSize(message ? message.size : 0) + visible: !isLocalFile + } + } + + VerticalScrollDecorator {} + } +} diff --git a/usr/share/jolla-email/pages/MessageInfoLabel.qml b/usr/share/jolla-email/pages/MessageInfoLabel.qml new file mode 100644 index 00000000..cec53bb4 --- /dev/null +++ b/usr/share/jolla-email/pages/MessageInfoLabel.qml @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2013 – 2019 Jolla Ltd. + * + * License: Proprietary + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 + +Label { + property bool header + + x: Theme.horizontalPageMargin + width: parent.width - 2*x + font.pixelSize: Theme.fontSizeSmall + elide: Text.ElideRight + maximumLineCount: 2 + wrapMode: Text.Wrap + color: header ? Theme.secondaryHighlightColor : Theme.highlightColor +} diff --git a/usr/share/jolla-email/pages/MessageInfoLinkedText.qml b/usr/share/jolla-email/pages/MessageInfoLinkedText.qml new file mode 100644 index 00000000..ad7041c4 --- /dev/null +++ b/usr/share/jolla-email/pages/MessageInfoLinkedText.qml @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2018 – 2019 Jolla Ltd. + * + * License: Proprietary + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Sailfish.TextLinking 1.0 + +Column { + property alias headerText: header.text + property alias plainText: body.plainText + + width: parent.width + + MessageInfoLabel { + id: header + font.pixelSize: Theme.fontSizeMedium + header: true + } + + LinkedText { + id: body + x: Theme.horizontalPageMargin + width: parent.width - 2*x + font.pixelSize: Theme.fontSizeSmall + wrapMode: Text.Wrap + } +} diff --git a/usr/share/jolla-email/pages/MessageInfoRepeater.qml b/usr/share/jolla-email/pages/MessageInfoRepeater.qml new file mode 100644 index 00000000..916a19ba --- /dev/null +++ b/usr/share/jolla-email/pages/MessageInfoRepeater.qml @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2018 – 2019 Jolla Ltd. + * + * License: Proprietary + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Sailfish.TextLinking 1.0 + +Column { + property alias headerText: header.text + property alias model: repeater.model + + width: parent.width + + MessageInfoLabel { + id: header + font.pixelSize: Theme.fontSizeMedium + header: true + } + + Repeater { + id: repeater + LinkedText { + x: Theme.horizontalPageMargin + width: parent.width - 2*x + plainText: model.modelData + font.pixelSize: Theme.fontSizeSmall + elide: Text.ElideRight + wrapMode: Text.Wrap + } + } +} diff --git a/usr/share/jolla-email/pages/MessageInfoSection.qml b/usr/share/jolla-email/pages/MessageInfoSection.qml new file mode 100644 index 00000000..db5dfc6e --- /dev/null +++ b/usr/share/jolla-email/pages/MessageInfoSection.qml @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2018 – 2019 Jolla Ltd. + * + * License: Proprietary + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 + +Column { + property alias headerText: header.text + property alias bodyText: body.text + + width: parent.width + MessageInfoLabel { + id: header + font.pixelSize: Theme.fontSizeMedium + header: true + } + + MessageInfoLabel { + id: body + } +} diff --git a/usr/share/jolla-email/pages/MessageItem.qml b/usr/share/jolla-email/pages/MessageItem.qml new file mode 100644 index 00000000..8db19979 --- /dev/null +++ b/usr/share/jolla-email/pages/MessageItem.qml @@ -0,0 +1,236 @@ +/* + * Copyright (c) 2013 - 2019 Jolla Ltd. + * Copyright (c) 2019 - 2021 Open Mobile Platform LLC. + * + * License: Proprietary + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Nemo.Email 0.1 +import com.jolla.email 1.1 +import Jolla.Email 1.0 +import "utils.js" as Utils + +ListItem { + id: messageItem + + property bool selectMode + property bool showRecipientsName + property alias primaryLine: senderName.text + property alias secondaryLine: subjectText.text + property alias date: msgDateTime.text + property bool isDraftsFolder + property string searchString + property bool highlightSender + property bool highlightRecipients + property bool highlightSubject + property bool highlightBody + + signal emailViewerRequested(int messageId) + + function remove() { + BatchedMessageDeletion.addMessage(model.messageId) + + var remorseItem = remorseDelete(function() { + animateRemoval() + BatchedMessageDeletion.messageReadyForDeletion(model.messageId) + BatchedMessageDeletion.run(emailAgent) + }) + remorseItem.canceled.connect(function() { + BatchedMessageDeletion.removeMessage(model.messageId) + }) + } + + onClicked: { + if (!selectMode) { + if (isDraftsFolder) { + pageStack.animatorPush(Qt.resolvedUrl("ComposerPage.qml"), + { messageId: model.messageId, + originalMessageId: model.messageId, + draft: true, + draftRemoveCallback: remove }) + } else { + emailViewerRequested(model.messageId) + } + } + } + + contentHeight: content.height + content.y + Theme.paddingMedium + menu: contextMenuComponent + highlighted: menuOpen || down || (model.selected && selectMode) + + GlassItem { + visible: !model.readStatus + width: Theme.itemSizeSmall + height: Theme.itemSizeSmall + anchors.horizontalCenter: parent.left + y: content.y + senderName.height/2 - height/2 + radius: 0.14 + falloffRadius: 0.13 + color: Theme.highlightColor + } + + Column { + id: content + y: Theme.paddingMedium + spacing: -Math.round(Theme.paddingSmall/2) + anchors { + left: parent.left + leftMargin: Theme.horizontalPageMargin + right: parent.right + rightMargin: Theme.horizontalPageMargin + } + Item { + height: senderName.height + width: parent.width + Label { + id: senderName + + text: showRecipientsName + ? (model.recipientsDisplayName.toString() != "" + ? Theme.highlightText(model.recipientsDisplayName.toString(), + (highlightRecipients ? searchString : ""), + Theme.highlightColor) + : //% "No recipients" + qsTrId("jolla-email-la-no_recipient")) + : Theme.highlightText(model.senderDisplayName, (highlightSender ? searchString : ""), Theme.highlightColor) + textFormat: Text.StyledText + anchors { + left: parent.left + right: msgDateTime.left + rightMargin: Theme.paddingLarge + } + truncationMode: TruncationMode.Fade + } + + Label { + id: msgDateTime + text: Format.formatDate(model.qDateTime, Formatter.TimeValue) + font.pixelSize: Theme.fontSizeExtraSmall + anchors { + right: parent.right + baseline: senderName.baseline + } + } + Row { + id: icons + spacing: Theme.paddingSmall + anchors { + top: senderName.baseline + topMargin: Theme.paddingSmall + right: parent.right + } + HighlightImage { + visible: model.replied || model.repliedAll || model.forwarded + source: model.forwarded && (model.replied || model.repliedAll) + ? "image://theme/icon-s-message-reply-forward" + : model.forwarded ? "image://theme/icon-s-message-forward" + : "image://theme/icon-s-message-reply" + } + + HighlightImage { + visible: model.priority != EmailMessageListModel.NormalPriority + source: Utils.priorityIcon(model.priority) + } + + HighlightImage { + visible: model.hasAttachments + source: "image://theme/icon-s-attach?" + } + HighlightImage { + visible: model.hasCalendarInvitation + source: "image://theme/icon-s-date?" + } + HighlightImage { + visible: model.hasCalendarCancellation + source: "image://theme/icon-s-calendar-cancelled?" + } + HighlightImage { + visible: model.hasSignature && EmailUtils.emailAppCryptoEnabled + source: "image://theme/icon-s-certificates" + } + HighlightImage { + visible: model.isEncrypted + source: "image://theme/icon-s-secure" + } + } + } + Label { + id: subjectText + + text: model.parsedSubject != "" + ? Theme.highlightText(model.parsedSubject, (highlightSubject ? searchString : ""), Theme.highlightColor) + : //: Empty subject + //% "(Empty subject)" + qsTrId("jolla-email-la-no_subject") + textFormat: Text.StyledText + font.pixelSize: Theme.fontSizeSmall + opacity: model.readStatus ? Theme.opacityHigh : 1.0 + width: parent.width - icons.width + anchors { + left: parent.left + right: parent.right + rightMargin: Screen.sizeCategory >= Screen.Large + ? Theme.paddingLarge + icons.width + : Theme.paddingMedium + icons.width + } + truncationMode: TruncationMode.Fade + } + + Label { + text: model.preview != "" + ? Theme.highlightText(model.preview, (highlightBody ? searchString : ""), Theme.primaryColor) + : // it should not show empty preview when preview is not retrived yet, first sync for e.g + model.isEncrypted + ? //% "(Encrypted content)" + qsTrId("jolla-email-la-encrypted_preview") + : //: Empty preview + //% "(Empty preview)" + qsTrId("jolla-email-la-no_preview") + textFormat: Text.StyledText + font.pixelSize: Theme.fontSizeSmall + color: Theme.highlightColor + opacity: model.readStatus ? Theme.opacityHigh : 1.0 + + maximumLineCount: Screen.sizeCategory >= Screen.Large ? 1 : ( model.readStatus ? 2 : 3) + lineHeight: subjectText.height - Math.round(Theme.paddingSmall/2) + lineHeightMode: Text.FixedHeight + width: parent.width + wrapMode: Text.Wrap + elide: Text.ElideRight + } + } + + Component { + id: contextMenuComponent + ContextMenu { + MenuItem { + //% "Move to" + text: qsTrId("jolla-email-me-move_to") + onClicked: { + pageStack.animatorPush(Qt.resolvedUrl("MoveToPage.qml"), + { msgId: model.messageId, + accountKey: emailAgent.accountIdForMessage(model.messageId)}) + } + } + MenuItem { + visible: model.readStatus + //% "Mark as unread" + text: qsTrId("jolla-email-me-mark-unread") + onDelayedClick: emailAgent.markMessageAsUnread(model.messageId) + } + MenuItem { + visible: !model.readStatus + //% "Mark as read" + text: qsTrId("jolla-email-me-mark-read") + onDelayedClick: emailAgent.markMessageAsRead(model.messageId) + } + MenuItem { + //% "Delete" + text: qsTrId("jolla-email-me-delete") + onClicked: messageItem.remove() + } + } + } +} diff --git a/usr/share/jolla-email/pages/MessageListHeader.qml b/usr/share/jolla-email/pages/MessageListHeader.qml new file mode 100644 index 00000000..dea1b9b4 --- /dev/null +++ b/usr/share/jolla-email/pages/MessageListHeader.qml @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2013 – 2019 Jolla Ltd. + * + * License: Proprietary + */ + +import QtQuick 2.6 +import Sailfish.Silica 1.0 + +PageHeader { + id: root + + property alias folderName: folderText.text + property int count + property string errorText + + title: count > 0 ? count.toLocaleString() : "" + titleColor: palette.highlightColor + interactive: true // don't wait until folder list is pushed to indicate the header is interactive + highlighted: defaultHighlighted || folderMouseArea.containsMouse + + height: Math.max(_preferredHeight, _titleItem.y + _titleItem.height + ((errorText.length > 0) ? errorLabel.height : 0) + Theme.paddingMedium) + + Behavior on height { NumberAnimation { duration: 250; easing.type: Easing.InOutQuad } } + + Label { + id: folderText + + parent: root.extraContent + + x: parent.width - width + y: Math.floor((root._preferredHeight - height) / 2) + + width: Math.min(implicitWidth, parent.width) + + rightPadding: root.title !== "" ? Theme.paddingMedium : 0 + + truncationMode: TruncationMode.Fade + + font: root._titleItem.font + color: highlighted ? palette.highlightColor : palette.primaryColor + + MouseArea { + id: folderMouseArea + + anchors { + verticalCenter: parent.verticalCenter + right: parent.right + } + width: parent.implicitWidth + Theme.paddingLarge + height: root.height + + onClicked: pageStack.navigateForward() + } + } + + // The styling matches the description field taken from + // sailfish-silica/components/private/PageHeaderDescription.qml + // It's reimplemented here so that the opacity can be animated + Item { + id: errorItem + + x: root.leftMargin + y: root._titleItem.y + root._titleItem.height + width: root.width - root.leftMargin - root.rightMargin + + opacity: ((root.errorText.length > 0) ? 1 : 0) + Behavior on opacity { FadeAnimation {} } + + Icon { + id: errorIcon + + x: errorLabel.x - width - Theme.paddingSmall + y: (errorLabel.height - height) / 2 + + source: "image://theme/icon-s-warning" + color: palette.secondaryHighlightColor + } + + Label { + id: errorLabel + + x: errorItem.width - width + + width: Math.min(implicitWidth, errorItem.width - errorIcon.width - Theme.paddingSmall) + + font.pixelSize: Theme.fontSizeSmall + color: palette.secondaryHighlightColor + horizontalAlignment: Text.AlignRight + truncationMode: TruncationMode.Fade + } + } + + onErrorTextChanged: { + if (errorText.length > 0) { + errorLabel.text = errorText + } + } +} diff --git a/usr/share/jolla-email/pages/MessageListPage.qml b/usr/share/jolla-email/pages/MessageListPage.qml new file mode 100644 index 00000000..883f98a6 --- /dev/null +++ b/usr/share/jolla-email/pages/MessageListPage.qml @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2012 – 2019 Jolla Ltd. + * Copyright (c) 2019 Open Mobile Platform LLC. + * + * License: Proprietary + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Nemo.Email 0.1 + +Page { + id: messageListPage + + property alias folderAccessor: messageListView.folderAccessor + property Page folderListPage + + function removeMessage(id) { + messageListView.removeMessage(id) + } + + onStatusChanged: { + if (status === PageStatus.Active) { + app.coverMode = "mainView" + if (!app.syncInProgress + && Qt.application.state === Qt.ApplicationActive + && messageListView.folderType !== EmailFolder.InvalidFolder) { + messageListView.synchronize(false) + } + + if (folderListPage && !pageStack.nextPage(messageListPage)) { + pageStack.pushAttached(folderListPage) + } + } + } + + onFolderListPageChanged: { + if (status === PageStatus.Active && folderListPage && + !pageStack.nextPage(messageListPage)) { + pageStack.pushAttached(folderListPage) + } + } + + MessageListView { + id: messageListView + } + + Loader { + anchors.fill: parent + asynchronous: true + // Note: doesn't update if content changes to different account later + Component.onCompleted: { + setSource(Qt.resolvedUrl("FolderListPage.qml"), {accountKey: messageListView.accountId}) + } + + onLoaded: folderListPage = item + onStatusChanged: { + if (status == Loader.Error && sourceComponent) { + console.log(sourceComponent.errorString()) + } + } + onItemChanged: { + if (item) { + item.folderClicked.connect(function(accessor) { + messageListView.folderAccessor = accessor + + // if the message list is sorted by sender and we are navigating to a outgoing folder, + // default to sort by recipients since sort by sender is not available for outgoing folders + if (messageListView.isOutgoingFolder && messageListView.sortBy == EmailMessageListModel.Sender) { + messageListView.sortBy = EmailMessageListModel.Recipients + } else if (!messageListView.isOutgoingFolder && messageListView.sortBy == EmailMessageListModel.Recipients) { + messageListView.sortBy = EmailMessageListModel.Sender + } + + pageStack.navigateBack() + }) + + item.deletingFolder.connect(function(id) { + if (id === messageListView.folderId) { + var inbox = emailAgent.inboxFolderId(messageListView.accountId) + if (inbox > 0) { + var accessor = emailAgent.accessorFromFolderId(inbox) + messageListView.folderAccessor = accessor + } else { + console.log("Delete current folder: unable to set current folder to Inbox") + } + } + }) + } + } + } + + FolderAccessHint { + pageActive: messageListPage.status == PageStatus.Active && Qt.application.active && folderListPage + } +} diff --git a/usr/share/jolla-email/pages/MessageListView.qml b/usr/share/jolla-email/pages/MessageListView.qml new file mode 100644 index 00000000..cb43b823 --- /dev/null +++ b/usr/share/jolla-email/pages/MessageListView.qml @@ -0,0 +1,396 @@ +/* + * Copyright (c) 2013 – 2019 Jolla Ltd. + * Copyright (c) 2019 Open Mobile Platform LLC. + * + * License: Proprietary + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Nemo.Email 0.1 +import "utils.js" as Utils + +SilicaListView { + id: messageListView + + property alias folderAccessor: messageModel.folderAccessor + property alias sortBy: messageModel.sortBy + property alias accountId: folder.parentAccountId + property alias messageCount: messageModel.count + readonly property alias folderId: folder.folderId + readonly property alias folderType: folder.folderType + readonly property bool isDraftsFolder: folderType === EmailFolder.DraftsFolder + readonly property bool isOutboxFolder: folderType === EmailFolder.OutboxFolder + readonly property alias isOutgoingFolder: folder.isOutgoingFolder + readonly property bool showGetMoreMails: !(mailAccountListModel.customFieldFromAccountId("showMoreMails", accountId) == "false") + readonly property int fetchMore: 50 + readonly property int getMore: 20 + property bool waitToFetchMore + property bool errorOccurred + property string lastErrorText + readonly property bool updating: emailAgent.currentSynchronizingAccountId === accountId + + function sendAll() { + if (isOutboxFolder) { + emailAgent.processSendingQueue(accountId) + } else { + console.log("Trying to process sending queue for something else than outbox.") + } + } + + function synchronize(force) { + if (isOutboxFolder) + return + + var key = "sync_" + accountId.toString() + "_" + folder.folderId.toString() + var now = Date.now() + + if (typeof force == "boolean" && !force && (!Utils.canSyncFolder(key, now) || !emailAgent.isOnline())) { + return + } + + // Store both manual sync and navigation between folders + // time stamps. + Utils.updateRecentSync(key, now) + + emailAgent.exportUpdates(accountId) + emailAgent.retrieveMessageList(accountId, folder.folderId) + } + + function removeMessage(id) { + var index = messageModel.indexFromMessageId(id) + currentIndex = index + positionViewAtIndex(index, ListView.Contain) + currentItem.remove() + } + + anchors.fill: parent + + header: MessageListHeader { + folderName: messageListView.updating + //: Updating header + //% "Updating..." + ? qsTrId("jolla-email-he-updating") + : Utils.standardFolderName(folder.folderType, folder.displayName) + count: messageListView.updating ? 0 : folder.folderUnreadCount + errorText: messageListView.errorOccurred ? messageListView.lastErrorText : "" + + onHeightChanged: { + // If an error message causes the header height to change, compensate by moving the scroll position + if ((messageListView.contentY >= messageListView.originY) + && (messageListView.contentY < messageListView.originY + Theme.itemSizeSmall) + && (!messageListView.dragging)) { + messageListView.contentY = messageListView.originY + } + } + } + + footer: Item { + width: messageListView.width + height: Theme.paddingLarge + } + + section { + property: _sectionProperty() + criteria: _sectionCriteria() + + delegate: SectionHeader { + text: _sectionDelegateText(section) + height: text === "" ? 0 : Theme.itemSizeSmall + horizontalAlignment: Text.AlignHCenter + font.capitalization: (messageModel.sortBy == EmailMessageListModel.Sender + || messageModel.sortBy == EmailMessageListModel.Subject) + ? Font.AllUppercase : Font.MixedCase + + function _sectionDelegateText(section) { + var sortBy = messageModel.sortBy + if (sortBy === EmailMessageListModel.Time) { + return Format.formatDate(section, Formatter.TimepointSectionRelative) + } else if (sortBy === EmailMessageListModel.Size) { + if (section == "0") { + //: Section header for small size emails + //% "Small (<100 KB)" + return qsTrId("jolla-email-la-small_size") + } else if (section == "1") { + //: Section header for medium size emails + //% "Medium (100-500 KB)" + return qsTrId("jolla-email-la-medium_size") + } else { + //: Section header for large size emails + //% "Large (>500 KB)" + return qsTrId("jolla-email-la-large_size") + } + } else if (sortBy === EmailMessageListModel.ReadStatus) { + if (section == "true") { + //: Read emails section header + //% "Read emails" + return qsTrId("jolla-email-la-read_email") + } else { + //: Unread emails section header + //% "Unread emails" + return qsTrId("jolla-email-la-unread_email") + } + } else if (sortBy === EmailMessageListModel.Priority) { + // assuming javascript handling string and enum comparison + if (section == EmailMessageListModel.HighPriority) { + //: High priority section header + //% "High" + return qsTrId("jolla-email-la-high_priority") + } else if (section == EmailMessageListModel.LowPriority) { + //: Low priority section header + //% "Low" + return qsTrId("jolla-email-la-low_priority") + } else { + //: Normal priority section header + //% "Normal" + return qsTrId("jolla-email-la-normal_priority") + } + } else if (sortBy === EmailMessageListModel.Attachments) { + if (section == "true") { + //: Contains attachments section header + //% "Contains attachments" + return qsTrId("jolla-email-la-contains_attachments") + } else { + //: No attachments section header + //% "No attachments" + return qsTrId("jolla-email-la-no_attachments") + } + } else { + return section + } + } + } + } + + onAtYEndChanged: { + if (atYEnd && messageModel.canFetchMore) { + if (quickScrollAnimating) { + waitToFetchMore = true + } else { + messageModel.limit = messageModel.limit + fetchMore + } + } + } + + onQuickScrollAnimatingChanged: { + if (!quickScrollAnimating && waitToFetchMore) { + waitToFetchMore = false + messageModel.limit = messageModel.limit + fetchMore + } + } + + Binding { + target: app + property: "inboxUnreadCount" + when: folder.folderType == EmailFolder.InboxFolder + value: folder.folderUnreadCount + } + + EmailFolder { + id: folder + folderAccessor: messageModel.folderAccessor + } + + PullDownMenu { + busy: app.syncInProgress + + MenuItem { + //: Selects message list sort method + //% "Sort by: %1" + text: qsTrId("jolla-email-me-sort_by").arg(Utils.sortTypeText(messageModel.sortBy)) + visible: messageListView.count > 0 + onClicked: { + // Don't show sort by sender for outgoing folders + var obj = pageStack.animatorPush(Qt.resolvedUrl("SortPage.qml"), + { isOutgoingFolder: folder.isOutgoingFolder }) + obj.pageCompleted.connect(function(page) { + page.sortSelected.connect(function(sortType) { + messageModel.sortBy = sortType + pageStack.pop() + }) + }) + } + } + + MenuItem { + //% "Select messages" + text: qsTrId("jolla-email-me-select_messages") + visible: messageListView.count > 0 + onClicked: { + pageStack.animatorPush(Qt.resolvedUrl("MultiSelectionPage.qml"), { + removeRemorse: removeRemorse, + selectionModel: messageModel, + accountId: accountId, + folderId: folder.folderId, + deletionModel: deletionModel + }) + } + } + + MenuItem { + //: Search from messages + //% "Search" + text: qsTrId("jolla-email-me-search") + onClicked: { + pageStack.animatorPush(Qt.resolvedUrl("SearchPage.qml"), { accountId: accountId }) + } + } + + MenuItem { + //: Send all messages currently in the outbox + //% "Send all" + text: isOutboxFolder ? qsTrId("jolla-email-me-send-all") + //: Synchronise account menu item + //% "Sync" + : qsTrId("jolla-email-me-sync") + enabled: !isOutboxFolder || (isOutboxFolder && messageListView.count > 0) + onClicked: { + if (isOutboxFolder) { + sendAll() + } else { + synchronize() + } + } + } + + MenuItem { + enabled: mailAccountListModel.numberOfTransmitAccounts > 0 + //: New message menu item + //% "New Message" + text: qsTrId("jolla-email-me-new_message") + onClicked: { + pageStack.animatorPush(Qt.resolvedUrl("ComposerPage.qml"), { accountId: accountId }) + } + } + } + + PushUpMenu { + visible: !messageModel.canFetchMore && showGetMoreMails + busy: app.syncInProgress + + MenuItem { + //% "Get more mails" + text: qsTrId("jolla-email-me-get_more_mails") + onClicked: { + if (messageModel.limit) { + messageModel.limit = messageModel.limit + getMore + } + emailAgent.getMoreMessages(folder.folderId, getMore) + } + } + } + + MessageRemorsePopup { + id: removeRemorse + } + + ViewPlaceholder { + opacity: messageListView.count === 0 ? 1.0 : 0.0 + + text: { + if (!app.syncInProgress) { + //: Empty message list placeholder label + //% "No emails" + return qsTrId("jolla-email-la-empty_list") + } + return "" + } + } + + model: DeletionDelegateModel { + id: deletionModel + + model: EmailMessageListModel { + id: messageModel + + limit: app.defaultMessageListLimit + // reset limit upon content change + onFolderAccessorChanged: { + limit = app.defaultMessageListLimit + messageListView.contentY = messageListView.originY + } + } + + delegate: messageModel.sortBy == EmailMessageListModel.Subject + ? subjectSortedDelegate + : (messageModel.sortBy == EmailMessageListModel.Time + ? timeSortedDelegate + : defaultSortedDelegate) + } + + Component { + id: subjectSortedDelegate + MessageListViewItem { + //% "(Empty subject)" + primaryLine: model.subject != "" ? model.subject : qsTrId("jolla-email-la-no_subject") + secondaryLine: model.senderDisplayName != "" ? model.senderDisplayName : model.senderEmailAddress + showRecipientsName: folder.isOutgoingFolder + isDraftsFolder: messageListView.isDraftsFolder + } + } + Component { + id: timeSortedDelegate + MessageListViewItem { + showRecipientsName: folder.isOutgoingFolder + isDraftsFolder: messageListView.isDraftsFolder + } + } + Component { + id: defaultSortedDelegate + MessageListViewItem { + date: Format.formatDate(model.qDateTime, Formatter.TimepointRelative) + showRecipientsName: folder.isOutgoingFolder + isDraftsFolder: messageListView.isDraftsFolder + } + } + + VerticalScrollDecorator {} + + function _sectionProperty() { + var sortBy = messageModel.sortBy + if (sortBy === EmailMessageListModel.Time) { + return "timeSection" + } else if (sortBy === EmailMessageListModel.Sender) { + return "senderDisplayName" + } else if (sortBy === EmailMessageListModel.Size) { + return "sizeSection" + } else if (sortBy === EmailMessageListModel.ReadStatus) { + return "readStatus" + } else if (sortBy === EmailMessageListModel.Priority) { + return "priority" + } else if (sortBy === EmailMessageListModel.Attachments) { + return "hasAttachments" + } else if (sortBy === EmailMessageListModel.Subject) { + return "subject" + } else if (sortBy === EmailMessageListModel.Recipients) { + return "recipients" + } + } + + function _sectionCriteria() { + var sortBy = messageModel.sortBy + if (sortBy === EmailMessageListModel.Sender || sortBy === EmailMessageListModel.Subject + || sortBy === EmailMessageListModel.Recipients) { + return ViewSection.FirstCharacter + } else { + return ViewSection.FullString + } + } + + Connections { + target: emailAgent + + onCurrentSynchronizingAccountIdChanged: { + if (emailAgent.currentSynchronizingAccountId === folder.parentAccountId) { + messageListView.errorOccurred = false + } + } + + onError: { + if (accountId === 0 || accountId === folder.parentAccountId) { + messageListView.lastErrorText = Utils.syncErrorText(syncError) + messageListView.errorOccurred = true + } + } + } +} diff --git a/usr/share/jolla-email/pages/MessageListViewItem.qml b/usr/share/jolla-email/pages/MessageListViewItem.qml new file mode 100644 index 00000000..2d8e22c8 --- /dev/null +++ b/usr/share/jolla-email/pages/MessageListViewItem.qml @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2014 – 2019 Jolla Ltd. + * + * License: Proprietary + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Nemo.Email 0.1 + +MessageItem { + // About to be deleted + onEmailViewerRequested: { + pageStack.animatorPush(app.getMessageViewerComponent(), { + "messageId": messageId, + "isOutgoing": isOutgoingFolder, + "removeCallback": function(id) { remove() } + }) + } +} diff --git a/usr/share/jolla-email/pages/MessageRemorsePopup.qml b/usr/share/jolla-email/pages/MessageRemorsePopup.qml new file mode 100644 index 00000000..fac9e815 --- /dev/null +++ b/usr/share/jolla-email/pages/MessageRemorsePopup.qml @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2013 – 2019 Jolla Ltd. + * + * License: Proprietary + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Nemo.Email 0.1 + +RemorsePopup { + property EmailMessageListModel selectionModel + property DeletionDelegateModel deletionModel + + function startDeleteSelectedMessages() { + //: Remorse popup for multiple emails deletion + //% "Deleted %n mail(s)" + execute(qsTrId("jolla-email-me-deleted-mails", selectionModel.selectedMessageCount), + function() { selectionModel.deleteSelectedMessages()}) + } + + onCanceled: { + selectionModel.deselectAllMessages() + deletionModel.clearHidden() + } +} diff --git a/usr/share/jolla-email/pages/MessageView.qml b/usr/share/jolla-email/pages/MessageView.qml new file mode 100644 index 00000000..a50ac119 --- /dev/null +++ b/usr/share/jolla-email/pages/MessageView.qml @@ -0,0 +1,147 @@ +/* + * Copyright (c) 2012 – 2019 Jolla Ltd. + * Copyright (c) 2019 Open Mobile Platform LLC. + * + * License: Proprietary + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Nemo.Email 0.1 +import Sailfish.WebView 1.0 +import Nemo.Configuration 1.0 + +WebViewPage { + id: messageViewPage + + property alias messageId: message.messageId + property Page messageInfoPage + property bool loaded + property bool isOutgoing + readonly property bool replyAll: message.multipleRecipients + property Page previousPage: pageStack.previousPage() + // either undefined or function taking message id as parameter + property var removeCallback + property string pathToLoad + property string messageAction + readonly property bool isLocalFile: pathToLoad !== "" + + property bool _sendReadReceipt + + function doRemove() { + if (removeCallback) { + removeCallback(messageId) + } else { + console.warn("MessageView requested removal, but there is no handler defined") + } + } + + onStatusChanged: { + if (status == PageStatus.Activating && isLocalFile) { + message.loadFromFile(pathToLoad) + } + if (status == PageStatus.Active) { + if (!loaded) { + htmlLoader.load(message) + htmlLoader.markAsRead() + } + pageStack.pushAttached(messageInfoComponent, { message: message }) + app.coverMode = "mailViewer" + } + } + + Component.onDestruction: { + if (_sendReadReceipt) { + switch (sendReadReceiptsConfig.value) { + case 0: + pageStack.animatorPush(Qt.resolvedUrl("SendReadReceiptDialog.qml"), { originalEmailId: messageId }) + break + case 1: // always send + if (!message.sendReadReceipt( + // Defined in SendReadReceiptDialog + qsTrId("jolla-email-la-read_receipt_email_subject_prefix"), + // Defined in SendReadReceiptDialog + qsTrId("jolla-email-la-read_receipt_email_body") + .arg(Qt.formatTime(originalEmail.date)) + .arg(Qt.formatDate(originalEmail.date)) + .arg(message.accountAddress))) { + // Defined in SendReadReceiptDialog + app.showSingleLineNotification(qsTrId("jolla-email-la-failed_send_read_receipt")) + } + break + default: + break + } + } + } + + EmailMessage { + id: message + autoVerifySignature: autoVerifySignatureConfig.value + + onHtmlBodyChanged: { + if (messageViewPage.status == PageStatus.Active) { + loaded = false + htmlLoader.load(message) + } + } + + onBodyChanged: { + if (messageViewPage.status == PageStatus.Active) { + loaded = false + htmlLoader.load(message) + } + } + } + + HtmlLoader { + id: htmlLoader + + anchors.fill: parent + portrait: messageViewPage.isPortrait + attachmentsModel: attachModel + isOutgoing: messageViewPage.isOutgoing + isLocalFile: messageViewPage.isLocalFile + initialAction: messageViewPage.messageAction + onRemoveRequested: messageViewPage.doRemove() + onNeedToSendReadReceipt: { + _sendReadReceipt = true + } + } + + Component { + id: messageInfoComponent + MessageInfo { + isLocalFile: messageViewPage.isLocalFile + } + } + + AttachmentListModel { + id: attachModel + messageId: message.messageId + } + + Binding { + target: app + property: "viewerSender" + value: message.fromDisplayName + } + + Binding { + target: app + property: "viewerSubject" + value: message.subject + } + + ConfigurationValue { + id: sendReadReceiptsConfig + key: "/apps/jolla-email/settings/sendReadReceipts" + defaultValue: 0 + } + + ConfigurationValue { + id: autoVerifySignatureConfig + key: "/apps/jolla-email/settings/autoVerifySignature" + defaultValue: false + } +} diff --git a/usr/share/jolla-email/pages/MessageViewFooter.qml b/usr/share/jolla-email/pages/MessageViewFooter.qml new file mode 100644 index 00000000..225f18c9 --- /dev/null +++ b/usr/share/jolla-email/pages/MessageViewFooter.qml @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2020 Open Mobile Platform LLC. + * + * License: Proprietary + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Sailfish.Silica.private 1.0 as SilicaPrivate +import Sailfish.Calendar 1.0 +import Sailfish.WebView.Controls 1.0 +import Sailfish.WebView.Popups 1.0 +import Nemo.Email 0.1 +import org.nemomobile.calendar 1.0 +import "utils.js" as Utils + +SilicaControl { + id: footer + + property bool open + property alias textSelectionController: textSelectionToolbar.controller + property bool showReplyAll + property alias portrait: textSelectionToolbar.portrait + + signal reply + signal replyAll + signal forward + signal deleteEmail + + height: portrait ? Theme.itemSizeMedium : Theme.itemSizeSmall + + palette.colorScheme: Theme.DarkOnLight + + Rectangle { + width: footer.width + height: footer.height + + color: "#f3f0f0" + } + + TextSelectionToolbar { + id: textSelectionToolbar + + width: footer.width + height: footer.height + + selectAllEnabled: true + + buttons: { + if (controller && controller.selectionVisible) { + return defaultButtons + } else { + var buttons = [ + { + "icon": "image://theme/icon-m-message-reply", + //% "Reply" + "label": qsTrId("jolla-email-la-reply"), + "action": footer.reply + } + ] + + if (footer.showReplyAll) { + buttons.push( + { + "icon": "image://theme/icon-m-message-reply-all", + //% "Reply All" + "label": qsTrId("jolla-email-la-reply_all"), + "action": footer.replyAll + }) + } + buttons.push( + { + "icon": "image://theme/icon-m-delete", + //% "Delete" + "label": qsTrId("jolla-email-la-delete"), + "action": footer.deleteEmail + }, { + "icon": "image://theme/icon-m-message-forward", + //% "Forward" + "label": qsTrId("jolla-email-la-forward"), + "action": footer.forward + }) + return buttons + } + } + + onCall: Qt.openUrlExternally("tel:" + controller.text) + onShare: webShareAction.shareText(controller.text) + } + + WebShareAction { + id: webShareAction + } +} diff --git a/usr/share/jolla-email/pages/MessageViewHeader.qml b/usr/share/jolla-email/pages/MessageViewHeader.qml new file mode 100644 index 00000000..c3d0d442 --- /dev/null +++ b/usr/share/jolla-email/pages/MessageViewHeader.qml @@ -0,0 +1,174 @@ +/* + * Copyright (c) 2013 - 2022 Jolla Ltd. + * Copyright (c) 2020 - 2021 Open Mobile Platform LLC. + * + * License: Proprietary + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Sailfish.Calendar 1.0 +import Nemo.Email 0.1 +import org.nemomobile.calendar 1.0 +import Jolla.Email 1.0 +import "utils.js" as Utils + +Column { + id: root + + property EmailMessage email + property AttachmentListModel attachmentsModel + property bool portrait + property bool showLoadImages + property bool isOutgoing + + property bool open: true + + property alias contentX: content.x + readonly property alias contentY: header.height + + property alias contentItem: buttonsColumn + property alias heightBehaviorEnabled: heightBehavior.enabled + + property QtObject event + property QtObject occurrence + + readonly property bool inlineInvitation: email && email.calendarInvitationSupportsEmailResponses + + signal loadImagesClicked + signal loadImagesCloseClicked + + signal clicked + + function _emailRecipients() { + var recipientsDisplayName = email.recipientsDisplayName.toString() + return recipientsDisplayName != "" + ? //: 'To: ' message recipients (keep the colon separator here) + //% "To: %1" + qsTrId("jolla-email-la-recipients_header").arg(recipientsDisplayName) + : //% "No recipients" + qsTrId("jolla-email-la-no_recipient") + } + + PageHeader { + id: header + + width: parent.width + rightMargin: headerIcons.width > 0 + ? (headerIcons.width + Theme.paddingMedium + headerIcons.anchors.rightMargin) + : Theme.horizontalPageMargin + descriptionRightMargin: Theme.horizontalPageMargin + interactive: true + + title: email ? (isOutgoing ? _emailRecipients() : email.fromDisplayName) : "" + description: email ? email.subject : "" + + Row { + id: headerIcons + + anchors { + right: parent.right + rightMargin: Theme.horizontalPageMargin + Theme.paddingSmall + verticalCenter: parent.verticalCenter + verticalCenterOffset: -Theme.paddingLarge / 3 + } + spacing: Theme.paddingSmall + + HighlightImage { + anchors.verticalCenter: parent.verticalCenter + source: inlineInvitation && event + && (event.secrecy === CalendarEvent.SecrecyPrivate + || event.secrecy === CalendarEvent.SecrecyConfidential) + ? "image://theme/icon-s-secure" + : "" + color: header.titleColor + } + + HighlightImage { + anchors.verticalCenter: parent.verticalCenter + source: email ? Utils.priorityIcon(email.priority) : "" + color: header.titleColor + } + } + } + + ImportModel { + icsString: inlineInvitation ? email.calendarInvitationBody : "" + onCountChanged: { + if (count > 0) { + root.event = getEvent(0) + root.occurrence = root.event ? root.event.nextOccurrence() : null + } else { + root.event = null + root.occurrence = null + } + } + } + + SilicaControl { + id: content + + palette.colorScheme: Theme.DarkOnLight + + width: root.width - x + height: root.open ? buttonsColumn.implicitHeight : 0 + + clip: heightAnimation.running + visible: root.open || heightAnimation.running + + Behavior on height { + id: heightBehavior + SmoothedAnimation { id: heightAnimation; easing.type: Easing.InOutQuad; duration: 100 } + } + + Rectangle { + width: root.width + height: buttonsColumn.implicitHeight + + color: "#f3f0f0" + } + + Column { + id: buttonsColumn + width: parent.width + + LoadImagesItem { + width: root.width + + visible: root.showLoadImages + + onClicked: root.loadImagesClicked() + onCloseClicked: root.loadImagesCloseClicked() + } + + DecryptItem { + width: root.width + email: root.email + } + + AttachmentRow { + width: root.width + + visible: email && email.numberOfAttachments > 0 + attachmentsModel: root.attachmentsModel + emailMessage: email + } + + Loader { + visible: email && email.signatureStatus != EmailMessage.NoDigitalSignature + // SignatureItem.qml is not installed by default. + active: root.email && EmailUtils.emailAppCryptoEnabled + source: "SignatureItem.qml" + onItemChanged: if (item) item.email = root.email + width: parent.width + } + + CalendarDelegate { + width: root.width + + visible: email && (email.hasCalendarInvitation || email.hasCalendarCancellation) && !inlineInvitation + email: root.email + } + } + } +} diff --git a/usr/share/jolla-email/pages/MessageViewPullDown.qml b/usr/share/jolla-email/pages/MessageViewPullDown.qml new file mode 100644 index 00000000..f10addf5 --- /dev/null +++ b/usr/share/jolla-email/pages/MessageViewPullDown.qml @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2013 – 2019 Jolla Ltd. + * + * License: Proprietary + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 + +PullDownMenu { + id: root + + signal removeRequested + + function _openComposer(action) { + pageStack.animatorPush(Qt.resolvedUrl("ComposerPage.qml"), { popDestination: previousPage, action: action, originalMessageId: message.messageId }) + } + + MenuItem { + //% "Delete" + text: qsTrId("jolla-email-me-delete") + onClicked: { + pageStack.pop() + root.removeRequested() + } + } + MenuItem { + //: Forward message menu item + //% "Forward" + text: qsTrId("jolla-email-me-forward") + onClicked: _openComposer('forward') + } + MenuItem { + visible: replyAll + //: Reply to all message recipients menu item + //% "Reply to All" + text: qsTrId("jolla-email-me-reply_all") + onClicked: _openComposer('replyAll') + } + MenuItem { + //: Reply to message sender menu item + //% "Reply" + text: qsTrId("jolla-email-me-reply") + onClicked: _openComposer('reply') + } +} diff --git a/usr/share/jolla-email/pages/MoveFolderPage.qml b/usr/share/jolla-email/pages/MoveFolderPage.qml new file mode 100644 index 00000000..719aceef --- /dev/null +++ b/usr/share/jolla-email/pages/MoveFolderPage.qml @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2018 – 2019 Jolla Ltd. + * + * License: Proprietary + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Nemo.Email 0.1 +import "utils.js" as Utils + +Page { + id: root + + property int selectedFolderId: -1 + property int folderId + property int parentFolderId + property FolderListModel folderModel + + onStatusChanged: { + if (status === PageStatus.Deactivating) { + if (selectedFolderId != -1) { + emailAgent.moveFolder(folderId, selectedFolderId) + } + } + } + + FolderListProxyModel { + id: folderProxyModel + sourceModel: root.folderModel + includeRoot: true + } + + SilicaListView { + model: folderProxyModel + header: PageHeader { + //: Move folder page header + //% "Select folder:" + title: qsTrId("email-ph-folder_move") + } + + anchors.fill: parent + + delegate: FolderItem { + enabled: canCreateChild && + folderId != root.folderId && + folderId != root.parentFolderId + // don't show local folders and descendant folders in the list + hidden: Utils.isLocalFolder(folderId) || root.folderModel.isFolderAncestorOf(folderId, root.folderId) + highlighted: folderId == root.selectedFolderId + isCurrentItem: folderId == root.folderId + onClicked: { + root.selectedFolderId = folderId + pageStack.pop() + } + } + VerticalScrollDecorator {} + + Component.onCompleted: { + // Scroll list to current folder + // Take into account 'Root' folder on top of the list + currentIndex = root.folderModel.indexFromFolderId(folderId) + 1 + } + } +} diff --git a/usr/share/jolla-email/pages/MoveToPage.qml b/usr/share/jolla-email/pages/MoveToPage.qml new file mode 100644 index 00000000..a773dda7 --- /dev/null +++ b/usr/share/jolla-email/pages/MoveToPage.qml @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2013 – 2019 Jolla Ltd. + * + * License: Proprietary + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Nemo.Email 0.1 +import "utils.js" as Utils + +Page { + property alias accountKey: folderModel.accountKey + // Need either messageId or messageModel + property int msgId + property EmailMessageListModel messageModel + property int currentFolder: emailAgent.folderIdForMessage(msgId) + property int selectedFolder + + onStatusChanged: { + if (status === PageStatus.Deactivating) { + if (selectedFolder) { + if (messageModel) { + messageModel.moveSelectedMessages(selectedFolder) + messageModel.deselectAllMessages() + } else { + emailAgent.moveMessage(msgId, selectedFolder) + } + } else if (messageModel) { + messageModel.deselectAllMessages() + } + } + } + + FolderListModel { + id: folderModel + } + + SilicaListView { + anchors.fill: parent + model: folderModel + header: PageHeader { + //: Move to folder page header + //% "Select Folder:" + title: qsTrId("jolla-email-he-select_folder") + } + + delegate: FolderItem { + // don't show local folders in the list + hidden: Utils.isLocalFolder(folderId) + enabled: folderId != currentFolder && canHaveMessages + onClicked: { + selectedFolder = folderId + pageStack.pop() + } + } + + VerticalScrollDecorator {} + + Component.onCompleted: { + // Scroll list to current folder + // Take into account 'Root' folder on top of the list + currentIndex = folderModel.indexFromFolderId(currentFolder) + 1 + } + } +} diff --git a/usr/share/jolla-email/pages/MultiSelectionPage.qml b/usr/share/jolla-email/pages/MultiSelectionPage.qml new file mode 100644 index 00000000..fa85d519 --- /dev/null +++ b/usr/share/jolla-email/pages/MultiSelectionPage.qml @@ -0,0 +1,183 @@ +/* + * Copyright (c) 2013 – 2019 Jolla Ltd. + * Copyright (c) 2019 Open Mobile Platform LLC. + * + * License: Proprietary + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Nemo.Email 0.1 + +Page { + id: page + + readonly property int selectionCount: selectionModel ? selectionModel.selectedMessageCount : 0 + // these two for move to page + property int accountId + property int folderId + property bool actionInProgress + property bool showMove: accountId != 0 + property MessageRemorsePopup removeRemorse + property EmailMessageListModel selectionModel + property DeletionDelegateModel deletionModel + + onStatusChanged: { + if (status === PageStatus.Deactivating) { + if (!actionInProgress) { + selectionModel.deselectAllMessages() + } + deletionModel.clearSelected() + } else if (status === PageStatus.Active) { + // we want to have all messages available here for the mass operations + selectionModel.limit = 0 + } + } + + function _deleteClicked() { + actionInProgress = true + deletionModel.hideSelected() + removeRemorse.selectionModel = selectionModel + removeRemorse.deletionModel = deletionModel + removeRemorse.startDeleteSelectedMessages() + pageStack.pop() + } + + function _moveClicked() { + actionInProgress = true + pageStack.animatorReplace(Qt.resolvedUrl("MoveToPage.qml"), { + messageModel: selectionModel, + accountKey: accountId, + currentFolder: folderId + }) + } + + SilicaListView { + clip: dockedPanel.expanded + + anchors { + top: parent.top + left: parent.left + right: parent.right + bottom: dockedPanel.top + } + + header: PageHeader { + title: selectionCount ? //: Selected messages + //% "Selected %n" + qsTrId("jolla-email-he-select_messages", selectionCount) + : //: Message selection header, no currently selected messages + //% "Selected" + qsTrId("jolla-email-he-zero_selected_messages") + } + + model: selectionModel + + section { + property: 'timeSection' + + delegate: SectionHeader { + text: Format.formatDate(section, Formatter.TimepointSectionRelative) + height: text === "" ? 0 : implicitHeight + Theme.itemSizeSmall / 2 + horizontalAlignment: Text.AlignHCenter + } + } + + delegate: MessageItem { + function _toggleSelection() { + if (model.selected) { + selectionModel.deselectMessage(model.index) + page.deletionModel.deselectItem(model.index) + } else { + selectionModel.selectMessage(model.index) + page.deletionModel.selectItem(model.index) + } + } + + menu: undefined // actions in docked panel + selectMode: true + + onClicked: _toggleSelection() + onPressAndHold: _toggleSelection() + } + + PullDownMenu { + busy: app.syncInProgress + + MenuItem { + //: Deselect all messages + //% "Deselect all" + text: qsTrId("jolla-email-me-deselect_all_messages") + visible: selectionCount + onClicked: { + selectionModel.deselectAllMessages() + page.deletionModel.clearSelected() + } + } + + MenuItem { + //: Select all messages + //% "Select all" + text: qsTrId("jolla-email-me-select_all_messages") + visible: selectionModel.count > 0 && selectionCount < selectionModel.count + onClicked: { + selectionModel.selectAllMessages() + page.deletionModel.selectAll() + } + } + } + + VerticalScrollDecorator {} + } + + DockedPanel { + id: dockedPanel + width: parent.width + height: Theme.itemSizeLarge + dock: Dock.Bottom + open: selectionCount + + Image { + anchors.fill: parent + fillMode: Image.PreserveAspectFit + source: "image://theme/graphic-gradient-edge" + } + Row { + Item { + width: moveIcon.visible ? dockedPanel.width/3 : dockedPanel.width/2 + height: Theme.itemSizeLarge + IconButton { + anchors.centerIn: parent + icon.source: "image://theme/icon-m-delete" + onClicked: _deleteClicked() + } + } + Item { + width: moveIcon.visible ? dockedPanel.width/3 : dockedPanel.width/2 + height: Theme.itemSizeLarge + IconButton { + anchors.centerIn: parent + icon.source: selectionModel.unreadMailsSelected ? "image://theme/icon-m-mail-open" : "image://theme/icon-m-mail" + onClicked: { + if (selectionModel.unreadMailsSelected) { + selectionModel.markAsReadSelectedMessages() + } else { + selectionModel.markAsUnReadSelectedMessages() + } + } + } + } + Item { + id: moveIcon + visible: showMove + width: dockedPanel.width/3 + height: Theme.itemSizeLarge + IconButton { + anchors.centerIn: parent + icon.source: "image://theme/icon-m-message-forward" + onClicked: _moveClicked() + } + } + } + } +} diff --git a/usr/share/jolla-email/pages/NewFolderDialog.qml b/usr/share/jolla-email/pages/NewFolderDialog.qml new file mode 100644 index 00000000..1ec89cee --- /dev/null +++ b/usr/share/jolla-email/pages/NewFolderDialog.qml @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2018 – 2019 Jolla Ltd. + * + * License: Proprietary + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Nemo.Email 0.1 + +Dialog { + id: root + + property int parentFolderId + property string parentFolderName + property FolderListModel folderModel + + canAccept: folderNameField.acceptableInput + + onAcceptBlocked: { + if (!folderNameField.acceptableInput) { + folderNameField.errorHighlight = true + } + } + + onAccepted: { + emailAgent.createFolder(folderNameField.text, folderModel.accountKey, parentFolderId) + } + + SilicaFlickable { + anchors.fill: parent + contentHeight: content.height + Theme.paddingLarge + Column { + width: parent.width + DialogHeader { + id: dialogHeader + //% "Create" + acceptText: qsTrId("email-ph-folder_create") + } + TextField { + id: folderNameField + + //% "Folder name" + label: qsTrId("jolla-email-newfolder_folder_name_placeholder") + //% "Folder name required" + description: errorHighlight ? qsTrId("jolla-email-newfolder_folder_name_placeholder_error") : "" + + acceptableInput: text.length > 0 + onActiveFocusChanged: if (!activeFocus) errorHighlight = !acceptableInput + onAcceptableInputChanged: if (acceptableInput) errorHighlight = false + + focus: true + EnterKey.enabled: root.canAccept + EnterKey.iconSource: "image://theme/icon-m-enter-accept" + EnterKey.onClicked: root.accept() + } + + ValueButton { + id: valueButton + //% "Parent folder" + label: qsTrId("jolla-email-newfolder_parent_label") + value: parentFolderName + onClicked: { + var selector = pageStack.animatorPush(Qt.resolvedUrl("NewFolderParentSelectionPage.qml"), { + selectedFolderId: root.parentFolderId, + folderModel: root.folderModel + }) + selector.pageCompleted.connect(function(page) { + page.folderSelected.connect(function(folderId, folderName) { + pageStack.pop() + root.parentFolderName = folderName + root.parentFolderId = folderId + }) + }) + + } + } + } + VerticalScrollDecorator {} + } +} diff --git a/usr/share/jolla-email/pages/NewFolderParentSelectionPage.qml b/usr/share/jolla-email/pages/NewFolderParentSelectionPage.qml new file mode 100644 index 00000000..9ff45eed --- /dev/null +++ b/usr/share/jolla-email/pages/NewFolderParentSelectionPage.qml @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2018 – 2019 Jolla Ltd. + * + * License: Proprietary + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Nemo.Email 0.1 +import "utils.js" as Utils + +Page { + id: root + + property int selectedFolderId: -1 + property FolderListModel folderModel + + signal folderSelected(int folderId, string folderName) + + FolderListProxyModel { + id: folderProxyModel + sourceModel: root.folderModel + includeRoot: true + } + + SilicaListView { + anchors.fill: parent + model: folderProxyModel + header: PageHeader { + //% "Parent folder" + title: qsTrId("jolla-email-newfolder_select_parent_title") + } + + delegate: FolderItem { + enabled: canCreateChild + isCurrentItem: folderId == root.selectedFolderId + // don't show local folders in the list + hidden: Utils.isLocalFolder(folderId) + onClicked: { + root.folderSelected(folderId, folderDisplayName) + } + } + VerticalScrollDecorator {} + + Component.onCompleted: { + // Scroll list to current folder + // Take into account 'Root' folder on top of the list + currentIndex = root.folderModel.indexFromFolderId(selectedFolderId) + 1 + } + } +} diff --git a/usr/share/jolla-email/pages/NoAccountsPage.qml b/usr/share/jolla-email/pages/NoAccountsPage.qml new file mode 100644 index 00000000..e5ee5f71 --- /dev/null +++ b/usr/share/jolla-email/pages/NoAccountsPage.qml @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2019 Jolla Ltd. + * + * License: Proprietary + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import com.jolla.email 1.1 +import Sailfish.Policy 1.0 + +Page { + SilicaFlickable { + anchors.fill: parent + + PullDownMenu { + visible: AccessPolicy.accountCreationEnabled + MenuItem { + //: Add account menu item + //% "Add account" + text: qsTrId("jolla-email-me-add_account") + onClicked: { + app.showAccountsCreationDialog() + } + } + } + + PageHeader { + //: Email page header + //% "Mail" + title: qsTrId("email-he-email") + } + + NoAccountsPlaceholder { + enabled: true + } + } +} diff --git a/usr/share/jolla-email/pages/PendingInboxPage.qml b/usr/share/jolla-email/pages/PendingInboxPage.qml new file mode 100644 index 00000000..e0b00d05 --- /dev/null +++ b/usr/share/jolla-email/pages/PendingInboxPage.qml @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2019 Jolla Ltd. + * + * License: Proprietary + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Nemo.Email 0.1 +import com.jolla.email 1.1 + +Page { + id: root + + property int accountId + + onStatusChanged: { + if (status === PageStatus.Active) { + tryReplaceWithMessagePage() + } + } + + function tryReplaceWithMessagePage() { + if (root.status != PageStatus.Active) + return + + var inbox = emailAgent.inboxFolderId(accountId) + if (inbox > 0) { + var accessor = emailAgent.accessorFromFolderId(inbox) + pageStack.replace(Qt.resolvedUrl("MessageListPage.qml"), { folderAccessor: accessor }, + PageStackAction.Immediate) + } + } + + Connections { + target: emailAgent + onStandardFoldersCreated: tryReplaceWithMessagePage() + } + + BusyLabel { + //% "Synchronizing account" + text: qsTrId("jolla-email-la-synchronizing_account") + running: true + } +} diff --git a/usr/share/jolla-email/pages/RenameFolderDialog.qml b/usr/share/jolla-email/pages/RenameFolderDialog.qml new file mode 100644 index 00000000..b52f70e2 --- /dev/null +++ b/usr/share/jolla-email/pages/RenameFolderDialog.qml @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2018 – 2019 Jolla Ltd. + * + * License: Proprietary + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Nemo.Email 0.1 + +Dialog { + id: root + + property int folderId + property string folderName + canAccept: nameEdit.acceptableInput + + onAcceptBlocked: { + if (!nameEdit.acceptableInput) { + nameEdit.errorHighlight = true + } + } + + onAccepted: { + emailAgent.renameFolder(folderId, nameEdit.text) + } + + Column { + width: parent.width + DialogHeader { + //% "Rename" + acceptText: qsTrId("email-ph-folder_rename_title") + } + + TextField { + id: nameEdit + text: folderName + focus: true + + //% "Folder name" + label: qsTrId("email-la-folder_rename") + //% "New folder name required" + description: errorHighlight ? qsTrId("email-la-folder_rename_error") : "" + + acceptableInput: text.length > 0 && text !== folderName + onActiveFocusChanged: if (!activeFocus) errorHighlight = !acceptableInput + onAcceptableInputChanged: if (acceptableInput) errorHighlight = false + + EnterKey.enabled: root.canAccept + EnterKey.iconSource: "image://theme/icon-m-enter-accept" + EnterKey.onClicked: root.accept() + } + } +} diff --git a/usr/share/jolla-email/pages/SearchOptionsPage.qml b/usr/share/jolla-email/pages/SearchOptionsPage.qml new file mode 100644 index 00000000..a7109543 --- /dev/null +++ b/usr/share/jolla-email/pages/SearchOptionsPage.qml @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2015 – 2019 Jolla Ltd. + * + * License: Proprietary + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Nemo.Email 0.1 + +Page { + property EmailMessageListModel searchModel + + SilicaListView { + anchors.fill: parent + contentHeight: column.height + + Column { + id: column + width: parent.width + + PageHeader { + //% "Search options" + title: qsTrId("jolla-email-he-search_options") + } + + ComboBox { + id: searchInCombo + + currentIndex: searchModel.searchOn === EmailMessageListModel.LocalAndRemote ? 0 : searchModel.searchOn === EmailMessageListModel.Local ? 1 : 2 + //% "Search on" + label: qsTrId("jolla-email-la-search_on") + menu: ContextMenu { + MenuItem { + //: Search on server and device + //% "Server and device" + text: qsTrId("jolla-email-me_search_server_and_device") + onClicked: searchModel.searchOn = EmailMessageListModel.LocalAndRemote + } + MenuItem { + //: Search on device + //% "Device" + text: qsTrId("jolla-email-me_search_device") + onClicked: searchModel.searchOn = EmailMessageListModel.Local + } + MenuItem { + //: Search on server + //% "Server" + text: qsTrId("jolla-email-me_search_server") + onClicked: searchModel.searchOn = EmailMessageListModel.Remote + } + } + } + + SectionHeader { + visible: searchInCombo.currentIndex === 1 + //% "Search in" + text: qsTrId("jolla-email-la-search_in") + } + + TextSwitch { + visible: searchInCombo.currentIndex === 1 + //: Search From address, the email sender + //% "From" + text: qsTrId("jolla-email-la-search_from") + checked: searchModel.searchFrom + onCheckedChanged: searchModel.searchFrom = checked + } + + TextSwitch { + visible: searchInCombo.currentIndex === 1 + //: Search recipients addresses, the recipients of the email + //% "Recipients" + text: qsTrId("jolla-email-la-search_recipients") + checked: searchModel.searchRecipients + onCheckedChanged: searchModel.searchRecipients = checked + } + + TextSwitch { + visible: searchInCombo.currentIndex === 1 + //: Search the email subject + //% "Subject" + text: qsTrId("jolla-email-la-search_subject") + checked: searchModel.searchSubject + onCheckedChanged: searchModel.searchSubject = checked + } + + TextSwitch { + visible: searchInCombo.currentIndex === 1 + //: Search email body, the email content + //% "Message body" + text: qsTrId("jolla-email-la-search_body") + checked: searchModel.searchBody + onCheckedChanged: searchModel.searchBody = checked + } + } + VerticalScrollDecorator {} + } +} diff --git a/usr/share/jolla-email/pages/SearchPage.qml b/usr/share/jolla-email/pages/SearchPage.qml new file mode 100644 index 00000000..86c19079 --- /dev/null +++ b/usr/share/jolla-email/pages/SearchPage.qml @@ -0,0 +1,161 @@ +/* + * Copyright (c) 2015 – 2019 Jolla Ltd. + * Copyright (c) 2019 Open Mobile Platform LLC. + * + * License: Proprietary + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Nemo.Email 0.1 +import "utils.js" as Utils + +Page { + id: searchPage + + property int accountId + property string searchString: searchField.text.toLowerCase().trim() + property bool openingTopPage + property bool resetSearch + + onSearchStringChanged: messageListModel.setSearch(searchString) + + onStatusChanged: { + if (status === PageStatus.Deactivating) { + if (openingTopPage) { + messageListModel.cancelSearch() + } + } else if (status === PageStatus.Active) { + listView.currentIndex = -1 // To keep focus in a search field after coming back from message viewer + if (!openingTopPage) { + searchField.forceActiveFocus() + } + // Updating model search options doesn't automatically refresh + // the search results, so we force it manually here on returning + if (resetSearch) { + resetSearch = false + messageListModel.setSearch(searchString) + } + openingTopPage = false + } + } + + EmailMessageListModel { + id: messageListModel + + limit: app.defaultMessageListLimit + folderAccessor: emailAgent.accountWideSearchAccessor(accountId) + } + + SilicaListView { + id: listView + anchors.fill: parent + currentIndex: -1 // to keep focus + + header: Item { + width: headerContainer.width + height: headerContainer.height + } + + PullDownMenu { + busy: app.syncInProgress + + MenuItem { + //% "Search options" + text: qsTrId("jolla-email-me-search_options") + onClicked: { + openingTopPage = true + // TODO: Only reset the search if one of the search options changes + resetSearch = true + pageStack.animatorPush("SearchOptionsPage.qml", {searchModel: messageListModel}) + } + } + } + + model: messageListModel + + section { + property: 'timeSection' + + delegate: SectionHeader { + text: Format.formatDate(section, Formatter.TimepointSectionRelative) + height: text === "" ? 0 : implicitHeight + Theme.itemSizeSmall / 2 + horizontalAlignment: Text.AlignHCenter + } + } + + function removeMessage(id) { + if (currentItem) { + currentItem.doRemove(id) + } else { + console.warn("No current item when deleting searched email") + } + } + + delegate: MessageItem { + searchString: searchPage.searchString + highlightSender: messageListModel.searchFrom + highlightRecipients: messageListModel.searchRecipients + highlightSubject: messageListModel.searchSubject + highlightBody: messageListModel.searchBody + + // If the user selects the context menu option to move a message, this + // avoids forcing the active focus to the search field when we return + onMenuOpenChanged: openingTopPage = menuOpen + + function doRemove(id) { + if (model.messageId != id) { + console.warn("Something went wrong removing an item in search page") + return + } + remove() + } + + onEmailViewerRequested: { + // search model can delete delegates while viewing, workaround by going thru current item + listView.currentIndex = model.index + openingTopPage = true + // TODO: isOutgoing doesn't work for local folders this way, but hard to tell + // here the "virtual" folder + pageStack.animatorPush(app.getMessageViewerComponent(), { + "messageId": messageId, + "removeCallback": listView.removeMessage, + "isOutgoing": ((folderId == emailAgent.sentFolderId(accountId)) + || (folderId == emailAgent.draftsFolderId(accountId)) + || (folderId == emailAgent.outboxFolderId(accountId))) + }) + } + + // dismiss keyboard when scrolling + onPressed: searchField.focus = false + } + + VerticalScrollDecorator {} + + Column { + id: headerContainer + + width: searchPage.width + parent: listView.contentItem + anchors.top: listView.headerItem ? listView.headerItem.top : listView.top + + PageHeader { + //% "Search" + title: qsTrId("jolla-email-he-search") + } + + SearchField { + id: searchField + + width: parent.width + //% "Search emails" + placeholderText: qsTrId("jolla-components_email-la-search_emails") + autoScrollEnabled: false + // avoid removing focus whenever a message is added to the selection list + focusOutBehavior: FocusBehavior.KeepFocus + EnterKey.iconSource: "image://theme/icon-m-enter-close" + EnterKey.onClicked: focus = false + } + } + } +} diff --git a/usr/share/jolla-email/pages/SendReadReceiptDialog.qml b/usr/share/jolla-email/pages/SendReadReceiptDialog.qml new file mode 100644 index 00000000..fc063547 --- /dev/null +++ b/usr/share/jolla-email/pages/SendReadReceiptDialog.qml @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2018 – 2019 Jolla Ltd. + * + * License: Proprietary + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Nemo.Email 0.1 +import Nemo.Configuration 1.0 + +Dialog { + id: root + + property alias originalEmailId: originalEmail.messageId + + canAccept: true + + EmailMessage { + id: originalEmail + } + + DialogHeader { + //% "Send receipt" + acceptText: qsTrId("email-dh-accept_send_read_receipt") + //% "Ignore" + cancelText: qsTrId("email-dh-do_not_send_read_receipt") + } + + Column { + width: parent.width + spacing: Theme.paddingLarge + anchors { + top: parent.top + topMargin: Theme.itemSizeLarge // Page header size + } + + Label { + x: Theme.horizontalPageMargin + width: parent.width - 2*x + wrapMode: Text.Wrap + font.pixelSize: Theme.fontSizeHuge + color: Theme.highlightColor + //% "Read receipt requested" + text: qsTrId("jolla-email-la-send_read_receipt") + } + + Label { + x: Theme.horizontalPageMargin + width: parent.width - 2*x + wrapMode: Text.Wrap + font.pixelSize: Theme.fontSizeSmall + color: Theme.highlightColor + //% "Sender requested a read receipt. Do you want to send a receipt?" + text: qsTrId("jolla-email-la-send_read_receipt_description") + } + TextSwitch { + id: rememberChoiceSwitch + //% "Remember my choice" + text: qsTrId("jolla-email-ts-remember_choice") + checked: sendReadReceiptsConfig.value + } + } + + onAccepted: { + if (originalEmail.messageId && !originalEmail.sendReadReceipt( + //% "Read: " + qsTrId("jolla-email-la-read_receipt_email_subject_prefix"), + //: %1:original email timestamp; %2:date of an email; %3:receiver email address + //% "Your email sent at %1 on %2 to %3 was read." + qsTrId("jolla-email-la-read_receipt_email_body") + .arg(Qt.formatTime(originalEmail.date)) + .arg(Qt.formatDate(originalEmail.date)) + .arg(originalEmail.accountAddress))) { + //% "Failed to send read receipt" + app.showSingleLineNotification(qsTrId("jolla-email-la-failed_send_read_receipt")) + } + if (rememberChoiceSwitch.checked) { + sendReadReceiptsConfig.value = 1 // means always send + } + } + onRejected: { + if (rememberChoiceSwitch.checked) { + sendReadReceiptsConfig.value = 2 // means always ignore + } + } + + ConfigurationValue { + id: sendReadReceiptsConfig + key: "/apps/jolla-email/settings/sendReadReceipts" + defaultValue: 0 + } +} diff --git a/usr/share/jolla-email/pages/ShareComposerPage.qml b/usr/share/jolla-email/pages/ShareComposerPage.qml new file mode 100644 index 00000000..c492933f --- /dev/null +++ b/usr/share/jolla-email/pages/ShareComposerPage.qml @@ -0,0 +1,113 @@ +/**************************************************************************************** +** +** Copyright (c) 2013 - 2021 Jolla Ltd. +** Copyright (c) 2021 Open Mobile Platform LLC +** All rights reserved. +** +** License: Proprietary. +** +****************************************************************************************/ +import QtQuick 2.6 +import Sailfish.Silica 1.0 +import Sailfish.Share 1.0 +import Nemo.FileManager 1.0 + +ComposerPage { + id: root + + property var shareActionConfiguration + + property var _fileResources: [] + property var _contentResources: [] + property var _tempFiles: [] + + function _addAttachment(fileInfo, title) { + var url = fileInfo.url + "" + + if (!title) { + var fnIndex = url.lastIndexOf('/') + if (fnIndex >= 0) { + title = decodeURIComponent(url.slice(fnIndex+1)) + } + } + + attachmentsModel.append({ + "url": url, + "title": title || fileInfo.fileName, + "mimeType": fileInfo.mimeType, + "fileSize": fileInfo.size + }) + } + + Component.onDestruction: { + shareAction.removeFilesAndRmdir(_tempFiles) + } + + ShareAction { + id: shareAction + + Component.onCompleted: { + shareAction.loadConfiguration(root.shareActionConfiguration) + root.accountId = shareAction.selectedTransferMethodInfo.accountId + + var resources = shareAction.resources + for (var i = 0; i < resources.length; ++i) { + if (typeof resources[i] === "string") { + _fileResources.push(resources[i]) + } else if ((resources[i].type === "text/plain" || resources[i].type === "text/x-url") + && (!resources[i].name)) { + // Show the contents inline within the email. + _contentResources.push(resources[i]) + } else { + var tempFile = shareAction.writeContentToFile(resources[i], root.maximumAttachmentsSize) + if (tempFile.length > 0) { + _tempFiles.push(tempFile) + _fileResources.push(tempFile) + } + } + } + fileInstantiator.model = _fileResources + contentInstantiator.model = _contentResources + } + } + + Instantiator { + id: fileInstantiator + + model: undefined + + delegate: FileInfo { + id: fileInfo + + Component.onCompleted: { + fileInfo.url = modelData + _addAttachment(fileInfo) + } + } + } + + Instantiator { + id: contentInstantiator + + model: undefined + + delegate: FileInfo { + id: contentFileInfo + + Component.onCompleted: { + var content = modelData + if (content.type !== "text/plain" && content.type !== "text/x-url") { + // Other file types should have been converted into temporary files by ShareAction. + console.warn("Unexpected inline email content type:", content.type) + return + } + if (content.type === "text/x-url" && content.linkTitle) { + root.emailBody += (content.linkTitle + "\n\n") + } + if (!!content.status) { + root.emailBody += (content.status + "\n\n") + } + } + } + } +} diff --git a/usr/share/jolla-email/pages/SortPage.qml b/usr/share/jolla-email/pages/SortPage.qml new file mode 100644 index 00000000..35c758bd --- /dev/null +++ b/usr/share/jolla-email/pages/SortPage.qml @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2013 – 2019 Jolla Ltd. + * + * License: Proprietary + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Nemo.Email 0.1 +import "utils.js" as Utils + +Page { + id: root + + property bool isOutgoingFolder + + signal sortSelected(int sortType) + + Component.onCompleted: { + if (isOutgoingFolder) { + sortModel.insert(1, {sortType: EmailMessageListModel.Recipients}) + } else { + sortModel.insert(1, {sortType: EmailMessageListModel.Sender}) + } + } + + SilicaListView { + anchors.fill: parent + model: sortModel + + header: PageHeader { + //% "Sort by" + title: qsTrId("jolla-email-he-sort_by") + } + + delegate: BackgroundItem { + Label { + x: Theme.horizontalPageMargin + anchors.verticalCenter: parent.verticalCenter + text: Utils.sortTypeText(sortType) + color: highlighted ? Theme.highlightColor : Theme.primaryColor + } + + onClicked: root.sortSelected(sortType) + } + VerticalScrollDecorator {} + } + + ListModel { + id: sortModel + + ListElement { + sortType: EmailMessageListModel.Time + } + ListElement { + sortType: EmailMessageListModel.Size + } + ListElement { + sortType: EmailMessageListModel.ReadStatus + } + ListElement { + sortType: EmailMessageListModel.Priority + } + ListElement { + sortType: EmailMessageListModel.Attachments + } + ListElement { + sortType: EmailMessageListModel.Subject + } + } +} diff --git a/usr/share/jolla-email/pages/utils.js b/usr/share/jolla-email/pages/utils.js new file mode 100644 index 00000000..4880dd01 --- /dev/null +++ b/usr/share/jolla-email/pages/utils.js @@ -0,0 +1,174 @@ +/* + * Copyright (c) 2019 Jolla Ltd. + * Copyright (c) 2019 Open Mobile Platform LLC. + * + * License: Proprietary + */ + +.pragma library +.import Sailfish.Silica 1.0 as SS +.import Nemo.Email 0.1 as Email + +var recentFolderSyncs = {} + +function canSyncFolder(key, time) { + // 30 second default folder sync interval. + // Try to avoid too frequent syncs when switching between folders. + var timeout = 30000 + + var lastSync = recentFolderSyncs[key] || 0 + var elapsed = time - lastSync + + if (elapsed < timeout) { + return false + } + + return true +} + +function updateRecentSync(key, value) { + recentFolderSyncs[key] = value +} + +function lastSyncTime(lastSynchronized) { + if (lastSynchronized === 0) { + return "" + } else { + var elapsedTime = SS.Format.formatDate(lastSynchronized, SS.Formatter.TimeElapsed) + + if (elapsedTime === "") { + //: 'Up to date label' + //% "Up to date" + return qsTrId("email-la_up_to_date") + } else { + return elapsedTime + } + } +} + +function priorityIcon(priority) { + if (priority === Email.EmailMessageListModel.HighPriority) { + return "image://theme/icon-s-high-importance" + } else if (priority === Email.EmailMessageListModel.LowPriority) { + return "image://theme/icon-s-low-importance" + } else { + return "" + } +} + +function standardFolderName(folderType, folderName) { + if (folderType === Email.EmailFolder.InboxFolder) { + //: Inbox folder + //% "Inbox" + return qsTrId("jolla-email-la-inbox_folder") + } else if (folderType === Email.EmailFolder.OutboxFolder) { + //: Outbox folder + //% "Outbox" + return qsTrId("jolla-email-la-outbox_folder") + } else if (folderType === Email.EmailFolder.SentFolder) { + //: Sent folder + //% "Sent" + return qsTrId("jolla-email-la-sent_folder") + } else if (folderType === Email.EmailFolder.DraftsFolder) { + //: Drafts folder + //% "Drafts" + return qsTrId("jolla-email-la-drafts_folder") + } else if (folderType === Email.EmailFolder.TrashFolder) { + //: Trash folder + //% "Trash" + return qsTrId("jolla-email-la-trash_folder") + } else { + return folderName + } +} + +function isLocalFolder(folderId) { + return folderId === 1 +} + +function syncErrorText(syncError) { + if (syncError === Email.EmailAgent.SyncFailed) { + //: Synchronization failed error (Shown in app cover, small space) + //% "Sync error" + return qsTrId("jolla-email-la-sync_failed") + } else if (syncError === Email.EmailAgent.LoginFailed) { + //: Login failed error (Shown in app cover, small space) + //% "Login failed" + return qsTrId("jolla-email-la-login_failed") + } else if (syncError === Email.EmailAgent.DiskFull) { + //: Disk full error (Shown in app cover, small space) + //% "Disk Full" + return qsTrId("jolla-email-la-disk_full") + } else if (syncError === Email.EmailAgent.InvalidConfiguration) { + //: Invalid configuration (Shown in app cover, small space) + //% "Configuration error" + return qsTrId("jolla-email-la-invalid_configuration") + } else if (syncError === Email.EmailAgent.UntrustedCertificates) { + //: Invalid certificate (Shown in app cover, small space) + //% "Invalid certificate" + return qsTrId("jolla-email-la-invalid_certificate") + } else if (syncError === Email.EmailAgent.InternalError) { + //: Internal error (Shown in app cover, small space) + //% "Internal error" + return qsTrId("jolla-email-la-internal_error") + } else if (syncError === Email.EmailAgent.SendFailed) { + //: Send failed (Shown in app cover, small space) + //% "Send failed" + return qsTrId("jolla-email-la-send_failed") + } else if (syncError === Email.EmailAgent.Timeout) { + //: Connection timeout (Shown in app cover, small space) + //% "Connection timeout" + return qsTrId("jolla-email-la-connection_timeout") + } else if (syncError === Email.EmailAgent.ServerError) { + //: Server error (Shown in app cover, small space) + //% "Server error" + return qsTrId("jolla-email-la-server_error") + } else if (syncError === Email.EmailAgent.NotConnected) { + //: Not connected (Shown in app cover, small space) + //% "Not connected" + return qsTrId("jolla-email-la-not_connected") + } + + console.warn("Unknown error message") + return "" +} + +function sortTypeText(sortType) { + if (sortType === Email.EmailMessageListModel.Time) { + //: Sort by time + //% "Time" + return qsTrId("jolla-email-me-sort_time") + } else if (sortType === Email.EmailMessageListModel.Sender) { + //: sort by sender + //% "Sender" + return qsTrId("jolla-email-me-sort_sender") + } else if (sortType === Email.EmailMessageListModel.Recipients) { + //: sort by recipients + //% "Recipients" + return qsTrId("jolla-email-me-sort_recipients") + } else if (sortType === Email.EmailMessageListModel.Size) { + //: sort by size + //% "Size" + return qsTrId("jolla-email-me-sort_size") + } else if (sortType === Email.EmailMessageListModel.ReadStatus) { + //: sort by status + //% "Status" + return qsTrId("jolla-email-me-sort_status") + } else if (sortType === Email.EmailMessageListModel.Priority) { + //: sort by priority + //% "Importance" + return qsTrId("jolla-email-me-sort_importance") + } else if (sortType === Email.EmailMessageListModel.Attachments) { + //: sort by attachments + //% "Attachments" + return qsTrId("jolla-email-me-sort_attachments") + } else if (sortType === Email.EmailMessageListModel.Subject) { + //: sort by subject + //% "Subject" + return qsTrId("jolla-email-me-sort_subject") + } + + console.warn("Unknown sort type") + return "" +} + diff --git a/usr/share/jolla-email/pages/webviewframescript.js b/usr/share/jolla-email/pages/webviewframescript.js new file mode 100644 index 00000000..cead0435 --- /dev/null +++ b/usr/share/jolla-email/pages/webviewframescript.js @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2020 Open Mobile Platform LLC. + * + * License: Proprietary + */ + +Services.scriptloader.loadSubScript("chrome://embedlite/content/ClickEventBlocker.js", this); + +addEventListener("DOMContentLoaded", function () { + // If the document doesn't have a viewport meta tag assume it is going to scale poorly and + // add one so it doesn't. + var viewport = content.document.querySelector("meta[name=viewport]"); + if (!viewport) { + viewport = content.document.createElement("meta"); + viewport.name = "viewport" + viewport.content = "width=device-width, initial-scale=1" + content.document.head.appendChild(viewport); + } + + if (content.document.images.length > 0) { + sendAsyncMessage("JollaEmail:DocumentHasImages", {}) + } +}) + +let global = this + +// This will send "embed:OpenLink" message when a link is clicked. +ClickEventBlocker.init(global) diff --git a/usr/share/jolla-gallery/mediasources/VKCacheMediaSource.qml b/usr/share/jolla-gallery/mediasources/FacebookCacheMediaSource.qml similarity index 54% rename from usr/share/jolla-gallery/mediasources/VKCacheMediaSource.qml rename to usr/share/jolla-gallery/mediasources/FacebookCacheMediaSource.qml index 48134f00..c8dd1b20 100644 --- a/usr/share/jolla-gallery/mediasources/VKCacheMediaSource.qml +++ b/usr/share/jolla-gallery/mediasources/FacebookCacheMediaSource.qml @@ -9,55 +9,55 @@ import QtQuick 2.6 import Sailfish.Accounts 1.0 import Sailfish.Silica 1.0 import com.jolla.gallery 1.0 -import com.jolla.gallery.vk 1.0 +import com.jolla.gallery.facebook 1.0 import org.nemomobile.socialcache 1.0 MediaSource { id: root - //: Label of the VK album in Jolla Gallery application - //% "VK" - title: qsTrId("jolla_gallery_vk-user_photos") - icon: StandardPaths.resolveImport("com.jolla.gallery.vk.VKGalleryIcon") + //: Label of the Facebook album in Jolla Gallery application + //% "Facebook" + title: qsTrId("jolla_gallery_facebook-user_photos") + icon: StandardPaths.resolveImport("com.jolla.gallery.facebook.FacebookGalleryIcon") model: allPhotos - count: model.count ready: syncHelper.syncProfiles.length > 0 && accountManager.cloudServiceReady property bool applicationActive: Qt.application.active - property VKImageCacheModel allPhotos: VKImageCacheModel { - type: VKImageCacheModel.Images - nodeIdentifier: constructNodeIdentifier("", "", "", "") - downloader: VKImageDownloader + property FacebookImageCacheModel allPhotos: FacebookImageCacheModel { + type: FacebookImageCacheModel.Images + nodeIdentifier: "" + downloader: FacebookImageDownloader } - property VKImageCacheModel vkUsers: VKImageCacheModel { - type: VKImageCacheModel.Users + property FacebookImageCacheModel fbUsers: FacebookImageCacheModel { + type: FacebookImageCacheModel.Users onCountChanged: { - root.page = count < 2 ? StandardPaths.resolveImport("com.jolla.gallery.vk.VKAlbumsPage") - : StandardPaths.resolveImport("com.jolla.gallery.vk.VKUsersPage") + root.page = count < 2 ? StandardPaths.resolveImport("com.jolla.gallery.facebook.AlbumsPage") + : StandardPaths.resolveImport("com.jolla.gallery.facebook.UsersPage") } + onModelUpdated: root.count = count > 0 ? getField(0, FacebookImageCacheModel.Count) : 0 } property AccountManager accountManager: AccountManager { property bool cloudServiceReady Component.onCompleted: { - cloudServiceReady = enabledAccounts("vk", "vk-images").length > 0 + cloudServiceReady = enabledAccounts("facebook", "facebook-images").length > 0 } } property SyncHelper syncHelper: SyncHelper { - socialNetwork: SocialSync.VK + socialNetwork: SocialSync.Facebook dataType: SocialSync.Images onLoadingChanged: { if (!loading) { - vkUsers.refresh() + fbUsers.refresh() allPhotos.refresh() } } onProfileDeleted: { - vkUsers.refresh() + fbUsers.refresh() allPhotos.refresh() } } @@ -65,7 +65,7 @@ MediaSource { // TODO: add a way to refresh the albums Component.onCompleted: { - vkUsers.refresh() + fbUsers.refresh() allPhotos.refresh() } } diff --git a/usr/share/jolla-gallery/pages/CoverPhoto.qml b/usr/share/jolla-gallery/pages/CoverPhoto.qml index 4a90ae38..22f04a47 100644 --- a/usr/share/jolla-gallery/pages/CoverPhoto.qml +++ b/usr/share/jolla-gallery/pages/CoverPhoto.qml @@ -1,6 +1,6 @@ import QtQuick 2.2 import Sailfish.Silica 1.0 -import org.nemomobile.thumbnailer 1.0 +import Nemo.Thumbnailer 1.0 Item { id: photo diff --git a/usr/share/jolla-gallery/pages/FlickableImageView.qml b/usr/share/jolla-gallery/pages/FlickableImageView.qml index 7833619f..38ebec5b 100644 --- a/usr/share/jolla-gallery/pages/FlickableImageView.qml +++ b/usr/share/jolla-gallery/pages/FlickableImageView.qml @@ -112,7 +112,7 @@ PagedView { player: GalleryMediaPlayer { autoPlay: root.autoPlay active: currentItem && !currentItem.isImage && Qt.application.active - source: active ? currentItem.source : "" + source: (currentItem && !currentItem.isImage) ? currentItem.source : "" onPlayingChanged: { if (playing && overlay.active) { // go fullscreen for playback if triggered via Play icon. diff --git a/usr/share/jolla-gallery/pages/GalleryCover.qml b/usr/share/jolla-gallery/pages/GalleryCover.qml index d4d259f2..228ebdbe 100644 --- a/usr/share/jolla-gallery/pages/GalleryCover.qml +++ b/usr/share/jolla-gallery/pages/GalleryCover.qml @@ -2,7 +2,7 @@ import QtQuick 2.0 import QtDocGallery 5.0 import Sailfish.Silica 1.0 import com.jolla.gallery 1.0 -import org.nemomobile.thumbnailer 1.0 +import Nemo.Thumbnailer 1.0 CoverBackground { id: cover diff --git a/usr/share/jolla-gallery/pages/GalleryMediaIcon.qml b/usr/share/jolla-gallery/pages/GalleryMediaIcon.qml index 0ede0c59..873d2621 100644 --- a/usr/share/jolla-gallery/pages/GalleryMediaIcon.qml +++ b/usr/share/jolla-gallery/pages/GalleryMediaIcon.qml @@ -1,6 +1,6 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 -import org.nemomobile.thumbnailer 1.0 +import Nemo.Thumbnailer 1.0 import com.jolla.gallery 1.0 MediaSourceIcon { diff --git a/usr/share/jolla-gallery/pages/GalleryStartPage.qml b/usr/share/jolla-gallery/pages/GalleryStartPage.qml index 59fc4b42..bc49ff05 100644 --- a/usr/share/jolla-gallery/pages/GalleryStartPage.qml +++ b/usr/share/jolla-gallery/pages/GalleryStartPage.qml @@ -72,8 +72,7 @@ Page { imageViewerPage = pageStack.push( Qt.resolvedUrl("GalleryFullscreenPage.qml"), { model: viewerModel, - currentIndex: viewerModel.count - urls.length, - viewerOnlyMode: true + currentIndex: viewerModel.count - urls.length }, PageStackAction.Immediate) if (viewerAction) { diff --git a/usr/share/jolla-mediaplayer/cover/IdleCover.qml b/usr/share/jolla-mediaplayer/cover/IdleCover.qml new file mode 100644 index 00000000..9a488ba7 --- /dev/null +++ b/usr/share/jolla-mediaplayer/cover/IdleCover.qml @@ -0,0 +1,68 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import com.jolla.mediaplayer 1.0 + +Item { + property bool sourcesReady + property url largeAlbumArt + property url leftSmallAlbumArt + property url rightSmallAlbumArt + + property bool _leftSmall: leftSmallAlbumArt != "" + property bool _rightSmall: rightSmallAlbumArt != "" + + onSourcesReadyChanged: { + // sourcesReady changes after _leftSmall and _rightSmall have changed. + // Thus, image sizes have been figured out before loading images. + if (sourcesReady) { + largeThumbnail.source = largeAlbumArt + + if (_leftSmall) { + leftThumbnail.source = leftSmallAlbumArt + } + + if (_rightSmall) { + rightThumbnail.source = rightSmallAlbumArt + } + } + } + + Image { + id: largeThumbnail + + width: parent.width + height: _leftSmall || _rightSmall ? width : parent.height + sourceSize.width: width + sourceSize.height: height + fillMode: Image.PreserveAspectCrop + } + + Image { + id: leftThumbnail + + anchors.top: largeThumbnail.bottom + width: _rightSmall ? parent.width / 2 : parent.width + height: parent.height - largeThumbnail.height + sourceSize.width: width + sourceSize.height: height + fillMode: Image.PreserveAspectCrop + opacity: Theme.opacityLow + visible: _leftSmall + } + + Image { + id: rightThumbnail + + anchors { + top: leftThumbnail.top + left: leftThumbnail.right + } + width: parent.width / 2 + height: parent.height - largeThumbnail.height + sourceSize.width: width + sourceSize.height: height + fillMode: Image.PreserveAspectCrop + opacity: Theme.opacityLow + visible: _rightSmall + } +} diff --git a/usr/share/jolla-mediaplayer/cover/MediaPlayerCover.qml b/usr/share/jolla-mediaplayer/cover/MediaPlayerCover.qml new file mode 100644 index 00000000..af23cc8a --- /dev/null +++ b/usr/share/jolla-mediaplayer/cover/MediaPlayerCover.qml @@ -0,0 +1,254 @@ +// -*- qml -*- + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import com.jolla.mediaplayer 1.0 + +CoverBackground { + id: root + + property alias idle: idleCover + property string idleArtist + property string idleSong + property var _reservedOrInvalid: ({}) + + function fetchAlbumArts(count) { + var artList = [] + for (var i = 0; i < count; ++i) { + var albumArt = randomArt(allSongModel, albumArtProvider) + artList.push(albumArt) + } + return artList + } + + function randomArt(model, albumArtProvider) { + var randomIndex = Math.floor(Math.random() * model.count) + var i = randomIndex + var count = model.count + var textualArt + // From index to end as index is a random model index + while (i < count) { + var song = model.get(i) + if (!_reservedOrInvalid[song.album + song.author]) { + var image = albumArtProvider.albumThumbnail(song.album, song.author) + _reservedOrInvalid[song.album + song.author] = true + if (image != "") { + return { + url: image, + author: song.author, + title: song.title + } + } + } else if (!textualArt) { + textualArt = { + url: image, + author: song.author, + title: song.title + } + } + ++i + } + + // From beginning to index as index is a random model index + i = 0 + count = randomIndex + while (i < count) { + song = model.get(i) + if (!_reservedOrInvalid[song.album + song.author]) { + image = albumArtProvider.albumThumbnail(song.album, song.author) + _reservedOrInvalid[song.album + song.author] = true + if (image != "") { + return { + url: image, + author: song.author, + title: song.title + } + } + } else if (!textualArt) { + textualArt = { + url: image, + author: song.author, + title: song.title + } + } + ++i + } + + return textualArt ? textualArt : { url : "", album: "", author: ""} + } + + width: Theme.coverSizeLarge.width + height: Theme.coverSizeLarge.height + + VisualAudioModel { + id: visualAudioModel + modelActive: status != Cover.Inactive + } + + CoverPlaceholder { + //: Coverpage text when there are no media + //% "Get music" + text: qsTrId("mediaplayer-la-get-music") + icon.source: "image://theme/icon-launcher-mediaplayer" + visible: allSongModel.count === 0 && !visualAudioModel.active + } + + IdleCover { + id: idleCover + anchors.fill: parent + visible: !visualAudioModel.active + } + + Image { + id: albumArtImage + visible: source != "" && visualAudioModel.active + anchors.fill: parent + sourceSize.width: width + sourceSize.height: height + source: visualAudioModel.metadata && 'url' in visualAudioModel.metadata + ? albumArtProvider.albumThumbnail(visualAudioModel.metadata.album, visualAudioModel.metadata.artist) + : "" + fillMode: Image.PreserveAspectCrop + } + + Rectangle { + width: parent.width + height: column.y + column.height + 2*Theme.paddingLarge + visible: albumArtImage.visible + gradient: Gradient { + GradientStop { position: 0.0; color: Qt.rgba(0, 0, 0, Theme.opacityHigh) } + GradientStop { position: 0.6; color: Qt.rgba(0, 0, 0, Theme.opacityLow) } + GradientStop { position: 1.0; color: "transparent" } + } + } + + Column { + id: column + + x: Theme.paddingMedium + y: Theme.paddingMedium + spacing: Theme.paddingSmall + visible: allSongModel.count > 0 + width: parent.width - 2*Theme.paddingMedium + + Label { + id: durationLabel + anchors.horizontalCenter: parent.horizontalCenter + text: visualAudioModel.duration >= 3600000 ? + Format.formatDuration(visualAudioModel.position / 1000, Formatter.DurationLong) : + Format.formatDuration(visualAudioModel.position / 1000, Formatter.DurationShort) + color: (albumArtImage.visible && Theme.colorScheme == Theme.DarkOnLight) + ? Theme.highlightFromColor(Theme.highlightColor, Theme.LightOnDark) + : Theme.highlightColor + font.pixelSize: visualAudioModel.duration >= 3600000 ? Theme.fontSizeExtraLarge : Theme.fontSizeHuge + opacity: visualAudioModel.active ? (visualAudioModel.state === Audio.Paused ? Theme.opacityHigh : 1.0) + : 0.0 + } + + Label { + id: artistName + visible: (!albumArtImage.visible && visualAudioModel.active && text != "") || idleArtist != "" + text: visualAudioModel.metadata && 'url' in visualAudioModel.metadata + ? visualAudioModel.metadata.artist + : (idleArtist != "" ? idleArtist : "") + anchors.horizontalCenter: parent.horizontalCenter + width: Math.min(implicitWidth, parent.width) + color: durationLabel.color + truncationMode: TruncationMode.Fade + font.pixelSize: Theme.fontSizeLarge + lineHeightMode: Text.FixedHeight + lineHeight: Theme.itemSizeMedium/2 // to align with clock cover text + maximumLineCount: 1 + } + + Item { + width: parent.width + height: songTitle.height + visible: visualAudioModel.active || idleSong != "" + + Label { + id: songTitle + text: visualAudioModel.metadata && 'url' in visualAudioModel.metadata + ? visualAudioModel.metadata.title + : (idleSong != "" ? idleSong : "") + anchors.horizontalCenter: parent.horizontalCenter + width: Math.min(implicitWidth, parent.width) + maximumLineCount: artistName.visible ? 2 : 3 + color: (albumArtImage.visible && Theme.colorScheme == Theme.DarkOnLight) + ? Theme.lightPrimaryColor : Theme.primaryColor + wrapMode: Text.WordWrap + font.pixelSize: Theme.fontSizeLarge + lineHeightMode: Text.FixedHeight + lineHeight: Theme.itemSizeMedium/2 // to align with clock cover text + onLineLaidOut: { + // last line can show text as much as there fits + if (line.number == maximumLineCount - 1) { + line.width = parent.width + 1000 + } + } + } + + OpacityRampEffect { + offset: 0.5 + // FIXME: OpacityRampEffect spits a warning when + // songTitle doesn't have an actual text + sourceItem: songTitle + enabled: songTitle.implicitWidth > Math.ceil(songTitle.width) + } + } + + } + + CoverActionList { + id: coverActions + + iconBackground: albumArtImage.visible + enabled: visualAudioModel.active + + CoverAction { + iconSource: visualAudioModel.state == Audio.Playing ? "image://theme/icon-cover-pause" : "image://theme/icon-cover-play" + onTriggered: AudioPlayer.playPause() + } + + CoverAction { + iconSource: "image://theme/icon-cover-next-song" + onTriggered: AudioPlayer.playNext(true) + } + } + + CoverActionList { + enabled: !coverActions.enabled && allSongModel.count > 0 + + CoverAction { + iconSource: "image://theme/icon-cover-shuffle" + onTriggered: AudioPlayer.shuffleAndPlay(allSongModel, allSongModel.count) + } + } + + Connections { + target: allSongModel + + //: placeholder string for albums without a known name + //% "Unknown album" + readonly property string unknownAlbum: qsTrId("mediaplayer-la-unknown-album") + + //: placeholder string to be shown for media without a known artist + //% "Unknown artist" + readonly property string unknownArtist: qsTrId("mediaplayer-la-unknown-artist") + + onFinished: { + var artList = fetchAlbumArts(3) + if (artList.length > 0) { + if (!artList[0].url || artList[0].url == "") { + root.idleArtist = artList[0].author ? artList[0].author : unknownArtist + root.idleSong = artList[0].title ? artList[0].title : unknownAlbum + } else { + root.idle.largeAlbumArt = artList[0].url + root.idle.leftSmallAlbumArt = artList[1] && artList[1].url ? artList[1].url : "" + root.idle.rightSmallAlbumArt = artList[2] && artList[2].url ? artList[2].url : "" + root.idle.sourcesReady = true + } + } + } + } +} diff --git a/usr/share/jolla-mediaplayer/mediaplayer.qml b/usr/share/jolla-mediaplayer/mediaplayer.qml new file mode 100644 index 00000000..c4b32a2a --- /dev/null +++ b/usr/share/jolla-mediaplayer/mediaplayer.qml @@ -0,0 +1,129 @@ +// -*- qml -*- + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Sailfish.Media 1.0 +import com.jolla.mediaplayer 1.0 +import Nemo.Thumbnailer 1.0 // register image provider +import Nemo.DBus 2.0 +import "cover" +import "pages" + +ApplicationWindow { + id: root + + property Item _dockedPanel + property alias playlists: playlistManager + property alias visualAudioAppModel: audioAppModel + property Item activeMediaSource + property Component coverOverride: (activeMediaSource && activeMediaSource.hasOwnProperty("cover")) + ? activeMediaSource.cover + : null + onActiveMediaSourceChanged: AudioPlayer.mprisPlayerOverride = + (activeMediaSource && activeMediaSource.hasOwnProperty("mprisPlayer")) + ? activeMediaSource.mprisPlayer + : null + + allowedOrientations: Screen.sizeCategory > Screen.Medium + ? defaultAllowedOrientations + : defaultAllowedOrientations & Orientation.PortraitMask + _defaultPageOrientations: Orientation.All + _defaultLabelFormat: Text.PlainText + + cover: coverOverride != null ? coverOverride : Qt.resolvedUrl("cover/MediaPlayerCover.qml") + + bottomMargin: _dockedPanel ? _dockedPanel.visibleSize : 0 + + initialPage: Component { + MainViewPage { + onMediaSourceActivated: { + root.activeMediaSource = source + } + } + } + + function dockedPanel() { + if (!_dockedPanel) _dockedPanel = panelComponent.createObject(contentItem) + return _dockedPanel + } + + VisualAudioAppModel { + id: audioAppModel + modelActive: root.applicationActive + onActiveChanged: dockedPanel() + onModelActiveChanged: { + if (active) { + dockedPanel().showControls() + } + } + } + + Component { + id: panelComponent + MediaPlayerDockedPanel { + z: 1 + author: audioAppModel.metadata && 'artist' in audioAppModel.metadata ? audioAppModel.metadata.artist : "" + title: audioAppModel.metadata && 'title' in audioAppModel.metadata ? audioAppModel.metadata.title : "" + duration: audioAppModel.duration / 1000 + state: audioAppModel.state + active: audioAppModel.active + position: audioAppModel.position / 1000 + } + } + + AlbumArtProvider { + id: albumArtProvider + songsModel: allSongModel + } + + GriloTrackerModel { + id: allSongModel + + query: AudioTrackerHelpers.getSongsQuery("", {"unknownArtist": "", "unknownAlbum": "" }) + } + + PlaylistManager { + id: playlistManager + } + + Component { + id: playQueuePage + PlayQueuePage {} + } + + Connections { + target: AudioPlayer + onTryingToPlay: dockedPanel().showControls() + } + + DBusAdaptor { + service: "com.jolla.mediaplayer" + path: "/com/jolla/mediaplayer/ui" + iface: "com.jolla.mediaplayer.ui" + + function activateWindow(arg) { + root.activate() + } + + function openUrl(arg) { + if (arg.length === 0) { + root.activate() + + return true + } + + AudioPlayer.playUrl(Qt.resolvedUrl(arg[0])) + if (!pageStack.currentPage || pageStack.currentPage.objectName !== "PlayQueuePage") { + pageStack.push(playQueuePage, {}, PageStackAction.Immediate) + } + dockedPanel().open = true + activate() + + return true + } + } + + // Ensure plugin overrides are disabled when the app shuts down + Component.onDestruction: activeMediaSource = null + Component.onCompleted: AudioPlayer.albumArtProvider = albumArtProvider +} diff --git a/usr/share/jolla-mediaplayer/pages/MainViewPage.qml b/usr/share/jolla-mediaplayer/pages/MainViewPage.qml new file mode 100644 index 00000000..138281a0 --- /dev/null +++ b/usr/share/jolla-mediaplayer/pages/MainViewPage.qml @@ -0,0 +1,194 @@ +// -*- qml -*- + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Sailfish.Media 1.0 +import com.jolla.mediaplayer 1.0 + +Page { + id: mainPage + + signal mediaSourceActivated(Item source) + + FilterModel { + id: filteredMediaSourceList + + sourceModel: mediaSourceList + + // Filter out if the value is not "1" + filterRegExp: mainPageHeader.searchText !== "" ? /^1$/ : RegExp("") + } + + MediaPlayerListView { + id: mainListView + model: filteredMediaSourceList + anchors.fill: parent + + PullDownMenu { + // FIXME: hiding search on this page now due to performing badly, should be reimplemented better + visible: visualAudioAppModel.active + NowPlayingMenuItem { id: nowPlaying } + + MenuItem { + //: Search menu entry + //% "Search" + text: qsTrId("mediaplayer-me-search") + onClicked: mainPageHeader.enableSearch() + visible: false + } + } + + header: SearchPageHeader { + id: mainPageHeader + width: parent.width + searchAsHeader: true + + //: Title for the main page + //% "Media" + title: qsTrId("mediaplayer-he-media") + + //: Main view search field placeholder text + //% "Search Media" + placeholderText: qsTrId("mediaplayer-tf-search-media") + + Binding { + target: mediaSourceList + property: "searchText" + value: if (pageStack.currentPage === mainPage) mainPageHeader.searchText + } + + Column { + id: playlistsItem + + width: parent.width + height: { + var height = playlistsCategory.shouldBeVisible ? playlistsCategory.height : 0 + if (playlists.populated && playlistRow.count === 0) { + return height + } else { + return height + playlistRow.height + } + } + + clip: playlistRow.count > playlistRow.maxCount + Behavior on height { NumberAnimation { duration: 200; easing.type: Easing.InOutQuad } } + + MediaContainerIconDelegate { + id: playlistsCategory + + readonly property bool populated: playlists.populated + readonly property bool createNew: playlists.count == 0 + readonly property bool shouldBeVisible: playlists.count !== 0 || mainPageHeader.searchText === "" + + width: parent.width + opacity: shouldBeVisible ? 1.0 : 0.0 + Behavior on opacity { FadeAnimation {} } + visible: opacity > 0.0 + iconSource: "image://theme/icon-m-media-playlists" + title: !populated ? " " + : createNew + ? //: Playlists list item in the main view + //% "New playlist" + qsTrId("mediaplayer-me-new-playlist") + : //: Playlists list item in the main view + //% "Playlists" + qsTrId("mediaplayer-la-playlists") + + //: Number of playlists + //% "%n playlists" + subtitle: populated ? (!createNew ? qsTrId("mediaplayer-la-number-of-playlists", playlists.count) : "") + : " " + onClicked: { + if (createNew) { + pageStack.animatorPush("com.jolla.mediaplayer.NewPlaylistDialog") + } else { + pageStack.animatorPush(Qt.resolvedUrl("PlaylistsPage.qml"), {searchText: mainPageHeader.searchText}) + } + } + } + + SilicaGridView { + id: playlistRow + + readonly property int maxCount: Math.floor(parent.width / Theme.itemSizeExtraLarge) + + width: parent.width + height: Theme.itemSizeExtraLarge + __silica_menu_height + cellHeight: Theme.itemSizeExtraLarge + cellWidth: width / Math.max(maxCount, 1) + flow: GridView.FlowTopToBottom + + opacity: count > 0 ? 1.0 : 0.0 + Behavior on opacity { FadeAnimator {} } + visible: !playlists.populated || count > 0 + + interactive: false + model: GriloTrackerModel { + id: playlistModel + query: PlaylistTrackerHelpers.getPlaylistsQuery(mainPageHeader.searchText, {"sortByUsage": true}) + } + + Connections { + target: playlists + onUpdated: playlistModel.refresh() + } + + delegate: PlaylistItem { + width: playlistRow.cellWidth + contentHeight: playlistRow.cellHeight + + highlighted: down || menuOpen + color: model.title != "" ? PlaylistColors.nameToColor(model.title) + : "transparent" + highlightColor: model.title != "" ? PlaylistColors.nameToHighlightColor(model.title) + : "transparent" + + menu: Component { + ContextMenu { + MenuItem { + //% "Delete" + text: qsTrId("mediaplayer-me-delete") + onClicked: remove() + } + } + } + + function remove() { + remorseDelete(function() { + playlists.removePlaylist(media) + }) + } + } + } + } + } + + delegate: MediaContainerIconDelegate { + id: delegate + + width: ListView.view.width + title: mediaSource.title + subtitle: mediaSource.subtitle + iconSource: mediaSource.icon + iconSourceSize.width: Theme.iconSizeMedium + iconSourceSize.height: Theme.iconSizeMedium + + onClicked: { + var obj = pageStack.animatorPush(Qt.resolvedUrl(mediaSource.mainView), + {model: mediaSource.model, searchText: mediaSource.searchText}) + obj.pageCompleted.connect(function(view) { + mainPage.mediaSourceActivated(view) + }) + } + ListView.onAdd: AddAnimation { target: delegate } + ListView.onRemove: animateRemoval() + } + + ViewPlaceholder { + //: Placeholder text for an empty search view + //% "No items found" + text: qsTrId("mediaplayer-la-empty-search") + enabled: mainListView.count === 0 && !playlistRow.visible && !playlistsCategory.visible + } + } +} diff --git a/usr/share/jolla-mediaplayer/pages/PlaylistItem.qml b/usr/share/jolla-mediaplayer/pages/PlaylistItem.qml new file mode 100644 index 00000000..5223dcc1 --- /dev/null +++ b/usr/share/jolla-mediaplayer/pages/PlaylistItem.qml @@ -0,0 +1,60 @@ +// -*- qml -*- + +import QtQuick 2.0 +import Sailfish.Silica 1.0 + +GridItem { + id: playlistItem + + property color color: Theme.overlayBackgroundColor + property color highlightColor: Theme.highlightBackgroundColor + + width: Theme.itemSizeExtraLarge + contentHeight: Theme.itemSizeExtraLarge + onClicked: pageStack.animatorPush(Qt.resolvedUrl("PlaylistPage.qml"), {media: media}) + + Rectangle { + id: background + + anchors.fill: parent + color: playlistItem.highlighted ? playlistItem.highlightColor : playlistItem.color + } + + Rectangle { + width: parent.width + height: parent.height / 2 + gradient: Gradient { + GradientStop { position: 0.0; color: Qt.rgba(0, 0, 0, Theme.opacityLow) } + GradientStop { position: 1.0; color: Qt.rgba(0, 0, 0, 0) } + } + } + + Image { + source: "image://theme/graphic-media-playlist-exlarge" + anchors.bottom: parent.bottom + anchors.right: parent.right + } + + Label { + id: name + + y: Theme.paddingMedium + x: Theme.paddingLarge + width: parent.width - x + truncationMode: TruncationMode.Fade + color: playlistItem.highlighted ? Theme.highlightColor: Theme.primaryColor + font.pixelSize: Theme.fontSizeExtraSmall + text: media.title + } + + Label { + anchors.left: name.left + anchors.right: name.right + anchors.top: name.bottom + anchors.topMargin: Theme.paddingSmall + truncationMode: TruncationMode.Fade + color: playlistItem.highlighted ? Theme.highlightColor: Theme.primaryColor + font.pixelSize: Theme.fontSizeLarge + text: media.childCount + } +} diff --git a/usr/share/jolla-mediaplayer/pages/PlaylistPage.qml b/usr/share/jolla-mediaplayer/pages/PlaylistPage.qml new file mode 100644 index 00000000..05180488 --- /dev/null +++ b/usr/share/jolla-mediaplayer/pages/PlaylistPage.qml @@ -0,0 +1,142 @@ +// -*- qml -*- + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Sailfish.Media 1.0 +import com.jolla.mediaplayer 1.0 + +Page { + id: page + + property var media + property bool isEditable: playlists.isEditable(media.url) + + Connections { + target: playlists + onUpdated: { + if (playlistUrl == originalPlaylistModel.url) { + originalPlaylistModel.clear() + originalPlaylistModel.populate() + } + } + } + + FilterModel { + id: playlistModel + sourceModel: originalPlaylistModel + + filterRegExp: RegExpHelpers.regExpFromSearchString(playlistHeader.searchText, true) + } + + PlaylistModel { + id: originalPlaylistModel + url: media.url + Component.onCompleted: populate() + } + + MediaPlayerListView { + id: view + anchors.fill: parent + model: playlistModel + + PullDownMenu { + enabled: playlistModel.count > 0 + visible: playlistModel.count > 0 + + MenuItem { + //: Add to playing queue drop down menu item in playlist page + //% "Add to playing queue" + text: qsTrId("mediaplayer-me-playlist-add-to-playing-queue") + onClicked: AudioPlayer.addToQueue(playlistModel) + } + + MenuItem { + //: Clear playlist drop down menu item in playlist page + //% "Clear playlist" + text: qsTrId("mediaplayer-me-playlist-clear-playlist") + visible: isEditable + + onClicked: { + //: Clearing the playlist + //% "Clearing" + Remorse.popupAction(page, qsTrId("mediaplayer-la-clearing"), function() { + originalPlaylistModel.clear() + if (playlists.clearPlaylist(media, originalPlaylistModel)) { + pageStack.pop() + } + }) + } + } + + NowPlayingMenuItem { } + + MenuItem { + //: Search menu entry + //% "Search" + text: qsTrId("mediaplayer-me-search") + onClicked: playlistHeader.enableSearch() + enabled: view.count > 0 || playlistHeader.searchText !== '' + } + } + + ViewPlaceholder { + text: { + if (playlistHeader.searchText !== '') { + //: Placeholder text for an empty search view + //% "No items found" + return qsTrId("mediaplayer-la-empty-search") + } else { + //: "Placeholder text for an empty playlist; Add songs to playlist" + //% "Add some media" + return qsTrId("mediaplayer-la-add-some-media") + } + } + enabled: playlistModel.count === 0 + } + + header: SearchPageHeader { + id: playlistHeader + width: parent.width + + title: media.title + + //: Playlist search field placeholder text + //% "Search song" + placeholderText: qsTrId("mediaplayer-tf-playlist-search") + } + + delegate: MediaListDelegate { + property int realIndex: playlistModel.mapRowToSource(index) + + formatFilter: playlistHeader.searchText + + function remove() { + remorseDelete(function() { + if (realIndex >= 0 ) { + originalPlaylistModel.remove(realIndex) + playlists.savePlaylist(page.media, originalPlaylistModel) + } + }) + } + + menu: menuComponent + onClicked: { + AudioPlayer.play(view.model, index) + playlists.updateAccessTime(page.media.url) + } + ListView.onRemove: animateRemoval() + + Component { + id: menuComponent + ContextMenu { + MenuItem { + //: Remove from playlist context menu item in playlist page + //% "Remove from playlist" + text: qsTrId("mediaplayer-me-playlist-remove-from-playlist") + onClicked: remove() + } + } + } + } + } +} diff --git a/usr/share/jolla-mediaplayer/pages/PlaylistsPage.qml b/usr/share/jolla-mediaplayer/pages/PlaylistsPage.qml new file mode 100644 index 00000000..8fa6333f --- /dev/null +++ b/usr/share/jolla-mediaplayer/pages/PlaylistsPage.qml @@ -0,0 +1,115 @@ +// -*- qml -*- + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Sailfish.Media 1.0 +import com.jolla.mediaplayer 1.0 + +Page { + id: playlistsPage + + property string searchText + + MediaPlayerListView { + id: view + + anchors.fill: parent + model: GriloTrackerModel { + id: playlistModel + query: PlaylistTrackerHelpers.getPlaylistsQuery(playlistsHeader.searchText, {}) + } + + Connections { + target: playlists + onUpdated: playlistModel.refresh() + } + + PullDownMenu { + + MenuItem { + //: Menu label for adding a new playlist + //% "New playlist" + text: qsTrId("mediaplayer-me-new-playlist") + onClicked: pageStack.animatorPush("com.jolla.mediaplayer.NewPlaylistDialog", {}) + } + + NowPlayingMenuItem { } + + MenuItem { + //: Search menu entry + //% "Search" + text: qsTrId("mediaplayer-me-search") + onClicked: playlistsHeader.enableSearch() + enabled: view.count > 0 || playlistsHeader.searchText !== '' + } + } + + header: SearchPageHeader { + id: playlistsHeader + + width: parent.width + + //: page header for the playlists page + //% "Playlists" + title: qsTrId("mediaplayer-he-playlists") + + //: Playlists search field placeholder text + //% "Search playlist" + placeholderText: qsTrId("mediaplayer-tf-playlists-search") + + searchText: playlistsPage.searchText + Component.onCompleted: if (searchText !== '') enableSearch() + } + + delegate: MediaContainerPlaylistDelegate { + formatFilter: playlistsHeader.searchText + color: model.title != "" ? PlaylistColors.nameToColor(model.title) + : "transparent" + highlightColor: model.title != "" ? PlaylistColors.nameToHighlightColor(model.title) + : "transparent" + title: media.title + songCount: media.childCount + menu: menuComponent + + // FIXME: makes the transparent color show up briefly + ListView.onRemove: animateRemoval() + onClicked: pageStack.animatorPush(Qt.resolvedUrl("PlaylistPage.qml"), {media: media}) + + function remove() { + remorseDelete(function() { playlists.removePlaylist(media) }) + } + + Component { + id: menuComponent + ContextMenu { + MenuItem { + //% "Delete" + text: qsTrId("mediaplayer-me-delete") + onClicked: remove() + } + } + } + } + + ViewPlaceholder { + text: { + if (playlistsHeader.searchText !== '') { + //: Placeholder text for an empty search view + //% "No items found" + return qsTrId("mediaplayer-la-empty-search") + } else { + //: Placeholder text for an empty playlists view + //% "Create a playlist" + return qsTrId("mediaplayer-la-create-a-playlist") + } + } + enabled: view.count === 0 && !busyIndicator.running + } + + PageBusyIndicator { + id: busyIndicator + + running: playlistModel.fetching + } + } +} diff --git a/usr/share/jolla-mediaplayer/plugins/fmradio/ChannelSearchPage.qml b/usr/share/jolla-mediaplayer/plugins/fmradio/ChannelSearchPage.qml deleted file mode 100644 index 244351bb..00000000 --- a/usr/share/jolla-mediaplayer/plugins/fmradio/ChannelSearchPage.qml +++ /dev/null @@ -1,119 +0,0 @@ -import QtQuick 2.0 -import QtMultimedia 5.0 -import Sailfish.Silica 1.0 -import com.jolla.mediaplayer.radio 1.0 - -Page { - property Radio radio - property var availableStations - property var bookmarks - - onStatusChanged: { - if (status == PageStatus.Inactive) { - radio.cancelSearchAll() - } else if (status == PageStatus.Activating && availableStations.length == 0) { - radio.searchAll() - } - } - - FrequencyFormatter { - id: formatter - } - - SilicaListView { - id: channelList - - header: PageHeader { - // translation on push up menu - title: qsTrId("jolla-mediaplayer-radio-available_channels") - } - - anchors.fill: parent - model: availableStations - delegate: ListItem { - id: channelItem - - menu: contextMenu - onClicked: { - radio.frequency = modelData - radio.startPlay() - } - - Label { - id: frequencyText - - anchors.centerIn: parent - font.pixelSize: Theme.fontSizeLarge - color: channelItem.highlighted || radio.frequency == modelData ? Theme.highlightColor - : Theme.primaryColor - text: formatter.formatMegahertz(modelData / 1000000) - } - Label { - anchors.left: frequencyText.right - anchors.right: parent.right - anchors.leftMargin: Theme.paddingMedium - anchors.baseline: frequencyText.baseline - font.pixelSize: Theme.fontSizeMedium - truncationMode: TruncationMode.Fade - color: channelItem.highlighted || radio.frequency == modelData ? Theme.highlightColor - : Theme.secondaryColor - text: { - var index = bookmarks.findByFrequency(modelData) - if (index >= 0) { - return bookmarks.get(index, RadioBookmarks.NameRole) - } else if (modelData == radio.frequency) { - return radio.radioData.stationName.trim() - } else { - return "" - } - } - } - Component { - id: contextMenu - - ContextMenu { - MenuItem { - //% "Add to favorites" - text: qsTrId("jolla-mediaplayer-radio-add_to_favorites") - onClicked: { - bookmarks.addStation(radio.radioData.stationName.trim(), - radio.radioData.stationId.trim(), - radio.frequency) - channelList.update() - } - } - } - } - } - - PullDownMenu { - visible: !radio.searching - - MenuItem { - //: Initiate channel search from pulley menu - //% "Search" - text: qsTrId("jolla-mediaplayer-radio-search") - onClicked: radio.searchAll() - } - } - - Label { - anchors.bottom: searchIndicator.top - anchors.bottomMargin: Theme.paddingMedium - anchors.horizontalCenter: searchIndicator.horizontalCenter - color: Theme.highlightColor - //% "Searching..." - text: qsTrId("jolla-mediaplayer-radio-searching_stations") - opacity: searchIndicator.opacity - visible: searchIndicator.visible - } - - BusyIndicator { - id: searchIndicator - - anchors.centerIn: parent - size: BusyIndicatorSize.Large - running: radio.searching - } - } -} diff --git a/usr/share/jolla-mediaplayer/plugins/fmradio/fmradio.qml b/usr/share/jolla-mediaplayer/plugins/fmradio/fmradio.qml deleted file mode 100644 index 8a6d6fb5..00000000 --- a/usr/share/jolla-mediaplayer/plugins/fmradio/fmradio.qml +++ /dev/null @@ -1,565 +0,0 @@ -// -*- qml -*- - -import QtQuick 2.0 -import Sailfish.Silica 1.0 -import Sailfish.Media 1.0 -import com.jolla.mediaplayer 1.0 -import com.jolla.mediaplayer.radio 1.0 -import org.nemomobile.configuration 1.0 -import QtMultimedia 5.0 -import Amber.Mpris 1.0 - -Page { - id: root - - property var model // set by framework, should have a better interface - property string searchText // unused, just expected by mediaplayer page push - - property var bookmarks: model - property var availableStations: [] - property Component cover: Component { - CoverBackground { - Image { - anchors.fill: parent - source: "image://theme/graphic-cover-fmradio" - } - - Column { - anchors.bottom: parent.bottom - anchors.bottomMargin: Theme.paddingLarge - width: parent.width - - Label { - text: formatter.formatMegahertz(radio.frequency / 1000000) - anchors.horizontalCenter: parent.horizontalCenter - font.pixelSize: Theme.fontSizeHuge - } - - Item { - width: 1 - height: Theme.paddingSmall - } - - Label { - width: Math.min(parent.width - Theme.paddingMedium, implicitWidth) - x: Math.max((parent.width - width) / 2, Theme.paddingMedium) - truncationMode: TruncationMode.Fade - font.pixelSize: Theme.fontSizeSmall - text: stationText.text - } - - Label { - width: Math.min(parent.width - Theme.paddingMedium, implicitWidth) - x: Math.max((parent.width - width) / 2, Theme.paddingMedium) - truncationMode: TruncationMode.Fade - color: Theme.secondaryColor - font.pixelSize: Theme.fontSizeExtraSmall - text: radioText.text - } - } - } - } - property ProxyMprisPlayer mprisPlayer: ProxyMprisPlayer { - // Mpris2 Player Interface - canControl: true - canGoNext: radio.antennaConnected - canGoPrevious: radio.antennaConnected - canPause: radio.antennaConnected - canPlay: radio.antennaConnected - canSeek: false - loopStatus: Mpris.LoopNone - - metaData.contributingArtist: formatter.formatMegahertz(radio.frequency / 1000000) + " MHz" - metaData.title: radio.radioData.stationName.trim() - - playbackStatus: radio.active ? Mpris.Playing : Mpris.Stopped - shuffle: false - volume: 1 - - onPauseRequested: radio.stop() - onPlayRequested: radio.start() - onPlayPauseRequested: radio.togglePlayPause() - onStopRequested: radio.stop() - onNextRequested: radio.scanUp() - onPreviousRequested: radio.scanDown() - } - - onStatusChanged: { - // we don't want two panels - if (status == PageStatus.Activating || status == PageStatus.Active) { - dockedPanel().hide(true) - } - } - - ConfigurationValue { - id: lastFrequency - - key: "/apps/jolla-mediaplayer/radio_last_frequency" - defaultValue: 0 - } - - Radio { - id: radio - - // TODO: overriding Radio property. Can be removed when plugin detects antenna state. - // This will ask antenna during phone call, but assume no one is around to see it. - property bool antennaConnected: audioRoute.allowed - property var _stationsFound: [] - property bool _searchingAll - property bool active: state === Radio.ActiveState - - band: Radio.FM - onFrequencyChanged: channelList.update() - onStationFound: { - _stationsFound.push(frequency) - } - - onSearchingChanged: { - if (!_searchingAll) { - return - } - - if (searching) { - _stationsFound = [] - availableStations = _stationsFound - } else { - // Iris backend returns everything when search is finished. not bother to do incremental additions - _stationsFound.sort(function(first, second) { return first - second } ) - availableStations = _stationsFound - _searchingAll = false - } - } - - onStateChanged: { - if (state === Radio.ActiveState) { - startPlay() - } - } - - Component.onCompleted: { - if (lastFrequency.value > 0) { - radio.frequency = lastFrequency.value - } - } - - function searchAll() { - _searchingAll = true - searchAllStations(Radio.SearchFast) - } - - function cancelSearchAll() { - _searchingAll = false - cancelScan() - } - - function startPlay() { - if (audioRoute.allowed) { - if (radio.state === Radio.StoppedState) { - radio.start() - } - - if (!audioResource.acquired) { - audioResource.acquire() - } - - if (audioResource.acquired && radio.state === Radio.ActiveState) { - audioRoute.enable() - } - } - } - - function stopPlay() { - radio.stop() - audioRoute.disable() - if (audioResource.acquired) - audioResource.release() - } - - function togglePlayPause() { - if (radio.active) { - radio.stopPlay() - } else { - radio.startPlay() - } - } - } - - FrequencyFormatter { - id: formatter - } - - AudioResource { - id: audioResource - - onAcquiredChanged: { - if (acquired && audioRoute.allowed) { - radio.startPlay() - } else { - radio.stopPlay() - } - } - } - - AudioRoute { - id: audioRoute - - onAllowedChanged: { - if (allowed) { - radio.startPlay() - } else { - radio.stopPlay() - } - } - } - - SilicaFlickable { - anchors.fill: parent - contentHeight: parent.height - - SilicaListView { - id: channelList - - model: radio.antennaConnected ? bookmarks : 0 - width: parent.width - height: parent.height - panelContent.height - clip: true - header: PageHeader { - //% "FM Radio" - title: qsTrId("mediaplayer-radio-he-fm_radio") - } - - currentIndex: -1 - onModelChanged: update() - - function update() { - if (radio.antennaConnected) { - currentIndex = bookmarks.findByFrequency(radio.frequency) - lastFrequency.value = radio.frequency - } - } - - delegate: ListItem { - id: listItem - - menu: contextMenu - - onClicked: { - radio.frequency = model.frequency - radio.startPlay() - } - - function edit() { - var obj = pageStack.animatorPush(renamePage, { name: model.name }) - obj.pageCompleted.connect(function(dialog) { - dialog.accepted.connect(function() { - bookmarks.modifyName(model.index, dialog.name) - }) - }) - } - - function remove() { - bookmarks.remove(model.index) - channelList.update() - } - - Label { - id: frequencyText - - width: Theme.itemSizeExtraLarge - anchors.verticalCenter: parent.verticalCenter - horizontalAlignment: Text.AlignRight - font.pixelSize: Theme.fontSizeLarge - color: (listItem.highlighted || listItem.ListView.isCurrentItem) ? Theme.highlightColor - : Theme.secondaryColor - text: formatter.formatMegahertz(model.frequency / 1000000) - } - Label { - anchors.left: frequencyText.right - anchors.right: parent.right - anchors.leftMargin: Theme.paddingMedium - anchors.baseline: frequencyText.baseline - font.pixelSize: Theme.fontSizeMedium - elide: Text.ElideRight - color: (listItem.highlighted || listItem.ListView.isCurrentItem) ? Theme.highlightColor - : Theme.primaryColor - text: model.name - } - - Component { - id: contextMenu - - ContextMenu { - MenuItem { - //% "Rename" - text: qsTrId("jolla-mediaplayer-radio-rename") - onClicked: listItem.edit() - } - MenuItem { - //% "Delete" - text: qsTrId("jolla-mediaplayer-radio-delete") - onClicked: listItem.remove() - } - } - } - } - - InfoLabel { - visible: !radio.antennaConnected - anchors.verticalCenter: parent.verticalCenter - //: Placeholder text on radio main page - //% "Plug in your earphones. They are used as radio antenna" - text: qsTrId("jolla-mediaplayer-radio-attach_earphones_hint") - } - } - - MediaPlayerPanelBackground { - width: parent.width - height: panelContent.height - anchors.top: channelList.bottom - - BusyIndicator { - size: BusyIndicatorSize.Small - anchors.horizontalCenter: parent.horizontalCenter - y: stationText.y - running: radio.searching - } - - Column { - id: panelContent - - width: parent.width - opacity: enabled ? 1.0 : 0.6 - enabled: radio.antennaConnected - - Item { - width: 1 - height: root.isLandscape ? Theme.paddingSmall : Theme.paddingMedium - } - - Item { - width: parent.width - height: tuner.height - - IconButton { - property bool bookmarked: channelList.currentIndex >= 0 - - visible: !tuner.adjusting - width: parent.width / 3 - anchors.verticalCenter: parent.verticalCenter - icon.source: bookmarked ? "image://theme/icon-m-favorite-selected" : "image://theme/icon-m-favorite" - onClicked: { - if (bookmarked) { - bookmarks.remove(channelList.currentIndex) - } else { - bookmarks.addStation(radio.radioData.stationName.trim(), - radio.radioData.stationId.trim(), - radio.frequency) - } - channelList.update() - } - } - - IconButton { - visible: tuner.adjusting - width: parent.width / 3 - anchors.verticalCenter: parent.verticalCenter - icon.source: "image://theme/icon-m-left" - onClicked: { - radio.tuneDown() - adjustAutoStop.restart() - } - } - - Text { - id: tuner - - property bool adjusting - - text: formatter.formatMegahertz(radio.frequency / 1000000) - font.pixelSize: Theme.fontSizeHuge - color: tunerMouseArea.pressed ? Theme.highlightColor : Theme.primaryColor - anchors.horizontalCenter: parent.horizontalCenter - - Timer { - id: adjustAutoStop - interval: 5000 - onTriggered: tuner.adjusting = false - } - - MouseArea { - id: tunerMouseArea - anchors.fill: parent - onClicked: { - tuner.adjusting = !tuner.adjusting - if (tuner.adjusting) { - adjustAutoStop.restart() - } - } - } - } - - Text { - visible: !tuner.adjusting - anchors.left: tuner.right - anchors.leftMargin: Theme.paddingSmall - anchors.baseline: tuner.baseline - color: Theme.primaryColor - text: "MHz" - } - - IconButton { - visible: !tuner.adjusting - width: parent.width / 3 - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - icon.source: audioRoute.routeToSpeaker ? "image://theme/icon-m-speaker-on" - : "image://theme/icon-m-speaker" - onClicked: audioRoute.routeToSpeaker = !audioRoute.routeToSpeaker - } - - IconButton { - visible: tuner.adjusting - width: parent.width / 3 - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - icon.source: "image://theme/icon-m-right" - onClicked: { - radio.tuneUp() - adjustAutoStop.restart() - } - } - } - Label { - id: stationText - - property string trimmedText: radio.radioData.stationName.trim() - text: trimmedText != "" ? trimmedText : " " - width: Math.min(parent.width - Theme.paddingMedium, implicitWidth) - x: Math.max((parent.width - width) / 2, Theme.paddingMedium) - truncationMode: TruncationMode.Fade - font.pixelSize: Theme.fontSizeSmall - color: Theme.primaryColor - } - Label { - id: radioText - - property string trimmedText: radio.radioData.radioText.trim() - text: trimmedText != "" ? trimmedText : " " - width: Math.min(parent.width - Theme.paddingMedium, implicitWidth) - x: Math.max((parent.width - width) / 2, Theme.paddingMedium) - truncationMode: TruncationMode.Fade - font.pixelSize: Theme.fontSizeExtraSmall - color: Theme.secondaryColor - } - - Item { - width: 1 - height: Theme.paddingLarge - } - - Row { - id: navigation - width: parent.width - - IconButton { - id: gotoPrevious - width: parent.width / 3 - icon.source: "image://theme/icon-m-previous" - anchors.verticalCenter: parent.verticalCenter - onPressAndHold: radio.scanDown() - onClicked: { - radio.scanDown() - } - } - - IconButton { - id: playPause - - width: parent.width / 3 - icon.source: radio.active ? "image://theme/icon-m-pause" - : "image://theme/icon-m-play" - onClicked: radio.togglePlayPause() - } - - IconButton { - id: gotoNext - width: parent.width / 3 - icon.source: "image://theme/icon-m-next" - anchors.verticalCenter: parent.verticalCenter - onPressAndHold: radio.scanUp() - onClicked: { - radio.scanUp() - } - } - } - Item { - width: 1 - height: (root.isLandscape ? 1 : 2) * Theme.paddingLarge - } - } - } - - PushUpMenu { - visible: radio.antennaConnected - - BackgroundItem { - id: channelButton - - anchors.horizontalCenter: parent.horizontalCenter - height: icon.height + iconText.height + 2*Theme.paddingSmall - width: Math.max(Theme.itemSizeHuge, iconText.width + 2*Theme.paddingSmall) - onClicked: pageStack.animatorPush(Qt.resolvedUrl("ChannelSearchPage.qml"), - { radio: radio, - availableStations: Qt.binding(function() { return availableStations } ), - bookmarks: bookmarks - }) - - Image { - id: icon - - y: Theme.paddingSmall - anchors.horizontalCenter: parent.horizontalCenter - source: "image://theme/icon-m-media-radio" + (channelButton.highlighted ? ("?" + Theme.highlightColor) - : "") - } - Text { - id: iconText - - anchors.top: icon.bottom - anchors.horizontalCenter: parent.horizontalCenter - color: channelButton.highlighted ? Theme.highlightColor : Theme.primaryColor - //% "Available channels" - text: qsTrId("jolla-mediaplayer-radio-available_channels") - } - } - } - } - - Component { - id: renamePage - - Dialog { - id: dialog - property alias name: nameEditor.text - - Column { - width: parent.width - - DialogHeader { - } - TextField { - id: nameEditor - - width: parent.width - focus: true - //: Channel name editor placeholder text - //% "Channel name" - placeholderText: qsTrId("jolla-mediaplayer-radio-channel_name") - label: placeholderText - EnterKey.iconSource: "image://theme/icon-m-enter-accept" - EnterKey.onClicked: dialog.accept() - } - } - } - } -} diff --git a/usr/share/jolla-messages/common/CommHistoryService.qml b/usr/share/jolla-messages/common/CommHistoryService.qml index 5e0226ad..c12b218c 100644 --- a/usr/share/jolla-messages/common/CommHistoryService.qml +++ b/usr/share/jolla-messages/common/CommHistoryService.qml @@ -1,5 +1,5 @@ import QtQuick 2.0 -import org.nemomobile.dbus 2.0 +import Nemo.DBus 2.0 DBusInterface { id: service diff --git a/usr/share/jolla-messages/messages.qml b/usr/share/jolla-messages/messages.qml index 809dfef4..c0d9c710 100644 --- a/usr/share/jolla-messages/messages.qml +++ b/usr/share/jolla-messages/messages.qml @@ -219,6 +219,13 @@ ApplicationWindow { } } + function sendMessage(localUid, remoteUid, message) { + groupManager.createOutgoingMessageEvent(-1 /*groupId*/, localUid, remoteUid, message, function(eventId) { + var channel = channelManager.getConversation(localUid, remoteUid) + channel.sendMessage(message, eventId) + }) + } + function loadAndShowSMSConversation(remoteUids, body) { loadAndShowConversation(MessageUtils.telepathyAccounts.ringAccountPath, remoteUids, body, true) } diff --git a/usr/share/jolla-messages/pages/ImageView.qml b/usr/share/jolla-messages/pages/ImageView.qml index 82dd571f..79de52c0 100644 --- a/usr/share/jolla-messages/pages/ImageView.qml +++ b/usr/share/jolla-messages/pages/ImageView.qml @@ -43,7 +43,7 @@ FullscreenContentPage { anchors.fill: parent additionalActions: Component { IconButton { - icon.source: "image://theme/icon-m-download" + icon.source: "image://theme/icon-m-cloud-download" onClicked: { root.copy() pageStack.pop() diff --git a/usr/share/jolla-messages/pages/MainPage.qml b/usr/share/jolla-messages/pages/MainPage.qml index 2459f288..a5852902 100644 --- a/usr/share/jolla-messages/pages/MainPage.qml +++ b/usr/share/jolla-messages/pages/MainPage.qml @@ -8,8 +8,8 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 import Sailfish.Messages 1.0 -import org.nemomobile.notifications 1.0 -import org.nemomobile.time 1.0 +import Nemo.Notifications 1.0 +import Nemo.Time 1.0 import "groups" diff --git a/usr/share/jolla-messages/pages/MmsShare.qml b/usr/share/jolla-messages/pages/MmsShare.qml index b3eea489..b4fd39a8 100644 --- a/usr/share/jolla-messages/pages/MmsShare.qml +++ b/usr/share/jolla-messages/pages/MmsShare.qml @@ -12,10 +12,10 @@ import Sailfish.Messages 1.0 import Sailfish.Telephony 1.0 import Sailfish.Share 1.0 import org.nemomobile.contacts 1.0 -import org.nemomobile.thumbnailer 1.0 +import Nemo.Thumbnailer 1.0 import org.nemomobile.ofono 1.0 import org.nemomobile.commhistory 1.0 -import MeeGo.QOfono 0.2 +import QOfono 0.2 MessageComposerPage { id: newMessagePage diff --git a/usr/share/jolla-messages/pages/common/CommHistoryService.qml b/usr/share/jolla-messages/pages/common/CommHistoryService.qml index 5e0226ad..c12b218c 100644 --- a/usr/share/jolla-messages/pages/common/CommHistoryService.qml +++ b/usr/share/jolla-messages/pages/common/CommHistoryService.qml @@ -1,5 +1,5 @@ import QtQuick 2.0 -import org.nemomobile.dbus 2.0 +import Nemo.DBus 2.0 DBusInterface { id: service diff --git a/usr/share/jolla-messages/pages/conversation/AttachmentDelegate.qml b/usr/share/jolla-messages/pages/conversation/AttachmentDelegate.qml index dc882dc4..ec92b8a2 100644 --- a/usr/share/jolla-messages/pages/conversation/AttachmentDelegate.qml +++ b/usr/share/jolla-messages/pages/conversation/AttachmentDelegate.qml @@ -1,6 +1,6 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 -import org.nemomobile.thumbnailer 1.0 +import Nemo.Thumbnailer 1.0 Thumbnail { id: attachment diff --git a/usr/share/jolla-messages/pages/conversation/MessagesView.qml b/usr/share/jolla-messages/pages/conversation/MessagesView.qml index fda01bc2..1f73bbc1 100644 --- a/usr/share/jolla-messages/pages/conversation/MessagesView.qml +++ b/usr/share/jolla-messages/pages/conversation/MessagesView.qml @@ -4,7 +4,7 @@ import Sailfish.Silica.private 1.0 import Sailfish.Contacts 1.0 import Sailfish.Messages 1.0 import org.nemomobile.commhistory 1.0 -import org.nemomobile.time 1.0 +import Nemo.Time 1.0 SilicaListView { id: messagesView @@ -14,6 +14,18 @@ SilicaListView { currentIndex: -1 _quickScrollItem.directionsEnabled: QuickScrollDirection.Down + function formatDate(date) { + var today = new Date + if (date.getDate() == today.getDate() + && date.getMonth() == today.getMonth() + && date.getFullYear() == today.getFullYear()) { + //% "Today" + return qsTrId("messages-la-today") + } else { + return Format.formatDate(date, Formatter.TimepointSectionRelative) + } + } + BackgroundRectangle { width: parent.width height: stickyHeader.height @@ -22,7 +34,7 @@ SilicaListView { SectionHeader { id: stickyHeader property var date: undefined - text: date !== undefined && Qt.application.active ? Format.formatDate(date, Formatter.TimepointSectionRelative) : "" + text: date !== undefined && Qt.application.active ? messagesView.formatDate(date) : "" horizontalAlignment: Text.AlignHCenter color: Theme.secondaryColor } @@ -135,13 +147,7 @@ SilicaListView { id: dateSection horizontalAlignment: Text.AlignHCenter color: Theme.secondaryColor - text: { - if (modelData && Qt.application.active) { // force refresh - return Format.formatDate(modelData.startTime, Formatter.TimepointSectionRelative) - } else { - return "" - } - } + text: (modelData && Qt.application.active) ? messagesView.formatDate(modelData.startTime) : "" } } } diff --git a/usr/share/jolla-messages/pages/conversation/SMSMessageDelegate.qml b/usr/share/jolla-messages/pages/conversation/SMSMessageDelegate.qml index 755f934a..49702bf4 100644 --- a/usr/share/jolla-messages/pages/conversation/SMSMessageDelegate.qml +++ b/usr/share/jolla-messages/pages/conversation/SMSMessageDelegate.qml @@ -87,9 +87,9 @@ ListItem { dateString = Format.formatDate(modelData.startTime, Formatter.WeekdayNameStandalone) timeString = Format.formatDate(date, Formatter.TimeValue) } else if (shorten) { - timeString = Format.formatDate(date, Formatter.DurationElapsedShort) + timeString = Format.formatDate(date, Formatter.TimeElapsedShort) } else { - timeString = Format.formatDate(date, Formatter.DurationElapsed) + timeString = Format.formatDate(date, Formatter.TimeElapsed) } if (dateString) { @@ -143,7 +143,7 @@ ListItem { bottomMargin: (groupFirst ? Theme.paddingSmall : 0) } - radius: Theme.paddingLarge + radius: Math.min(Theme.paddingLarge, height / 2) roundedCorners: { // Note: MessagesView has a BottomToTop layout direction, so groupFirst is the bottom-most var result = Corners.None diff --git a/usr/share/jolla-messages/pages/groups/GroupDelegate.qml b/usr/share/jolla-messages/pages/groups/GroupDelegate.qml index e7d084d6..01184aae 100644 --- a/usr/share/jolla-messages/pages/groups/GroupDelegate.qml +++ b/usr/share/jolla-messages/pages/groups/GroupDelegate.qml @@ -113,7 +113,7 @@ ListItem { var daysDiff = (today - messageDate) / (24 * 60 * 60 * 1000) if (daysDiff === 0) { - label = Format.formatDate(model.startTime, Formatter.DurationElapsed) + label = Format.formatDate(model.startTime, Formatter.TimeElapsed) } else { label = Format.formatDate(model.startTime, Formatter.TimeValue) } diff --git a/usr/share/jolla-notes/cover/NotesCover.qml b/usr/share/jolla-notes/cover/NotesCover.qml new file mode 100644 index 00000000..36ac78aa --- /dev/null +++ b/usr/share/jolla-notes/cover/NotesCover.qml @@ -0,0 +1,58 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 + +CoverBackground { + property string testName: "coverpage" + property int topMargin: Theme.itemSizeSmall + property int itemCount: Math.round((parent.height-Theme.itemSizeSmall)/label.lineHeight) + + Repeater { + model: itemCount + delegate: Rectangle { + y: topMargin + index * label.lineHeight + width: parent.width + // gives 1.5 on phone, which looks OK on the phone small cover. + height: Theme.paddingSmall/4 + color: Theme.primaryColor + opacity: Theme.opacityLow + } + } + + Label { + id: label + property var noteText: { + if (pageStack.depth > 1 && currentNotePage) { + return currentNotePage.text.trim() + } else if (notesModel.count > 0 && notesModel.moveCount) { + return notesModel.get(0).text.trim() + } + + return undefined + } + text: noteText !== undefined + ? noteText.replace(/\n/g, " ") + // From notes.cpp + : qsTrId("notes-de-name") + x: Theme.paddingSmall/2 + y: topMargin - baselineOffset - Theme.paddingSmall + (noteText !== undefined ? 0 : lineHeight) + opacity: Theme.opacityHigh + font.pixelSize: Theme.fontSizeExtraLarge + font.italic: true + width: noteText !== undefined ? parent.width + Theme.itemSizeLarge : parent.width - Theme.paddingSmall + horizontalAlignment: noteText !== undefined || implicitWidth > width - Theme.paddingSmall ? Text.AlignLeft : Text.AlignHCenter + lineHeightMode: Text.FixedHeight + lineHeight: Math.floor(Theme.fontSizeExtraLarge * 1.35) + wrapMode: noteText !== undefined ? Text.Wrap : Text.NoWrap + maximumLineCount: itemCount + } + + CoverActionList { + CoverAction { + iconSource: "image://theme/icon-cover-new" + onTriggered: { + openNewNote(PageStackAction.Immediate) + activate() + } + } + } +} diff --git a/usr/share/jolla-notes/notes.qml b/usr/share/jolla-notes/notes.qml new file mode 100644 index 00000000..6acb5cb7 --- /dev/null +++ b/usr/share/jolla-notes/notes.qml @@ -0,0 +1,116 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Nemo.DBus 2.0 +import "pages" + +ApplicationWindow +{ + id: app + + property Item currentNotePage + + initialPage: Component { + OverviewPage { + id: overviewpage + property Item currentPage: pageStack.currentPage + onCurrentPageChanged: { + if (currentPage == overviewpage) { + currentNotePage = null + } else if (currentPage.hasOwnProperty("__jollanotes_notepage")) { + currentNotePage = currentPage + } + } + } + } + cover: Qt.resolvedUrl("cover/NotesCover.qml") + allowedOrientations: defaultAllowedOrientations + _defaultPageOrientations: Orientation.All + _defaultLabelFormat: Text.PlainText + + // exposed as a property so that the tests can access it + property NotesModel notesModel: NotesModel { id: notesModel } + + function openNewNote(operationType) { + pageStack.pop(null, PageStackAction.Immediate) + pageStack.animatorPush(notePage, {potentialPage: 1, editMode: true}, operationType) + } + + Component { + id: notePage + NotePage { } + } + + DBusAdaptor { + service: "com.jolla.notes" + path: "/" + iface: "com.jolla.notes" + + function newNote() { + if (pageStack.currentPage.__jollanotes_notepage === undefined || pageStack.currentPage.currentIndex >= 0) { + // don't open a new note if already showing a new unedited note + openNewNote(PageStackAction.Immediate) + } + app.activate() + } + + function openUrl(urls) { + if (urls.length === 0) { + app.activate() + } else { + importNoteFile(urls) + } + } + + function importNoteFile(pathList) { + // If the user has an empty note open (or we automatically pushed newNote + // page due to having no notes) then we need to pop that page. + if (pageStack.currentPage.__jollanotes_notepage !== undefined) { + pageStack.pop(null, PageStackAction.Immediate) + } + + // For compatibility reasons this signal sometimes receives an array of strings + var filePath + if (typeof pathList === 'string') { + filePath = pathList + } else if (typeof pathList === 'object' && pathList.length !== undefined && pathList.length > 0) { + filePath = pathList[0] + if (pathList.length > 1) { + console.warn('jolla-notes: Importing only first path from:', pathList) + } + } + if (filePath && (String(filePath) != '')) { + console.log('jolla-notes: Importing note file:', filePath) + var plaintextNotes = vnoteConverter.importFromFile(filePath) + if (plaintextNotes.length === 0) { + var filename = filePath.substring(filePath.lastIndexOf("/") + 1) + //% "Unable to import: %1" + Notices.show(qsTrId("notes-la-unable_to_open").arg(filename)) + } + + for (var index = 0; index < plaintextNotes.length; ++index) { + // insert the note into the database + notesModel.newNote(index + 1, plaintextNotes[index], notesModel.nextColor()) + } + if (plaintextNotes.length === 1 && pageStack.depth === 1) { + pageStack.push(notePage, {currentIndex: -1}, PageStackAction.Immediate) + } else for (index = 0; index < plaintextNotes.length; ++index) { + if (pageStack.depth === 1) { + // the current page is the overview page. indicate to the user which notes were imported, + // by flashing the delegates of the imported notes in the gridview. + pageStack.currentPage.flashGridDelegate(index) + } else { + // a note is currently open. Queue up the indication to the user + // so that it gets displayed when they next return to the gridview. + var overviewPage = pageStack.previousPage(app.currentNotePage) + overviewPage._flashDelegateIndexes[overviewPage._flashDelegateIndexes.length] = index + } + } + app.activate() + } + } + + function activateWindow(arg) { + app.activate() + } + } +} diff --git a/usr/share/jolla-notes/pages/ColorItem.qml b/usr/share/jolla-notes/pages/ColorItem.qml new file mode 100644 index 00000000..13654950 --- /dev/null +++ b/usr/share/jolla-notes/pages/ColorItem.qml @@ -0,0 +1,28 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 + +Rectangle { + id: coloritem + + signal clicked + property alias pageNumber: label.text + + height: Theme.itemSizeExtraSmall + width: Math.max(Theme.itemSizeExtraSmall, label.width + 2*Theme.paddingMedium) + radius: Theme.paddingSmall/2 + anchors { + right: parent.right + rightMargin: Theme.horizontalPageMargin + verticalCenter: parent.verticalCenter + topMargin: Theme.paddingLarge + } + Label { + id: label + font.pixelSize: Theme.fontSizeLarge + anchors.centerIn: parent + } + MouseArea { + anchors { fill: parent; margins: -Theme.paddingMedium } + onClicked: parent.clicked() + } +} diff --git a/usr/share/jolla-notes/pages/NoteItem.qml b/usr/share/jolla-notes/pages/NoteItem.qml new file mode 100644 index 00000000..f3f23a1f --- /dev/null +++ b/usr/share/jolla-notes/pages/NoteItem.qml @@ -0,0 +1,91 @@ +import QtQuick 2.5 +import Sailfish.Silica 1.0 + +GridItem { + id: noteitem + + property int pageNumber + property color color + property alias text: summary.text + + // Create a tint with 10% of the primaryColor in the lower left, + // down to 0% in the upper right. + // Is there any way to use OpacityRampEffect instead of Gradient here? + Item { + // The rectangle inside is rotated to rotate the gradient, + // but then it needs to be clipped back to an upright square. + // This container item does the clipping so that the NoteItem itself + // doesn't have to clip (which would interfere with context menus) + anchors.fill: parent + clip: true + Rectangle { + rotation: 45 // diagonal gradient + // Use square root of 2, rounded up a little bit, to make the + // rotated square cover all of the parent square + width: parent.width * 1.412136 + height: parent.height * 1.412136 + x: parent.width - width + gradient: Gradient { + GradientStop { position: 0.0; color: Theme.rgba(Theme.primaryColor, 0) } + GradientStop { position: 1.0; color: Theme.rgba(Theme.primaryColor, Theme.opacityFaint) } + } + } + } + + Item { + anchors { fill: parent; margins: Theme.paddingLarge } + Text { + id: summary + anchors { + top: parent.top + topMargin: - (font.pixelSize / 4) + left: parent.left + right: parent.right + } + height: parent.height + textFormat: Text.StyledText + font { family: Theme.fontFamily; pixelSize: Theme.fontSizeSmall } + color: highlighted ? Theme.highlightColor : Theme.primaryColor + wrapMode: Text.Wrap + maximumLineCount: Math.floor((height - Theme.paddingLarge) / fontMetrics.height) + elide: Text.ElideRight + } + FontMetrics { + id: fontMetrics + font: summary.font + } + + OpacityRampEffect { + sourceItem: summary + slope: 0.6 + offset: 0 + direction: OpacityRamp.TopToBottom + } + + Rectangle { + id: colortag + property string testName: "colortag" + + anchors.bottom: parent.bottom + anchors.left: parent.left + width: Theme.itemSizeExtraSmall + height: width/8 + radius: Math.round(Theme.paddingSmall/3) + color: noteitem.color + } + } + + Text { + id: pagenumber + + anchors.baseline: parent.bottom + anchors.baselineOffset: -Theme.paddingMedium + anchors.right: parent.right + anchors.rightMargin: Theme.paddingMedium + opacity: Theme.opacityLow + color: highlighted ? Theme.highlightColor : Theme.primaryColor + font { family: Theme.fontFamily; pixelSize: Theme.fontSizeLarge } + horizontalAlignment: Text.AlignRight + text: noteitem.pageNumber + } +} diff --git a/usr/share/jolla-notes/pages/NotePage.qml b/usr/share/jolla-notes/pages/NotePage.qml new file mode 100644 index 00000000..46339307 --- /dev/null +++ b/usr/share/jolla-notes/pages/NotePage.qml @@ -0,0 +1,337 @@ +/* + * Copyright (C) 2015 - 2021 Jolla Ltd. + * Copyright (C) 2021 Open Mobile Platform LLC. + * + * License: Proprietary + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Sailfish.Share 1.0 +import Nemo.Configuration 1.0 + +Page { + id: page + + // currentIndex is for allocated notes. + // potentialPage is for empty notes that haven't been added to the db yet. + property int currentIndex: -1 + property int potentialPage + property alias editMode: textArea.focus + property alias text: textArea.text + property alias color: noteview.color + property alias pageNumber: noteview.pageNumber + property bool loaded // only load from notesModel[currentIndex] once + + property bool __jollanotes_notepage + + highContrast: true + + // TODO: should some kind of IndexConnection go into the silica components? + Connections { + target: notesModel + + onRowsRemoved: { + console.log("Notes removed: " + first + ".." + last) + if (currentIndex >= first) { + if (currentIndex > last) { + currentIndex -= (last - first + 1) + } else { + // current note was deleted; turn it into a potential note + potentialPage = pageNumber + } + } + } + + onRowsInserted: { + console.log("Notes inserted: " + first + ".." + last) + if (currentIndex >= first) + currentIndex += (last - first + 1) + } + + onRowsMoved: { + console.log("Notes moved: " + start + ".." + end + " -> " + row) + // start and end are indexes from before the move, + // "row" is start's new index after the move + var numMoved = end - start + 1 + if (currentIndex >= start && currentIndex <= end) { + // current note was among those moved + currentIndex += start - row + } else if (currentIndex > end && currentIndex < row + numMoved) { + // moved notes jumped over current note + currentIndex -= numMoved + } else if (currentIndex < start && currentIndex >= row) { + // moved notes jumped before current note + currentIndex += numMoved + } + } + onNewNoteInserted: currentIndex = 0 + } + + onCurrentIndexChanged: { + if (!loaded && currentIndex >= 0 && currentIndex < notesModel.count) { + potentialPage = 0 + var item = notesModel.get(currentIndex) + noteview.savedText = item.text + noteview.text = item.text + noteview.color = item.color + noteview.pageNumber = item.pagenr + loaded = true + } + } + + onStatusChanged: { + if (status == PageStatus.Deactivating) { + if (currentIndex >= 0 && noteview.text.trim() == '') { + notesModel.deleteNote(currentIndex) + currentIndex = -1 + } else { + saveNote() + } + } + } + + function saveNote() { + var text = textArea.text + if (text != noteview.savedText) { + noteview.savedText = text + if (potentialPage) { + if (text.trim() != '') { + notesModel.newNote(potentialPage, text, noteview.color) + return true + } + } else { + notesModel.updateNote(currentIndex, text) + return true + } + } + return false + } + + onPotentialPageChanged: { + if (potentialPage) { + currentIndex = -1 + noteview.savedText = '' + noteview.text = '' + noteview.color = notesModel.nextColor() + noteview.pageNumber = potentialPage + } + } + + function openColorPicker() { + var obj = pageStack.animatorPush("Sailfish.Silica.ColorPickerPage", + {"colors": notesModel.availableColors}) + obj.pageCompleted.connect(function(page) { + page.colorClicked.connect(function(color) { + noteview.color = color + if (currentIndex >= 0) { + notesModel.updateColor(currentIndex, color) + } + pageStack.pop() + }) + }) + } + + function noteFileName(noteText) { + // Return a name for this vnote that can be used as a filename + + // Remove any whitespace + var noWhitespace = noteText.replace(/\s/g, '') + + // shorten + var shortened = noWhitespace.slice(0, Math.min(8, noWhitespace.length)) + + // Convert to 7-bit ASCII + var sevenBit = Format.formatText(shortened, Formatter.Ascii7Bit) + if (sevenBit.length < shortened.length) { + // This note's name is not representable in ASCII + //: Placeholder name for note filename + //% "note" + sevenBit = qsTrId("notes-ph-default-note-name") + } + + // Remove any characters that are not part of the portable filename character set + return Format.formatText(sevenBit, Formatter.PortableFilename) + } + + SilicaFlickable { + id: noteview + + property color color: "white" + property alias text: textArea.text + property int pageNumber + property string savedText + + anchors.fill: parent + + // The PullDownMenu doesn't work if contentHeight is left implicit. + // It also doesn't work if contentHeight ends up equal to the + // page height, so add some padding. + contentHeight: column.y + column.height + + PullDownMenu { + id: pulley + + MenuItem { + //% "Change color" + text: qsTrId("notes-me-note-color") + onClicked: openColorPicker() + } + MenuItem { + //: Delete this note from note page + //% "Delete" + text: qsTrId("notes-me-delete-note") + onClicked: deleteNoteAnimation.restart() + SequentialAnimation { + id: deleteNoteAnimation + NumberAnimation { + target: noteview + property: "opacity" + duration: 200 + easing.type: Easing.InOutQuad + to: 0.0 + } + ScriptAction { + script: { + // If the note text is empty then the note + // will be deleted by onStatusChanged, and + // there should not be a remorse timer etc. + if (page.currentIndex >= 0 + && noteview.text.trim() != '') { + var overview = pageStack.previousPage() + overview.showDeleteNote(page.currentIndex) + } + pageStack.pop(null, PageStackAction.Immediate) + noteview.opacity = 1.0 + } + } + } + } + MenuItem { + //: This menu option can be used to share the note via Bluetooth + //% "Share" + text: qsTrId("notes-me-share-note") + enabled: noteview.text.trim() != '' + onClicked: { + var fileName = page.noteFileName(noteview.text) + (transferAsVNoteConfig.value == true ? ".vnt" : ".txt") + var mimeType = transferAsVNoteConfig.value == true ? "text/x-vnote" : "text/plain" + // vnoteConverter is a global installed by notes.cpp + var noteText = transferAsVNoteConfig.value == true ? vnoteConverter.vNote(textArea.text) : textArea.text + var content = { + "name": fileName, + "data": noteText, + "type": mimeType + } + + if (mimeType == "text/plain") { + // also some non-standard fields for Twitter/Facebook status sharing: + content["status"] = noteText + content["linkTitle"] = fileName + } + shareAction.resources = [content] + shareAction.mimeType = mimeType + shareAction.trigger() + } + ShareAction { + id: shareAction + + //: Page header for share method selection + //% "Share note" + title: qsTrId("notes-he-share-note") + } + } + MenuItem { + id: saveItem + enabled: !saving + + property bool saving + + function replace(force) { + if (!newNoteAnimation.running || force) { + app.pageStack.replace(notePage, { + potentialPage: 1, + editMode: true + }, PageStackAction.Immediate) + notesModel.newNoteInserted.disconnect(replace) + saving = false + } + } + + //: Create a new note ready for editing + //% "New note" + text: qsTrId("notes-me-new-note") + + onDelayedClick: { + if (saveNote()) { + saving = true + notesModel.newNoteInserted.connect(replace) + } + newNoteAnimation.restart() + } + + + SequentialAnimation { + id: newNoteAnimation + NumberAnimation { + target: noteview + property: "opacity" + duration: 200 + easing.type: Easing.InOutQuad + to: 0.0 + } + ScriptAction { + script: saveItem.replace(true) + } + } + } + } + + Column { + id: column + width: page.width + + Item { + id: headerItem + width: parent.width + height: Theme.itemSizeLarge + + ColorItem { + id: colorItem + color: noteview.color + pageNumber: noteview.pageNumber + onClicked: openColorPicker() + } + } + TextArea { + id: textArea + font { family: Theme.fontFamily; pixelSize: Theme.fontSizeMedium } + width: parent.width + height: Math.max(noteview.height - headerItem.height, implicitHeight) + //: Placeholder text for new notes. At this point there's + //: nothing else on the screen. + //% "Write a note..." + placeholderText: qsTrId("notes-ph-empty-note") + color: Theme.primaryColor + backgroundStyle: TextEditor.NoBackground + + onTextChanged: saveTimer.restart() + Timer { + id: saveTimer + interval: 5000 + onTriggered: page.saveNote() + } + Connections { + target: Qt.application + onActiveChanged: if (!Qt.application.active) page.saveNote() + } + } + } + VerticalScrollDecorator {} + } + + ConfigurationValue { + id: transferAsVNoteConfig + key: "/apps/jolla-notes/settings/transferAsVNote" + defaultValue: false + } +} diff --git a/usr/share/jolla-notes/pages/NotesModel.qml b/usr/share/jolla-notes/pages/NotesModel.qml new file mode 100644 index 00000000..35a42e34 --- /dev/null +++ b/usr/share/jolla-notes/pages/NotesModel.qml @@ -0,0 +1,88 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Nemo.Configuration 1.0 +import "notesdatabase.js" as Database + +ListModel { + id: model + + property string filter + property bool populated + property int moveCount: 1 + readonly property var availableColors: [ + "#cc0000", "#cc7700", "#ccbb00", + "#88cc00", "#00b315", "#00bf9f", + "#005fcc", "#0016de", "#bb00cc"] + property var colorIndexConf: ConfigurationValue { + key: "/apps/jolla-notes/next_color_index" + defaultValue: 0 + } + property var worker: WorkerScript { + source: "notesmodel.js" + onMessage: { + if (messageObject.reply === "insert") { + model.newNoteInserted() + } else if (messageObject.reply == "update") { + populated = true + } + } + } + signal newNoteInserted + + Component.onCompleted: { + refresh() + + if (Database.migrated_color_index !== -1) { + colorIndexConf.value = Database.migrated_color_index + } + } + onFilterChanged: refresh() + + function refresh() { + Database.updateNotes(filter, function (results) { + var msg = {'action': 'update', 'model': model, 'results': results} + worker.sendMessage(msg) + }) + } + + function nextColor() { + var index = colorIndexConf.value + if (index >= availableColors.length) + index = 0 + colorIndexConf.value = index + 1 + return availableColors[index] + } + + function newNote(pagenr, initialtext, color) { + var _color = color + "" // convert to string + Database.newNote(pagenr, _color, initialtext) + var msg = {'action': 'insert', 'model': model, "pagenr": pagenr, "text": initialtext, "color": _color } + worker.sendMessage(msg) + } + + function updateNote(idx, text) { + Database.updateNote(get(idx).pagenr, text) + var msg = {'action': 'textupdate', 'model': model, 'idx': idx, 'text': text} + worker.sendMessage(msg) + } + + function updateColor(idx, color) { + var _color = color + "" // convert to string + Database.updateColor(get(idx).pagenr, _color) + var msg = {'action': 'colorupdate', 'model': model, 'idx': idx, 'color': _color} + worker.sendMessage(msg) + } + + function moveToTop(idx) { + Database.moveToTop(get(idx).pagenr) + var msg = {'action': 'movetotop', 'model': model, 'idx': idx} + worker.sendMessage(msg) + moveCount++ + } + + function deleteNote(idx) { + Database.deleteNote(get(idx).pagenr) + var msg = {'action': 'remove', 'model': model, "idx": idx} + worker.sendMessage(msg) + } +} diff --git a/usr/share/jolla-notes/pages/OverviewPage.qml b/usr/share/jolla-notes/pages/OverviewPage.qml new file mode 100644 index 00000000..b5f3f711 --- /dev/null +++ b/usr/share/jolla-notes/pages/OverviewPage.qml @@ -0,0 +1,189 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 + +Page { + id: overviewpage + + function showDeleteNote(index) { + // This is needed both for UI (the user should see the remorse item) + // and to make sure the delegate exists. + view.positionViewAtIndex(index, GridView.Contain) + // Set currentIndex in order to find the corresponding currentItem. + // Is this really the only way to look up a delegate by index? + view.currentIndex = index + view.currentItem.deleteNote() + } + function flashGridDelegate(index) { + // This is needed both for UI (the user should see the remorse item) + // and to make sure the delegate exists. + view.positionViewAtIndex(index, GridView.Contain) + // Set currentIndex in order to find the corresponding currentItem. + // Is this really the only way to look up a delegate by index? + view.currentIndex = index + view.currentItem.flash() + } + property var _flashDelegateIndexes: [] + + readonly property bool populated: notesModel.populated + onPopulatedChanged: { + if (notesModel.count === 0) { + openNewNote(PageStackAction.Immediate) + } + } + + onStatusChanged: { + if (status === PageStatus.Active) { + if (populated && _flashDelegateIndexes.length) { + // Flash grid delegates of imported notes + for (var i in _flashDelegateIndexes) { + flashGridDelegate(_flashDelegateIndexes[i]) + } + _flashDelegateIndexes = [] + } + if (notesModel.filter.length > 0) { + notesModel.refresh() // refresh search + } + } else if (status === PageStatus.Inactive) { + if (notesModel.filter.length == 0) view.headerItem.active = false + } + } + + SilicaGridView { + id: view + + currentIndex: -1 + anchors.fill: overviewpage + model: notesModel + cellHeight: overviewpage.width / columnCount + cellWidth: cellHeight + // reference column width: 960 / 4 + property int columnCount: Math.floor((isLandscape ? Screen.height : Screen.width) / (Theme.pixelRatio * 240)) + + onMovementStarted: { + focus = false // close the vkb + } + + ViewPlaceholder { + id: placeholder + + // Avoid flickering empty state placeholder when updating search results + function placeholderText() { + //% "Sorry, we couldn't find anything" + return notesModel.filter.length > 0 ? qsTrId("notes-la-could_not_find_anything") + //: Comforting text when overview is empty + //% "Write a note" + : qsTrId("notes-la-overview-placeholder") + } + Component.onCompleted: text = placeholderText() + Binding { + when: placeholder.opacity == 0.0 + target: placeholder + property: "text" + value: placeholder.placeholderText() + } + + enabled: notesModel.populated && notesModel.count === 0 + } + header: SearchField { + width: parent.width + canHide: text.length === 0 + active: false + inputMethodHints: Qt.ImhNone // Enable predictive text + + onHideClicked: { + active = false + } + + onTextChanged: notesModel.filter = text + + EnterKey.iconSource: "image://theme/icon-m-enter-close" + EnterKey.onClicked: focus = false + } + + delegate: NoteItem { + id: noteItem + + // make model.index accessible to other delegates + property int index: model.index + + function deleteNote() { + remorseDelete(function() { + notesModel.deleteNote(index) + }) + } + + function flash() { + flashAnim.running = true + } + + + text: model.text ? Theme.highlightText(model.text.substr(0, Math.min(model.text.length, 300)), notesModel.filter, Theme.highlightColor) : "" + color: model.color + pageNumber: model.pagenr + menu: contextMenuComponent + + onClicked: pageStack.push(notePage, { currentIndex: model.index } ) + + Rectangle { + id: flashRect + anchors.fill: parent + color: noteItem.color + opacity: 0.0 + SequentialAnimation { + id: flashAnim + running: false + PropertyAnimation { target: flashRect; property: "opacity"; to: Theme.opacityLow; duration: 600; easing.type: Easing.InOutQuad } + PropertyAnimation { target: flashRect; property: "opacity"; to: 0.01; duration: 600; easing.type: Easing.InOutQuad } + PropertyAnimation { target: flashRect; property: "opacity"; to: Theme.opacityLow; duration: 600; easing.type: Easing.InOutQuad } + PropertyAnimation { target: flashRect; property: "opacity"; to: 0.00; duration: 600; easing.type: Easing.InOutQuad } + } + } + } + + PullDownMenu { + id: pullDownMenu + + MenuItem { + visible: notesModel.filter.length > 0 || notesModel.count > 0 + //% "Search" + text: qsTrId("notes-me-search") + onClicked: { + view.headerItem.active = true + view.headerItem.forceActiveFocus() + } + } + + MenuItem { + //: Create a new note ready for editing + //% "New note" + text: qsTrId("notes-me-new-note") + onClicked: app.openNewNote(PageStackAction.Animated) + } + } + VerticalScrollDecorator {} + } + + Component { + id: contextMenuComponent + ContextMenu { + id: contextMenu + + MenuItem { + //: Delete this note from overview + //% "Delete" + text: qsTrId("notes-la-delete") + onClicked: contextMenu.parent.deleteNote() + } + + MenuItem { + //: Move this note to be first in the list + //% "Move to top" + text: qsTrId("notes-la-move-to-top") + visible: contextMenu.parent && contextMenu.parent.index > 0 + property int index + onClicked: index = contextMenu.parent.index // parent is null by the time delayedClick() is called + onDelayedClick: notesModel.moveToTop(index) + } + } + } +} diff --git a/usr/share/jolla-notes/pages/notesdatabase.js b/usr/share/jolla-notes/pages/notesdatabase.js new file mode 100644 index 00000000..e498028f --- /dev/null +++ b/usr/share/jolla-notes/pages/notesdatabase.js @@ -0,0 +1,143 @@ +// Copyright (C) 2012-2013 Jolla Ltd. +// Contact: Richard Braakman + +// The page numbers in the db must stay sequential (starting from 1), +// but the page numbers in the model may have gaps if the filter is active. +// The page numbers in the model must still be ascending, though. + +// The details depend on Qt's openDatabaseSync implementation, but +// the data will probably be stored in an sqlite file under +// $HOME/.local/share/jolla-notes/QML/OfflineStorage/Databases/ + +.import QtQuick.LocalStorage 2.0 as Sql + +var migrated_color_index = -1 + +function _rawOpenDb() { + return Sql.LocalStorage.openDatabaseSync('silicanotes', '', 'Notes', 10000) +} + +function upgradeSchema(db) { + // Awkward. db.changeVersion does NOT update db.version, but DOES + // check that db.version is equal to the first parameter. + // So reopen the database after every changeVersion to get the + // updated db.version. + if (db.version == '') { + // Change the version directly to '3', no point creating the + // now obsolete next_color_index table and drop it immediately + // after that. + db.changeVersion('', '3', function (tx) { + tx.executeSql( + 'CREATE TABLE notes (pagenr INTEGER, color TEXT, body TEXT)') + }) + db = _rawOpenDb() + } + if (db.version == '1') { + // Version '1' equals to version '3'. Just change the version number. + // Old migration code to version '2' left in comments for reference. + db.changeVersion('1', '3') + /* + db.changeVersion('1', '2', function (tx) { + tx.executeSql('CREATE TABLE next_color_index (value INTEGER)') + tx.executeSql('INSERT INTO next_color_index VALUES (0)') + }) + */ + db = _rawOpenDb() + } + if (db.version == '2') { + db.changeVersion('2', '3', function (tx) { + // "next_color_index" table may be missing because it was never backed up. + var results = tx.executeSql('SELECT name FROM sqlite_master WHERE type="table" AND name="next_color_index"'); + if (results.rows.length) { + var r = tx.executeSql('SELECT value FROM next_color_index LIMIT 1') + migrated_color_index = parseInt(r.rows.item(0).value, 10) + // next_color_index is stored in dconf from now on. Drop the table. + tx.executeSql('DROP TABLE next_color_index') + } + }) + db = _rawOpenDb() + } +} + +function openDb() { + var db = _rawOpenDb() + if (db.version != '3') + upgradeSchema(db) + return db +} + +var regex = new RegExp(/['\%\\\_]/g) +var escaper = function escaper(char){ + var m = ["'", "%", "_", "\\"] + var r = ["''", "\\%", "\\_", "\\\\"] + return r[m.indexOf(char)] +} + +function updateNotes(filter, callback) { + var db = openDb() + db.readTransaction(function (tx) { + var results + if (filter.length > 0) { + results = tx.executeSql("SELECT pagenr, color, body FROM notes WHERE body LIKE '%" + filter.replace(regex, escaper) + "%' ESCAPE '\\' ORDER BY pagenr") + } else { + results = tx.executeSql("SELECT pagenr, color, body FROM notes ORDER BY pagenr") + } + + var array = [] + for (var i = 0; i < results.rows.length; i++) { + var item = results.rows.item(i) + array[i] = { + "pagenr": item.pagenr, + "text": item.body, + "color": item.color + } + } + + callback(array) + }) +} + +function newNote(pagenr, color, initialtext) { + var db = openDb() + db.transaction(function (tx) { + tx.executeSql('UPDATE notes SET pagenr = pagenr + 1 WHERE pagenr >= ?', + [pagenr]) + tx.executeSql('INSERT INTO notes (pagenr, color, body) VALUES (?, ?, ?)', + [pagenr, color, initialtext]) + }) +} + +function updateNote(pagenr, text) { + var db = openDb() + db.transaction(function (tx) { + tx.executeSql('UPDATE notes SET body = ? WHERE pagenr = ?', + [text, pagenr]) + }) +} + +function updateColor(pagenr, color) { + var db = openDb() + db.transaction(function (tx) { + tx.executeSql('UPDATE notes SET color = ? WHERE pagenr = ?', + [color, pagenr]) + }) +} + +function moveToTop(pagenr) { + var db = openDb() + db.transaction(function (tx) { + // Use modulo-pagenr arithmetic to rotate the page numbers: add 1 to + // all of them except pagenr itself, which goes to 1. + tx.executeSql('UPDATE notes SET pagenr = (pagenr % ?) + 1 WHERE pagenr <= ?', + [pagenr, pagenr]) + }) +} + +function deleteNote(pagenr) { + var db = openDb(); + db.transaction(function (tx) { + tx.executeSql('DELETE FROM notes WHERE pagenr = ?', [pagenr]) + tx.executeSql('UPDATE notes SET pagenr = pagenr - 1 WHERE pagenr > ?', + [pagenr]) + }) +} diff --git a/usr/share/jolla-notes/pages/notesmodel.js b/usr/share/jolla-notes/pages/notesmodel.js new file mode 100644 index 00000000..333d63f8 --- /dev/null +++ b/usr/share/jolla-notes/pages/notesmodel.js @@ -0,0 +1,60 @@ + +WorkerScript.onMessage = function(msg) { + var i + var model = msg.model + + if (msg.action === "insert") { + model.insert(0, { + "pagenr": msg.pagenr, + "text": msg.text, + "color": msg.color + }) + for (i = 1; i < model.count; i++) { + model.setProperty(i, "pagenr", model.get(i).pagenr + 1) + } + + } else if (msg.action === "remove") { + model.remove(msg.idx) + for (i = msg.idx; i < model.count; i++) { + model.setProperty(i, "pagenr", model.get(i).pagenr - 1) + } + + } else if (msg.action === "colorupdate") { + model.setProperty(msg.idx, "color", msg.color) + + } else if (msg.action === "textupdate") { + model.setProperty(msg.idx, "text", msg.text) + + } else if (msg.action === "movetotop") { + model.move(msg.idx, 0, 1) // move 1 item to position 0 + model.setProperty(0, "pagenr", 1) + for (i = 1; i <= msg.idx; i++) { + model.setProperty(i, "pagenr", model.get(i).pagenr + 1) + } + + } else if (msg.action === "update") { + var results = msg.results + if (model.count > results.length) { + model.remove(results.length, model.count - results.length) + } + for (i = 0; i < results.length; i++) { + var result = results[i] + if (i < model.count) { + model.set(i, { + "pagenr": result.pagenr, + "text": result.text, + "color": result.color + }) + } else { + model.append({ + "pagenr": result.pagenr, + "text": result.text, + "color": result.color + }) + } + } + } + + model.sync() + WorkerScript.sendMessage({"reply": msg.action}) +} diff --git a/usr/share/jolla-notes/qmldir b/usr/share/jolla-notes/qmldir new file mode 100644 index 00000000..881ddec0 --- /dev/null +++ b/usr/share/jolla-notes/qmldir @@ -0,0 +1 @@ +Notes 1.0 notes.qml \ No newline at end of file diff --git a/usr/share/jolla-settings/pages/ApplicationsGrid.qml b/usr/share/jolla-settings/pages/ApplicationsGrid.qml index 869c9725..5ef41d6f 100644 --- a/usr/share/jolla-settings/pages/ApplicationsGrid.qml +++ b/usr/share/jolla-settings/pages/ApplicationsGrid.qml @@ -35,6 +35,11 @@ Item { pageStack.animatorPush(page, { "applicationName": name, "applicationIcon": icon, "_desktopFile": filePath }) } + function openAndroidSettings(name, treeItem, icon, appPkg, appVer) { + pageStack.animatorPush("/usr/share/jolla-settings/pages/apk-configuration/apkConfigurationPage.qml", + { "applicationName": name, "applicationIcon": icon, "packageName": appPkg, "packageVersion": appVer }) + } + function openSettings(name, treeItem, icon) { var objdata = treeItem.data() var entryPath = treeItem.location().join("/") @@ -100,7 +105,7 @@ Item { width: gridView.cellWidth height: gridView.cellHeight - enabled: configurable || model.sandboxed + enabled: configurable || model.sandboxed || model.androidApp icon: model.iconId text: model.name @@ -109,6 +114,8 @@ Item { root.openSandboxed(text, model.section, model.iconId, model.filePath) } else if (configurable) { root.openSettings(text, model.section, model.iconId) + } else if (model.androidApp) { + root.openAndroidSettings(text, model.section, model.iconId, model.androidAppPkg, model.androidAppVer) } } } diff --git a/usr/share/jolla-settings/pages/ApplicationsView.qml b/usr/share/jolla-settings/pages/ApplicationsView.qml index e4e9ca3b..e5d37072 100644 --- a/usr/share/jolla-settings/pages/ApplicationsView.qml +++ b/usr/share/jolla-settings/pages/ApplicationsView.qml @@ -1,3 +1,9 @@ +/* + * Copyright (c) 2020 - 2022 Jolla Ltd. + * + * License: Proprietary + */ + import QtQuick 2.0 import Sailfish.Silica 1.0 import Sailfish.Silica.private 1.0 @@ -37,6 +43,7 @@ TabItem { applications: LauncherFolderModel { scope: "partnerspace" categories: "X-SailfishPartnerSpace" + iconDirectories: Theme.launcherIconDirectories } } BackgroundItem { diff --git a/usr/share/jolla-settings/pages/about/AddOnsField.qml b/usr/share/jolla-settings/pages/about/AddOnsField.qml new file mode 100644 index 00000000..9cd06419 --- /dev/null +++ b/usr/share/jolla-settings/pages/about/AddOnsField.qml @@ -0,0 +1,72 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Sailfish.Store 1.0 +import Nemo.DBus 2.0 + +MouseArea { + onClicked: settingsDbus.openAccountsPage() + width: parent.width + height: detail.height + + Component.onCompleted: { + addOnModel.populate() + } + + DetailItem { + id: detail + + //% "Add-Ons" + label: qsTrId("settings_about-la-add_ons") + valueFont.italic: addOnModel.error + + function load() { + var names = [] + if (addOnModel.populated) { + var licenseActiveOnly = true + names = addOnModel.displayNames(licenseActiveOnly) + } + if (addOnModel.error) + value = addOnModel.error + else if (names.length !== 0) + value = names.join(Format.listSeparator) + else + value = "-" + } + } + + BusyIndicator { + anchors { + verticalCenter: parent.verticalCenter + right: parent.right + rightMargin: Theme.horizontalPageMargin + } + size: BusyIndicatorSize.ExtraSmall + running: !addOnModel.populated + } + + Connections { + target: Qt.application + onActiveChanged: { + if (Qt.application.active) + addOnModel.populate() + } + } + + AddOnModel { + id: addOnModel + onPopulatedChanged: detail.load() + onErrorChanged: detail.load() + } + + DBusInterface { + id: settingsDbus + bus: DBus.SessionBus + service: "com.jolla.settings" + path: "/com/jolla/settings/ui" + iface: "com.jolla.settings.ui" + + function openAccountsPage() { + settingsDbus.call("showAccounts", []) + } + } +} diff --git a/usr/share/jolla-settings/pages/about/about.qml b/usr/share/jolla-settings/pages/about/about.qml index b918d290..3042c314 100644 --- a/usr/share/jolla-settings/pages/about/about.qml +++ b/usr/share/jolla-settings/pages/about/about.qml @@ -1,5 +1,5 @@ /* - * Copyright (c) 2013 - 2019 Jolla Ltd. + * Copyright (c) 2013 - 2023 Jolla Ltd. * Copyright (c) 2019 Open Mobile Platform LLC. * * License: Proprietary @@ -7,11 +7,12 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 +import Sailfish.Store 1.0 import com.jolla.settings.system 1.0 import org.nemomobile.systemsettings 1.0 import org.nemomobile.devicelock 1.0 import org.nemomobile.ofono 1.0 -import org.nemomobile.dbus 2.0 +import Nemo.DBus 2.0 Page { id: aboutPage @@ -184,6 +185,12 @@ Page { } } + Loader { + width: parent.width + active: StoreClient.isAvailable + source: "AddOnsField.qml" + } + DetailItem { //: Label for the version of the device-specific software package (drivers) //% "Device adaptation" @@ -196,12 +203,14 @@ Page { //% "WLAN MAC address" label: qsTrId("settings_about-la-wlan_mac_address") value: aboutSettings.wlanMacAddress + visible: value !== "" } DetailItem { //% "Bluetooth address" label: qsTrId("settings_about-la-bluetooth_address") value: bluetoothInfo.adapterAddress + visible: value !== "" // aboutSettings.bluetoothAddress may be 00:00:00:00:00 if the adapter could not // be initialized at start-up, use BluetoothInfo instead (guarantees a valid address) @@ -216,14 +225,16 @@ Page { source: "HomeEncryption.qml" } - DetailItem { - visible: tohInfo.tohReady && value !== "" - label: "The Other Half" - value: tohInfo.tohId + Item { + height: Theme.paddingMedium + width: 1 + } - TohInfo { - id: tohInfo - } + Button { + anchors.horizontalCenter: parent.horizontalCenter + //% "Licenses" + text: qsTrId("settings_about-la-licenses") + onClicked: pageStack.animatorPush("com.jolla.settings.system.PackagesPage") } Snippets { diff --git a/usr/share/jolla-settings/pages/about/snippets/010-icasa-jollaphone.qml b/usr/share/jolla-settings/pages/about/snippets/010-icasa-jollaphone.qml new file mode 100644 index 00000000..e56294cf --- /dev/null +++ b/usr/share/jolla-settings/pages/about/snippets/010-icasa-jollaphone.qml @@ -0,0 +1,24 @@ +import QtQuick 2.1 +import Sailfish.Silica 1.0 + +Item { + height: image.height + + Image { + id: image + + x: Theme.horizontalPageMargin + width: Theme.itemSizeSmall + height: width + source: "graphic-brand-icasa.png" + + Text { + anchors.left: parent.right + anchors.leftMargin: Theme.paddingMedium + anchors.verticalCenter: parent.verticalCenter + color: Theme.highlightColor + font.pixelSize: Theme.fontSizeTiny + text: "TA-2013/2346\nAPPROVED" + } + } +} diff --git a/usr/share/jolla-settings/pages/about/snippets/100-license-jolla.qml b/usr/share/jolla-settings/pages/about/snippets/100-license-jolla.qml index f7fd65c0..793002ad 100644 --- a/usr/share/jolla-settings/pages/about/snippets/100-license-jolla.qml +++ b/usr/share/jolla-settings/pages/about/snippets/100-license-jolla.qml @@ -11,7 +11,7 @@ Column { } AboutText { - text: "Jolla Oy
ATTN: Source Code Requests
Polttimonkatu 3
33210 Tampere
FINLAND" + text: "Jollyboys Ltd.
ATTN: Source Code Requests
Polttimonkatu 3
33210 Tampere
FINLAND" } AboutText { diff --git a/usr/share/jolla-settings/pages/about/snippets/101-trademark-sailfish-jolla.qml b/usr/share/jolla-settings/pages/about/snippets/101-trademark-sailfish-jolla.qml index 2c9dd1d3..8c65bcf7 100644 --- a/usr/share/jolla-settings/pages/about/snippets/101-trademark-sailfish-jolla.qml +++ b/usr/share/jolla-settings/pages/about/snippets/101-trademark-sailfish-jolla.qml @@ -6,7 +6,7 @@ Column { spacing: Theme.paddingLarge AboutText { - //% "Jolla and Sailfish are trademarks or registered trademarks of Jolla Ltd. Jolla's product names are either trademarks or registered trademarks of Jolla. Jolla’s software is protected by copyright, trademark, trade secrets and other intellectual property rights of Jolla and its licensors." - text: qsTrId("settings_about-la-jolla_trademark_notification") + //% "Jolla and Sailfish are trademarks or registered trademarks of Jollyboys Ltd. ('Our'). Our product names are either our trademarks or registered trademarks. Our Software is protected by copyright, trademark, trade secrets and other intellectual property rights." + text: qsTrId("settings_about-la-jolla_trademarks") } } diff --git a/usr/share/jolla-settings/pages/about/snippets/250-avc.qml b/usr/share/jolla-settings/pages/about/snippets/250-avc.qml new file mode 100644 index 00000000..4adf975a --- /dev/null +++ b/usr/share/jolla-settings/pages/about/snippets/250-avc.qml @@ -0,0 +1,11 @@ +import QtQuick 2.1 +import com.jolla.settings.system 1.0 + +Item { + height: textItem.height + + AboutText { + id: textItem + text: "AVC Video. THIS PRODUCT IS LICENSED UNDER THE AVC PATENT PORTFOLIO LICENSE FOR THE PERSONAL USE OF A CONSUMER OR OTHER USES IN WHICH IT DOES NOT RECEIVE REMUNERATION TO (i) ENCODE VIDEO IN COMPLIANCE WITH THE AVC STANDARD (“AVC VIDEO”) AND/OR (ii) DECODE AVC VIDEO THAT WAS ENCODED BY A CONSUMER ENGAGED IN A PERSONAL ACTIVITY AND/OR WAS OBTAINED FROM A VIDEO PROVIDER LICENSED TO PROVIDE AVC VIDEO. NO LICENSE IS GRANTED OR SHALL BE IMPLIED FOR ANY OTHER USE. ADDITIONAL INFORMATION MAY BE OBTAINED FROM MPEG LA, L.L.C. SEE HTTP://WWW.MPEGLA.COM." + } +} diff --git a/usr/share/jolla-settings/pages/about/snippets/260-mp4.qml b/usr/share/jolla-settings/pages/about/snippets/260-mp4.qml new file mode 100644 index 00000000..2911bf20 --- /dev/null +++ b/usr/share/jolla-settings/pages/about/snippets/260-mp4.qml @@ -0,0 +1,11 @@ +import QtQuick 2.1 +import com.jolla.settings.system 1.0 + +Item { + height: textItem.height + + AboutText { + id: textItem + text: "MP4 Video. THIS PRODUCT IS LICENSED UNDER THE MPEG-4 VISUAL PATENT PORTFOLIO LICENSE FOR THE PERSONAL AND NON-COMMERCIAL USE OF A CONSUMER FOR (i) ENCODING VIDEO IN COMPLIANCE WITH THE MPEG-4 VISUAL STANDARD (\"MPEG-4 VIDEO\") AND/OR (ii) DECODING MPEG-4 VIDEO THAT WAS ENCODED BY A CONSUMER ENGAGED IN A PERSONAL AND NON- COMMERCIAL ACTIVITY AND/OR WAS OBTAINED FROM A VIDEO PROVIDER LICENSED BY MPEG LA TO PROVIDE MPEG-4 VIDEO. NO LICENSE IS GRANTED OR SHALL BE IMPLIED FOR ANY OTHER USE. ADDITIONAL INFORMATION INCLUDING THAT RELATING TO PROMOTIONAL, INTERNAL AND COMMERCIAL USES AND LICENSING MAY BE OBTAINED FROM MPEG LA, LLC. SEE HTTP://WWW.MPEGLA.COM." + } +} diff --git a/usr/share/jolla-settings/pages/about/snippets/270-mp3.qml b/usr/share/jolla-settings/pages/about/snippets/270-mp3.qml new file mode 100644 index 00000000..afa57049 --- /dev/null +++ b/usr/share/jolla-settings/pages/about/snippets/270-mp3.qml @@ -0,0 +1,11 @@ +import QtQuick 2.1 +import com.jolla.settings.system 1.0 + +Item { + height: textItem.height + + AboutText { + id: textItem + text: "MPEG Layer-3. MPEG Layer-3 audio coding technology licensed from Fraunhofer IIS and Thomson Licensing. Supply of this product does not convey a license nor imply any right to distribute MPEG Layer-3 compliant content created with this product in revenue-generating broadcast systems (terrestrial, satellite, cable and/or other distribution channels), streaming applications (via Internet, intranets and/or other networks), other content distribution systems (pay-audio or audio-on-demand applications and the like) or on physical media (compact discs, digital versatile discs, semiconductor chips, hard drives, memory cards and the like). An independent license for such use is required. For details, please visit http://mp3licensing.com." + } +} diff --git a/usr/share/jolla-settings/pages/about/snippets/400-package-licenses.qml b/usr/share/jolla-settings/pages/about/snippets/400-package-licenses.qml deleted file mode 100644 index 1283d5e1..00000000 --- a/usr/share/jolla-settings/pages/about/snippets/400-package-licenses.qml +++ /dev/null @@ -1,19 +0,0 @@ -import QtQuick 2.1 -import Sailfish.Silica 1.0 -import com.jolla.settings.system 1.0 - -Column { - AboutText { - //: Text surrounded by %1 and %2 is underlined and colored differently - //% "You can %1see information about packages%2 installed on this system." - text: qsTrId("settings_package_licenses-la-packages_info") - .arg("") - .arg("") - - MouseArea { - id: mouseArea - anchors.fill: parent - onClicked: pageStack.animatorPush("com.jolla.settings.system.PackagesPage") - } - } -} diff --git a/usr/share/jolla-settings/pages/advanced-networking/mainpage.qml b/usr/share/jolla-settings/pages/advanced-networking/mainpage.qml index e5a8d12f..2f8f6691 100644 --- a/usr/share/jolla-settings/pages/advanced-networking/mainpage.qml +++ b/usr/share/jolla-settings/pages/advanced-networking/mainpage.qml @@ -110,7 +110,10 @@ Page { ProxyForm { id: proxyForm network: netProxy - enabled: !disabledByMdmBanner.active + enabled: netProxySwitch.checked && !disabledByMdmBanner.active + //: Referring to the network proxy method to use for all connections + //% "Global proxy configuration" + comboLabel: qsTrId("settings_network-la-global_proxy_configuration") } } } diff --git a/usr/share/jolla-settings/pages/battery/mainpage.qml b/usr/share/jolla-settings/pages/battery/mainpage.qml index c8e37b52..4a492046 100644 --- a/usr/share/jolla-settings/pages/battery/mainpage.qml +++ b/usr/share/jolla-settings/pages/battery/mainpage.qml @@ -14,6 +14,23 @@ Page { readonly property int effectivePowerSaveModeThreshold: displaySettings.powerSaveModeEnabled ? displaySettings.powerSaveModeThreshold : -1 + readonly property var chargingModeOptions: [BatteryStatus.EnableCharging, + //BatteryStatus.DisableCharging, + BatteryStatus.ApplyChargingThresholds, + //BatteryStatus.ApplyChargingThresholdsAfterFull, + ] + property alias chargingThresholdsSupported: batteryStatus.chargingSuspendendable + readonly property var chargingThresholdOptions: [80, 90] + readonly property int chargingThresholdDelta: 3 + readonly property bool chargingThresholdsAreValid: batteryStatus.chargeEnableLimit < batteryStatus.chargeDisableLimit + readonly property bool chargingThresholdsAreSimple: batteryStatus.chargeEnableLimit + chargingThresholdDelta == batteryStatus.chargeDisableLimit + readonly property bool chargingThresholdsAreRelevant: (batteryStatus.chargingMode == BatteryStatus.ApplyChargingThresholds + || batteryStatus.chargingMode== BatteryStatus.ApplyChargingThresholdsAfterFull) + readonly property bool forcedChargingIsRelevant: (batteryStatus.chargerStatus == BatteryStatus.Connected + && batteryStatus.status != BatteryStatus.Full + && batteryStatus.chargingMode != BatteryStatus.EnableCharging) + property alias forcedChargingIsActive: batteryStatus.chargingForced + function thresholdText(threshold) { if (threshold < 0 ) { //% "Not in use" @@ -31,6 +48,47 @@ Page { } } + function chargingModeText(mode) { + if (mode == BatteryStatus.EnableCharging) { + //% "Normal" + return qsTrId("settings_battery-la-charging_always_enabled") + } + if (mode == BatteryStatus.DisableCharging) { + //% "Disabled" + return qsTrId("settings_battery-la-charging_always_disabled") + } + if (mode == BatteryStatus.ApplyChargingThresholds) { + //% "Apply thresholds" + return qsTrId("settings_battery-la-charging_apply_thresholds") + } + if (mode == BatteryStatus.ApplyChargingThresholdsAfterFull) { + //% "Charge to full, then apply thresholds" + return qsTrId("settings_battery-la-charging_apply_thresholds_after_full") + } + //% "Unknown" + return qsTrId("settings_battery-la-charging_unknown_mode") + } + function chargingModeDescription(mode) { + if (mode == BatteryStatus.EnableCharging) { + //% "Charge whenever a charger is connected" + return qsTrId("settings_battery-la-charging_always_enabled_description") + } + if (mode == BatteryStatus.DisableCharging) { + //% "Charge only when battery is close to empty" + return qsTrId("settings_battery-la-charging_always_disabled_description") + } + if (mode == BatteryStatus.ApplyChargingThresholds + || mode == BatteryStatus.ApplyChargingThresholdsAfterFull) { + if (chargingThresholdsAreSimple) { + //% "Stop charging at specified value" + return qsTrId("settings_battery-la-charging_apply_thresholds_description_value") + } + //% "Keep battery level within specified range" + return qsTrId("settings_battery-la-charging_apply_thresholds_description_range") + } + return "" + } + SilicaFlickable { anchors.fill: parent contentHeight: content.height + Theme.paddingMedium @@ -45,21 +103,9 @@ Page { title: qsTrId("settings_system-he-battery") } - IconTextSwitch { - icon.source: "image://theme/icon-m-battery-saver" - automaticCheck: false - checked: displaySettings.powerSaveModeForced - //% "Enable battery saving mode until charger is connected the next time" - text: qsTrId("settings_battery-la-battery-saving-mode-enabled") - //% "Battery saving mode will adjust the device behaviour to help improve battery life. " - //% "It may disable email and calendar sync, lower display brightness etc." - description: qsTrId("settings_battery-la-battery-saving-mode-enabled_description") - onClicked: displaySettings.powerSaveModeForced = !displaySettings.powerSaveModeForced - } - SectionHeader { - //% "Automatic battery saving" - text: qsTrId("settings_battery-la-automatic_battery_saving") + //% "Battery saving mode" + text: qsTrId("settings_battery-la-battery_saving_mode") } ComboBox { @@ -68,8 +114,9 @@ Page { value: thresholdText(effectivePowerSaveModeThreshold) //% "Activation threshold" label: qsTrId("settings_battery-la-battery_saving_threshold") - //% "Set threshold for automatically enabling battery saving mode." - description: qsTrId("settings_battery-la-power_saving_mode_threshold_description") + //% "Battery saving mode will adjust the device behaviour to help improve battery life. " + //% "It may disable email and calendar sync, lower display brightness etc." + description: qsTrId("settings_battery-la-battery_saving_threshold_description") Binding { target: thresholdComboBox @@ -87,10 +134,103 @@ Page { } } } + + IconTextSwitch { + icon.source: "image://theme/icon-m-battery-saver" + automaticCheck: false + checked: displaySettings.powerSaveModeForced + //% "Enable battery saving mode until charger is connected the next time" + text: qsTrId("settings_battery-la-battery-saving-mode-forced") + //% "Temporarily enable battery saving mode regardless of the activation threshold" + description: qsTrId("settings_battery-la-battery-saving-mode-forced_description") + onClicked: displaySettings.powerSaveModeForced = !displaySettings.powerSaveModeForced + } + + SectionHeader { + visible: chargingThresholdsSupported + //% "Battery ageing protection" + text: qsTrId("settings_battery-la-battery_ageing_protection") + } + + ComboBox { + id: chargingModeComboBox + visible: chargingThresholdsSupported + //% "Charging mode" + label: qsTrId("settings_battery-la-charging_mode") + value: chargingModeText(batteryStatus.chargingMode) + description: chargingModeDescription(batteryStatus.chargingMode) + Binding { + target: chargingModeComboBox + property: "currentIndex" + value: root.chargingModeOptions.indexOf(batteryStatus.chargingMode) + } + menu: ContextMenu { + Repeater { + model: root.chargingModeOptions + MenuItem { + text: chargingModeText(modelData) + onClicked: batteryStatus.chargingMode = modelData + } + } + } + } + + ComboBox { + id: chargingThresholdsComboBox + value: { + if (chargingThresholdsAreSimple) { + return "%1%".arg(batteryStatus.chargeDisableLimit) + } + return "%1% - %2%".arg(batteryStatus.chargeEnableLimit).arg(batteryStatus.chargeDisableLimit) + } + label: { + if (chargingThresholdsAreSimple) { + //% "Stop charging at" + return qsTrId("settings_battery-la-charging_disable_limit") + } + //% "Keep in range" + return qsTrId("settings_battery-la-charging_keep_in_range") + } + visible: chargingThresholdsSupported && chargingThresholdsAreRelevant + valueColor: chargingThresholdsAreValid ? Theme.highlightColor : "red" + Binding { + target: chargingThresholdsComboBox + property: "currentIndex" + value: root.chargingThresholdOptions.indexOf(batteryStatus.chargeDisableLimit) + } + menu: ContextMenu { + Repeater { + model: root.chargingThresholdOptions + MenuItem { + text: "%1%".arg(modelData) + onClicked: { + batteryStatus.chargeDisableLimit = modelData + batteryStatus.chargeEnableLimit = modelData - chargingThresholdDelta + } + } + } + } + } + + IconTextSwitch { + icon.source: "image://theme/icon-m-battery" + automaticCheck: false + visible: chargingThresholdsSupported && forcedChargingIsRelevant + checked: forcedChargingIsActive + //% "Fully charge this time" + text: qsTrId("settings_battery-la-apply-charging-thresholds-after-full") + //% "Temporarily suppress battery ageing protection to fully charge the battery once" + description: qsTrId("settings_battery-la-apply-charging-thresholds-after-full_description") + onClicked: forcedChargingIsActive = !forcedChargingIsActive + } } } DisplaySettings { id: displaySettings } + + BatteryStatus { + id: batteryStatus + } } diff --git a/usr/share/jolla-settings/pages/bluetooth/BluetoothVisibilityComboBox.qml b/usr/share/jolla-settings/pages/bluetooth/BluetoothVisibilityComboBox.qml index 806902e2..cb01dd02 100644 --- a/usr/share/jolla-settings/pages/bluetooth/BluetoothVisibilityComboBox.qml +++ b/usr/share/jolla-settings/pages/bluetooth/BluetoothVisibilityComboBox.qml @@ -1,6 +1,6 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 -import org.nemomobile.configuration 1.0 +import Nemo.Configuration 1.0 ComboBox { id: root diff --git a/usr/share/jolla-settings/pages/bluetooth/EnableSwitch.qml b/usr/share/jolla-settings/pages/bluetooth/EnableSwitch.qml index f637df9f..8a428150 100644 --- a/usr/share/jolla-settings/pages/bluetooth/EnableSwitch.qml +++ b/usr/share/jolla-settings/pages/bluetooth/EnableSwitch.qml @@ -1,7 +1,7 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 import Sailfish.Lipstick 1.0 -import MeeGo.Connman 0.2 +import Connman 0.2 import Nemo.DBus 2.0 import com.jolla.settings 1.0 import Sailfish.Policy 1.0 diff --git a/usr/share/jolla-settings/pages/bluetooth/bluetoothSettings.qml b/usr/share/jolla-settings/pages/bluetooth/bluetoothSettings.qml index 6f1e05c4..d0ff7f3a 100644 --- a/usr/share/jolla-settings/pages/bluetooth/bluetoothSettings.qml +++ b/usr/share/jolla-settings/pages/bluetooth/bluetoothSettings.qml @@ -1,13 +1,13 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 -import MeeGo.Connman 0.2 +import Connman 0.2 import Sailfish.Bluetooth 1.0 import com.jolla.settings.bluetooth.translations 1.0 import org.kde.bluezqt 1.0 as BluezQt -import Nemo.Ssu 1.1 as Ssu import Nemo.DBus 2.0 import Sailfish.Policy 1.0 import com.jolla.settings.system 1.0 +import org.nemomobile.systemsettings 1.0 Page { id: root @@ -164,6 +164,7 @@ Page { TextField { id: deviceNameField + width: parent.width //: Name of bluetooth device @@ -172,14 +173,16 @@ Page { // Show default name as hint when no text is entered. Don't do this when adapter is // unavailable to avoid confusion if the name normally has a non-default value. - placeholderText: adapter ? Ssu.DeviceInfo.displayName(Ssu.DeviceInfo.DeviceModel) : "" + placeholderText: adapter ? deviceInfo.prettyName : "" - //Make sure there's adapter. If adapter use adapter name. If no adapter name use ssu name. - text: adapter ? (adapter.name ? adapter.name : adapter.name = Ssu.DeviceInfo.displayName(Ssu.DeviceInfo.DeviceModel)) : "" + //Make sure there's adapter. If adapter use adapter name. If no adapter name use device info name. + text: adapter ? (adapter.name ? adapter.name + : adapter.name = deviceInfo.prettyName) + : "" onActiveFocusChanged: { if (!activeFocus && adapter) { - var newName = text.length ? text : Ssu.DeviceInfo.displayName(Ssu.DeviceInfo.DeviceModel) + var newName = text.length ? text : deviceInfo.prettyName if (adapter.name != newName) { adapter.name = newName } else { @@ -247,6 +250,10 @@ Page { } } + DeviceInfo { + id: deviceInfo + } + Connections { id: adapterConn target: root.adapter diff --git a/usr/share/jolla-settings/pages/browser/browser.qml b/usr/share/jolla-settings/pages/browser/browser.qml index 48ed0cd0..5401e7f7 100644 --- a/usr/share/jolla-settings/pages/browser/browser.qml +++ b/usr/share/jolla-settings/pages/browser/browser.qml @@ -12,7 +12,7 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 import org.sailfishos.browser.settings 1.0 -import org.nemomobile.dbus 2.0 +import Nemo.DBus 2.0 import com.jolla.settings 1.0 ApplicationSettings { diff --git a/usr/share/jolla-settings/pages/crash-reporter/PendingUploads.qml b/usr/share/jolla-settings/pages/crash-reporter/PendingUploads.qml new file mode 100644 index 00000000..f0d3d3cb --- /dev/null +++ b/usr/share/jolla-settings/pages/crash-reporter/PendingUploads.qml @@ -0,0 +1,169 @@ +/* + * This file is part of crash-reporter + * + * Copyright (C) 2013 Jolla Ltd. + * Contact: Jakub Adam + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * version 2.1 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, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import com.jolla.settings.crashreporter 1.0 + +Page { + id: root + + property bool _modifyingReportList + property bool deletingUploads + + SilicaListView { + anchors.fill: parent + + PullDownMenu { + MenuItem { + enabled: Adapter.reportsToUpload > 0 + //% "Delete unsent reports" + text: qsTrId("quick-feedback_delete_reports") + onClicked: { + var remorse = Remorse.popupAction( + root, + //% "Deleted %n crash report(s)" + qsTrId("quick-feedback_deleted", Adapter.reportsToUpload), + function() { + root._modifyingReportList = true + Adapter.deleteAllCrashReports() + }) + root.deletingUploads = Qt.binding(function() { return remorse && remorse.active }) + } + } + + MenuItem { + enabled: Adapter.reportsToUpload > 0 + //% "Upload crash reports now" + text: qsTrId("quick-feedback_upload_now") + onClicked: { + root._modifyingReportList = true + Adapter.uploadAllCrashReports() + } + } + } + + header: PageHeader { + //% "Pending uploads" + title: qsTrId("crash-reporter_pending_uploads") + } + + model: Adapter.pendingUploads + + VerticalScrollDecorator {} + + delegate: ListItem { + id: listDelegate + + function remove() { + //% "Deleted %1" + var remorseMessage = qsTrId("settings_crash-reporter_deleted_application").arg(model.application) + + remorseAction(remorseMessage, function() { + Adapter.deleteCrashReport(model.filePath) + }) + } + + enabled: !root.deletingUploads + opacity: enabled ? 1.0 : 0.0 + Behavior on opacity { FadeAnimator {}} + contentHeight: crashDetails.visible ? Theme.itemSizeMedium : Theme.itemSizeSmall + + menu: Component { + ContextMenu { + MenuItem { + //% "Upload" + text: qsTrId("settings_crash-reporter_upload") + onClicked: { + Utils.notifyAutoUploader([ model.filePath ], false) + } + } + MenuItem { + //% "Delete" + text: qsTrId("settings_crash-reporter_delete") + onClicked: { + remove() + } + } + } + } + + Item { + x: Theme.horizontalPageMargin + width: parent.width - 2*x + height: parent.height + + Label { + id: appLabel + anchors { + verticalCenter: parent.verticalCenter + verticalCenterOffset: crashDetails.visible ? -implicitHeight/2 : 0 + } + width: parent.width - dateLabel.width + + text: model.application + truncationMode: TruncationMode.Fade + color: listDelegate.highlighted ? Theme.highlightColor : Theme.primaryColor + } + Label { + id: dateLabel + anchors { + right: parent.right + verticalCenter: appLabel.verticalCenter + } + text: Qt.formatDateTime(model.dateCreated) + font.pixelSize: Theme.fontSizeExtraSmall + color: appLabel.color + } + Row { + id: crashDetails + visible: Utils.reportIncludesCrash(model.application) + + anchors.top: appLabel.bottom + anchors.left: parent.left + + Label { + text: model.signal + font.pixelSize: Theme.fontSizeExtraSmall + color: appLabel.color + } + Label { + text: " PID " + font.pixelSize: Theme.fontSizeExtraSmall + color: listDelegate.highlighted ? Theme.secondaryHighlightColor : Theme.secondaryColor + } + Label { + text: model.pid + font.pixelSize: Theme.fontSizeExtraSmall + color: appLabel.color + } + } + } + } + + onCountChanged: { + if (count == 0 && root._modifyingReportList) { + pageStack.pop() + } + root._modifyingReportList = false + } + } +} diff --git a/usr/share/jolla-settings/pages/crash-reporter/ServerSettingsDialog.qml b/usr/share/jolla-settings/pages/crash-reporter/ServerSettingsDialog.qml new file mode 100644 index 00000000..286f88ae --- /dev/null +++ b/usr/share/jolla-settings/pages/crash-reporter/ServerSettingsDialog.qml @@ -0,0 +1,137 @@ +/* + * This file is part of crash-reporter + * + * Copyright (C) 2013 Jolla Ltd. + * Contact: Jakub Adam + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * version 2.1 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, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import com.jolla.settings.crashreporter 1.0 + +Dialog { + property bool acceptable: serverUrlText.acceptableInput + && serverPortText.acceptableInput + && serverPathText.acceptableInput + && usernameText.acceptableInput + && passwordText.acceptableInput + + canAccept: acceptable + + onAccepted: { + ApplicationSettings.serverUrl = serverUrlText.text + ApplicationSettings.serverPort = serverPortText.text + ApplicationSettings.serverPath = serverPathText.text + ApplicationSettings.useSsl = (serverUrlText.text.indexOf("https://") == 0) + + ApplicationSettings.username = usernameText.text + ApplicationSettings.password = passwordText.text + } + + SilicaFlickable { + anchors.fill: parent + contentHeight: column.height + + Column { + id: column + + width: parent.width + DialogHeader {} + + TextField { + id: serverUrlText + + width: parent.width + inputMethodHints: Qt.ImhUrlCharactersOnly | Qt.ImhNoPredictiveText + //% "Enter server URL" + placeholderText: qsTrId("settings_crash-reporter_server_url_placeholder") + text: ApplicationSettings.serverUrl + validator: RegExpValidator { regExp: /^http[s]?:\/\/\w([\w-\.]*\w)*$/ } + label: qsTrId("settings_crash-reporter_server_url") + EnterKey.enabled: acceptableInput + EnterKey.iconSource: "image://theme/icon-m-enter-next" + EnterKey.onClicked: serverPortText.focus = true + } + + TextField { + id: serverPortText + + width: parent.width + inputMethodHints: Qt.ImhDigitsOnly | Qt.ImhNoPredictiveText + //% "Enter server port" + placeholderText: qsTrId("settings_crash-reporter_server_port_placeholder") + text: ApplicationSettings.serverPort + validator: IntValidator { bottom: 1; top: 65535 } + //% "Server port" + label: qsTrId("settings_crash-reporter_server_port") + EnterKey.enabled: acceptableInput + EnterKey.iconSource: "image://theme/icon-m-enter-next" + EnterKey.onClicked: serverPathText.focus = true + } + + TextField { + id: serverPathText + + width: parent.width + inputMethodHints: Qt.ImhNoPredictiveText + //% "Enter server path" + placeholderText: qsTrId("settings_crash-reporter_server_path_placeholder") + text: ApplicationSettings.serverPath + validator: RegExpValidator { regExp: /^\/.*$/ } + //% "Server path" + label: qsTrId("settings_crash-reporter_server_path") + EnterKey.enabled: acceptableInput + EnterKey.iconSource: "image://theme/icon-m-enter-next" + EnterKey.onClicked: usernameText.focus = true + } + + SectionHeader { + //% "Security" + text: qsTrId("settings_crash-reporter_security") + } + + TextField { + id: usernameText + + width: parent.width + inputMethodHints: Qt.ImhNoPredictiveText + //% "Enter username" + placeholderText: qsTrId("settings_crash-reporter_username_placeholder") + text: ApplicationSettings.username + //% "Username" + label: qsTrId("settings_crash-reporter_username") + EnterKey.iconSource: "image://theme/icon-m-enter-next" + EnterKey.onClicked: passwordText.focus = true + } + + TextField { + id: passwordText + + width: parent.width + inputMethodHints: Qt.ImhNoPredictiveText + //% "Enter password" + placeholderText: qsTrId("settings_crash-reporter_password_placeholder") + text: ApplicationSettings.password + //% "Password" + label: qsTrId("settings_crash-reporter_password") + echoMode: TextInput.Password + EnterKey.iconSource: "image://theme/icon-m-enter-next" + EnterKey.onClicked: serverUrlText.focus = true + } + } + } +} diff --git a/usr/share/jolla-settings/pages/crash-reporter/crash-reporter.qml b/usr/share/jolla-settings/pages/crash-reporter/crash-reporter.qml new file mode 100644 index 00000000..c6d05bd0 --- /dev/null +++ b/usr/share/jolla-settings/pages/crash-reporter/crash-reporter.qml @@ -0,0 +1,319 @@ +/* + * This file is part of crash-reporter + * + * Copyright (C) 2013 Jolla Ltd. + * Contact: Jakub Adam + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * version 2.1 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, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import com.jolla.settings.crashreporter 1.0 + +Page { + id: root + + SilicaFlickable { + anchors.fill: parent + contentHeight: content.height + Theme.paddingLarge + + Column { + id: content + + width: parent.width + + PageHeader { + //% "Crash reporter" + title: qsTrId("settings_crash-reporter") + } + + PullDownMenu { + MenuItem { + enabled: Adapter.reportsToUpload > 0 + //% "Show pending uploads" + text: qsTrId("quick-feedback_pending_uploads") + onClicked: { + pageStack.animatorPush("PendingUploads.qml") + } + } + MenuLabel { + //% "%n report(s) to upload" + text: qsTrId("quick-feedback_reports_to_upload", Adapter.reportsToUpload) + } + } + + SystemdServiceSwitch { + serviceName: "crash-reporter.service" + + //% "Upload reports automatically" + text: qsTrId("settings_crash-reporter_upload_automatically") + //% "Uploads created crash reports to a telemetry server." + description: qsTrId("settings_crash-reporter_report_crashes_description") + + onAfterStateChange: { + serviceEnabled = newState + } + } + + ValueButton { + //% "Server URL" + label: qsTrId("settings_crash-reporter_server_url") + value: ApplicationSettings.serverUrl + ":" + ApplicationSettings.serverPort + ApplicationSettings.serverPath + + onClicked: { + pageStack.animatorPush("ServerSettingsDialog.qml") + } + } + + TextSwitch { + id: createReportsSwitch + automaticCheck: false + checked: PrivacySettings.coreDumping + //% "Create crash reports" + text: qsTrId("settings_crash-reporter_create reports") + //% "When an application crashes, a report is created including a core dump and other information to help the developers in tracing the bug." + description: qsTrId("settings_crash-reporter_create reports_description") + onClicked: PrivacySettings.coreDumping = !PrivacySettings.coreDumping + } + + TextSwitch { + automaticCheck: false + checked: PrivacySettings.notifications + //% "Notifications" + text: qsTrId("settings_crash-reporter_notifications") + //% "Displays user notifications when crash report is being uploaded." + description: qsTrId("settings_crash-reporter_notifications_description") + onClicked: PrivacySettings.notifications = !PrivacySettings.notifications + } + + TextSwitch { + automaticCheck: false + checked: PrivacySettings.autoDeleteDuplicates + //% "Auto-delete duplicates" + text: qsTrId("settings_crash-reporter_autodelete_duplicates") + //% "Each day, uploads only first 5 crash reports of an application. The others, likely duplicate, are deleted without being uploaded to conserve space." + description: qsTrId("settings_crash-reporter_autodelete_duplicates_description") + onClicked: PrivacySettings.autoDeleteDuplicates = !PrivacySettings.autoDeleteDuplicates + } + + TextSwitch { + //% "Endurance reports" + text: qsTrId("settings_crash-reporter_enable_endurance") + //% "Reports system statistics helping diagnose problems that " + //% "manifest themselves only after a long-term use of the " + //% "device like memory leaks, excessive battery drain, or " + //% "decreasing performance." + description: qsTrId("settings_crash-reporter_enable_endurance_description") + automaticCheck: false + checked: PrivacySettings.endurance + onClicked: { + checked = !checked + PrivacySettings.endurance = checked + Utils.setEnduranceServiceState(checked) + } + } + + TextSwitch { + //% "Journal spy" + text: qsTrId("settings_crash-reporter_enable_journalspy") + //% "Watches system logs for predefined regular expressions " + //% "and creates a telemetry submission upon a found match." + description: qsTrId("settings_crash-reporter_enable_journalspy_description") + automaticCheck: false + checked: PrivacySettings.journalSpy + onClicked: { + checked = !checked + PrivacySettings.journalSpy = checked + Utils.setJournalSpyServiceState(checked) + } + } + + SectionHeader { + //% "Data transmissions" + text: qsTrId("settings_crash-reporter_data_transmissions") + } + + TextSwitch { + automaticCheck: false + checked: PrivacySettings.allowMobileData + //% "Allow using mobile data" + text: qsTrId("settings_crash-reporter_use_mobile_data") + //% "Enables crash report transmissions through mobile network; additional charges from the network carrier may apply. When this option is off, WLAN connection must be available in order to send crash reports." + description: qsTrId("settings_crash-reporter_use_mobile_data_description") + onClicked: PrivacySettings.allowMobileData = !PrivacySettings.allowMobileData + } + + SectionHeader { + //% "Battery care" + text: qsTrId("settings_crash-reporter_battery_care") + } + + ComboBox { + id: dischargingThresholdComboBox + + readonly property var options: [-1, 20, 40, 60, 80] + readonly property int effectiveDischargingThreshold: PrivacySettings.restrictWhenDischarging + ? PrivacySettings.dischargingThreshold + : -1 + + function thresholdText(threshold) { + if (threshold < 0) { + //% "Never" + return qsTrId("settings_crash-reporter_disobey_discharging") + } + //% "Below %1%" + return qsTrId("settings_crash-reporter_require_charger_below").arg(Math.min(threshold, 100)) + } + + function setThreshold(threshold) { + if (threshold < 0) { + PrivacySettings.restrictWhenDischarging = false + } else { + PrivacySettings.dischargingThreshold = threshold + PrivacySettings.restrictWhenDischarging = true + } + } + + value: thresholdText(effectiveDischargingThreshold) + //% "Require charger" + label: qsTrId("settings_crash-reporter_restrict_when_discharging") + //% "Avoid power intensive tasks when discharging and battery level dropped too much." + description: qsTrId("settings_crash-reporter_restrict_when_discharging_description") + + Binding { + target: dischargingThresholdComboBox + property: "currentIndex" + value: dischargingThresholdComboBox.options.indexOf(dischargingThresholdComboBox.effectiveDischargingThreshold) + } + + menu: ContextMenu { + Repeater { + model: dischargingThresholdComboBox.options + MenuItem { + text: dischargingThresholdComboBox.thresholdText(modelData) + onClicked: dischargingThresholdComboBox.setThreshold(modelData) + } + } + } + } + + TextSwitch { + automaticCheck: false + checked: !PrivacySettings.restrictWhenLowBattery + //% "Allow when battery is low" + text: qsTrId("settings_crash-reporter_disobey_low_battery") + //% "Power intensive tasks may drain your battery regardless of charger presence." + description: qsTrId("settings_crash-reporter_disobey_low_battery_descrition") + onClicked: PrivacySettings.restrictWhenLowBattery = !PrivacySettings.restrictWhenLowBattery + } + + SectionHeader { + //% "Stack trace" + text: qsTrId("settings_crash-reporter_stack_trace") + } + + TextSwitch { + id: includeStackTraceSwitch + enabled: createReportsSwitch.checked + automaticCheck: false + checked: PrivacySettings.includeStackTrace + //% "Include stack trace" + text: qsTrId("settings_crash-reporter_include_stack_trace") + //% "Crash report will include a stack trace generated on the device." + description: qsTrId("settings_crash-reporter_include_stack_trace_description") + onClicked: PrivacySettings.includeStackTrace = !PrivacySettings.includeStackTrace + } + + TextSwitch { + enabled: includeStackTraceSwitch.checked + automaticCheck: false + checked: PrivacySettings.downloadDebuginfo + //% "Download debug symbols" + text: qsTrId("settings_crash-reporter_download_debug_symbols") + //% "Tries to automatically download missing debug symbols before making a stack trace." + description: qsTrId("settings_crash-reporter_download_debug_symbols_description") + onClicked: PrivacySettings.downloadDebuginfo = !PrivacySettings.downloadDebuginfo + } + + SectionHeader { + //% "Logging" + text: qsTrId("settings_crash-reporter_logging") + } + + ComboBox { + //% "Log reporter activity" + label: qsTrId("settings_crash-reporter_logger_type") + //% "Debug logging of crash reporter activities to the device doesn't affect the data sent to a server. Change of this setting takes effect after crash reporter restart." + description: qsTrId("settings_crash-reporter_after_restart") + + currentItem: { + switch (ApplicationSettings.loggerType) { + case "none": + return noneItem + case "file": + return fileItem + case "syslog": + return syslogItem + } + return noneItem + } + + onCurrentItemChanged: { + var type + + switch (currentItem) { + case noneItem: + type = "none" + break + case fileItem: + type = "file" + break + case syslogItem: + type = "syslog" + break + } + + ApplicationSettings.loggerType = type + } + + menu: ContextMenu { + MenuItem { + id: noneItem + //% "No logging" + text: qsTrId("settings_crash-reporter_logging_type_none") + } + MenuItem { + id: fileItem + //% "Into a file in /tmp" + text: qsTrId("settings_crash-reporter_logging_type_file") + } + MenuItem { + id: syslogItem + //% "Into systemd journal" + text: qsTrId("settings_crash-reporter_logging_type_syslog") + } + } + } + } + } + + Loader { + active: !PrivacySettings.privacyNoticeAccepted + sourceComponent: PrivacyNotice { + page: root + } + } +} diff --git a/usr/share/jolla-settings/pages/datacounters/mainpage.qml b/usr/share/jolla-settings/pages/datacounters/mainpage.qml index 20033714..094a11c8 100644 --- a/usr/share/jolla-settings/pages/datacounters/mainpage.qml +++ b/usr/share/jolla-settings/pages/datacounters/mainpage.qml @@ -2,7 +2,7 @@ import QtQuick 2.6 import Sailfish.Silica 1.0 import Sailfish.Telephony 1.0 import Sailfish.Policy 1.0 -import MeeGo.Connman 0.2 +import Connman 0.2 import com.jolla.settings.system 1.0 import Nemo.Configuration 1.0 import org.nemomobile.systemsettings 1.0 diff --git a/usr/share/jolla-settings/pages/developermode/AccountManager.qml b/usr/share/jolla-settings/pages/developermode/AccountManager.qml new file mode 100644 index 00000000..54be2033 --- /dev/null +++ b/usr/share/jolla-settings/pages/developermode/AccountManager.qml @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2022 Jolla Ltd. + * + * License: Proprietary + */ + +import QtQuick 2.6 +import Sailfish.Accounts 1.0 + +AccountManager { + function developerAccountProvider() { + var names = providerNames + for (var i = 0; i < names.length; ++i) { + var accountProvider = provider(names[i]) + if (providerHasService(accountProvider, "developermode")) { + return names[i] + } + } + return "" + } + + function providerHasService(provider, serviceName) { + var serviceNames = provider.serviceNames + for (var i = 0; i < serviceNames.length; ++i) { + var accountService = service(serviceNames[i]) + if (accountService.serviceType == serviceName) { + return true + } + } + return false + } + + function hasAccountForProvider(accountIds, providerName) { + for (var i = 0; i < accountIds.length; ++i) { + if (account(accountIds[i]).providerName == providerName) { + return true + } + } + return false + } + + Component.onCompleted: root.developerAccountProvider = developerAccountProvider() + onProviderNamesChanged: root.developerAccountProvider = developerAccountProvider() +} diff --git a/usr/share/jolla-settings/pages/developermode/developermode.qml b/usr/share/jolla-settings/pages/developermode/developermode.qml index c2455cc3..51b63c9f 100644 --- a/usr/share/jolla-settings/pages/developermode/developermode.qml +++ b/usr/share/jolla-settings/pages/developermode/developermode.qml @@ -9,15 +9,13 @@ import QtQuick 2.6 import Sailfish.Silica 1.0 import Nemo.DBus 2.0 import com.jolla.settings.system 1.0 -import com.jolla.settings.accounts 1.0 -import org.nemomobile.configuration 1.0 +import Nemo.Configuration 1.0 import org.nemomobile.devicelock 1.0 import org.nemomobile.systemsettings 1.0 -import org.nemomobile.notifications 1.0 +import Nemo.Notifications 1.0 import Sailfish.Policy 1.0 -import Sailfish.Accounts 1.0 import Nemo.Ssu 1.1 -import MeeGo.Connman 0.2 +import Connman 0.2 Page { id: root @@ -28,7 +26,8 @@ Page { property bool debugHomeRemorse property bool showDeveloperModeSettings: developerModeSettings.developerModeEnabled && !devAccountPrompt.active - readonly property bool hasDeveloperAccount: accountManager.hasAccountForProvider(accountManager.accountIdentifiers, developerAccountProvider) + property QtObject accountManager + readonly property bool hasDeveloperAccount: accountManager ? accountManager.hasAccountForProvider(accountManager.accountIdentifiers, developerAccountProvider) : true property string developerAccountProvider // DummyDeveloperModeSettings { // Replace for mock backend to test UI @@ -36,44 +35,6 @@ Page { id: developerModeSettings } - AccountManager { - id: accountManager - - function developerAccountProvider() { - var names = providerNames - for (var i = 0; i < names.length; ++i) { - var accountProvider = provider(names[i]) - if (providerHasService(accountProvider, "developermode")) { - return names[i] - } - } - return "" - } - - function providerHasService(provider, serviceName) { - var serviceNames = provider.serviceNames - for (var i = 0; i < serviceNames.length; ++i) { - var accountService = service(serviceNames[i]) - if (accountService.serviceType == serviceName) { - return true - } - } - return false - } - - function hasAccountForProvider(accountIds, providerName) { - for (var i = 0; i < accountIds.length; ++i) { - if (account(accountIds[i]).providerName == providerName) { - return true - } - } - return false - } - - Component.onCompleted: root.developerAccountProvider = developerAccountProvider() - onProviderNamesChanged: root.developerAccountProvider = developerAccountProvider() - } - NetworkManager { id: networkManager readonly property bool online: state == "online" @@ -977,6 +938,11 @@ Page { } Component.onCompleted: { + var accountManagerComponent = Qt.createComponent(Qt.resolvedUrl("AccountManager.qml")) + if (accountManagerComponent.status === Component.Ready) { + accountManager = accountManagerComponent.createObject(root) + } + /* Request existing password from the password manager */ passwordManager.passwordChanged() passwordManager.call('isLoginEnabled', [], passwordManager.loginEnabledChanged) diff --git a/usr/share/jolla-settings/pages/devicelock/devicelock.qml b/usr/share/jolla-settings/pages/devicelock/devicelock.qml index 84421052..20f0768b 100644 --- a/usr/share/jolla-settings/pages/devicelock/devicelock.qml +++ b/usr/share/jolla-settings/pages/devicelock/devicelock.qml @@ -2,7 +2,7 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 import Sailfish.Silica.private 1.0 import com.jolla.settings.system 1.0 -import org.nemomobile.dbus 2.0 +import Nemo.DBus 2.0 import org.nemomobile.devicelock 1.0 import org.nemomobile.systemsettings 1.0 @@ -13,6 +13,7 @@ Page { readonly property bool applicationActive: Qt.application.active readonly property bool settingsAvailable: securityCodeSettings.set && deviceLockSettings.authorization.status == Authorization.ChallengeIssued + readonly property var automaticLockingOptions: [-1, 0, 5, 10, 30, 60, 254] function addFingerprint() { pageStack.animatorPush(fingerprintSettings.fingers.count > 0 @@ -40,6 +41,27 @@ Page { } } + function automaticLockingText(minutes) { + if (minutes < 0) { + //% "Not in use" + //: Device locking is disabled (or lock code has not been defined) + return qsTrId("settings_devicelock-me-off") + } + if (minutes == 0) { + //% "No delay" + //: Device is to be locked immediately whenever display turns off + return qsTrId("settings_devicelock-me-on0") + } + if (minutes >= 254) { + //% "Manual" + //: Device is to be locked only when user explicitly locks it + return qsTrId("settings_devicelock-me-on-manual") + } + //% "%n minutes" + //: Device is to be locked automatically after N minutes of inactivity + return qsTrId("settings_devicelock-me-on-minutes", minutes) + } + onApplicationActiveChanged: { if (applicationActive) { deviceLockSettings.authorization.requestChallenge() @@ -78,7 +100,6 @@ Page { } } - onAutomaticLockingChanged: lockingCombobox.currentIndex = lockingCombobox.updateIndex(deviceLockSettings.automaticLocking) onMaximumAttemptsChanged: { attemptsSlider.value = deviceLockSettings.maximumAttempts != -1 ? deviceLockSettings.maximumAttempts : attemptsSlider.maximumValue } @@ -133,7 +154,12 @@ Page { width: parent.width //% "Automatic locking" label: qsTrId("settings_devicelock-la-status_combobox") - currentIndex: updateIndex(deviceLockSettings.automaticLocking) + value: automaticLockingText(deviceLockSettings.automaticLocking) + Binding { + target: lockingCombobox + property: "currentIndex" + value: page.automaticLockingOptions.indexOf(deviceLockSettings.automaticLocking) + } menu: ContextMenu { // If the context menu is opened in a sub-page the transition for opening the @@ -143,44 +169,13 @@ Page { // right. closeOnActivation: false - MenuItem { - //% "Not in use" - text: qsTrId("settings_devicelock-me-off") - visible: deviceLockSettings.maximumAutomaticLocking === -1 - onClicked: lockingCombobox.setAutomaticLocking(-1) - } - MenuItem { - //% "No delay" - text: qsTrId("settings_devicelock-me-on0") - onClicked: lockingCombobox.setAutomaticLocking(0) - } - MenuItem { - //% "5 minutes" - text: qsTrId("settings_devicelock-me-on5") - visible: deviceLockSettings.maximumAutomaticLocking === -1 - || deviceLockSettings.maximumAutomaticLocking >= 5 - onClicked: lockingCombobox.setAutomaticLocking(5) - } - MenuItem { - //% "10 minutes" - text: qsTrId("settings_devicelock-me-on10") - visible: deviceLockSettings.maximumAutomaticLocking === -1 - || deviceLockSettings.maximumAutomaticLocking >= 10 - onClicked: lockingCombobox.setAutomaticLocking(10) - } - MenuItem { - //% "30 minutes" - text: qsTrId("settings_devicelock-me-on30") - visible: deviceLockSettings.maximumAutomaticLocking === -1 - || deviceLockSettings.maximumAutomaticLocking >= 30 - onClicked: lockingCombobox.setAutomaticLocking(30) - } - MenuItem { - //% "60 minutes" - text: qsTrId("settings_devicelock-me-on60") - visible: deviceLockSettings.maximumAutomaticLocking === -1 - || deviceLockSettings.maximumAutomaticLocking >= 60 - onClicked: lockingCombobox.setAutomaticLocking(60) + Repeater { + model: page.automaticLockingOptions + MenuItem { + text: automaticLockingText(modelData) + onClicked: lockingCombobox.setAutomaticLocking(modelData) + visible: deviceLockSettings.maximumAutomaticLocking < 0 || deviceLockSettings.maximumAutomaticLocking >= modelData + } } } @@ -192,29 +187,12 @@ Page { menu.close() } }, function() { - lockingCombobox.currentIndex = lockingCombobox.updateIndex(deviceLockSettings.automaticLocking) if (menu) { menu.close() } }) } } - - function updateIndex(value) { - if (value === -1) { - return 0 - } else if (value === 0) { - return 1 - } else if (value === 5) { - return 2 - } else if (value === 10) { - return 3 - } else if (value === 30) { - return 4 - } else if (value === 60) { - return 5 - } - } } TextSwitch { @@ -231,46 +209,6 @@ Page { } } - TextSwitch { - id: peekSwitch - //% "Allow feeds while locked" - text: qsTrId("settings_devicelock-la-allow_feeds") - //visible: securityCodeSettings.set - visible: false // hidden until JB#27250 has been implemented. - enabled: page.settingsAvailable - automaticCheck: false - checked: deviceLockSettings.peekingAllowed - onClicked: { - page.authenticate(function(authenticationToken) { - deviceLockSettings.setPeekingAllowed(authenticationToken, !checked) - }) - } - } - - TextSwitch { - //: This switch chooses between Digit only keypad (current default behaviour) and new qwerty-keyboard for devicelock - //% "Digit only keypad" - text: qsTrId("settings_devicelock-la-digit_only_keypad") - // [TMP HOTFIX] do not permit alphanum code to new users until proper fix is in place. Contributes to jb#24201 - // Those who already have enabled alphanumeric code right after update10, and want to revert back to numpad, a cmdline tool can be provided - visible: false // securityCodeSettings.set - enabled: page.settingsAvailable - automaticCheck: false - checked: !deviceLockSettings.codeInputIsKeyboard - //: This description how to get digit only keypad back is showed when user has defined non-digit lockcode and he has qwerty enabled - //% "You can only enable when your security code is digit only" - description: !deviceLockSettings.codeCurrentIsDigitOnly ? qsTrId("settings_devicelock-la-busy-description") : "" - onClicked: { - if (deviceLockSettings.codeCurrentIsDigitOnly || checked) { - page.authenticate(function(authenticationToken) { - deviceLockSettings.setInputIsKeyboard(authenticationToken, checked) - }) - } - } - } - - - Column { width: parent.width visible: fingerprintSettings.hasSensor diff --git a/usr/share/jolla-settings/pages/display/OrientationSettings.qml b/usr/share/jolla-settings/pages/display/OrientationSettings.qml new file mode 100644 index 00000000..b220d41e --- /dev/null +++ b/usr/share/jolla-settings/pages/display/OrientationSettings.qml @@ -0,0 +1,57 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import com.jolla.settings 1.0 +import com.jolla.settings.system 1.0 +import org.nemomobile.systemsettings 1.0 + +Column { + property alias orientationLockCombo: orientationLockCombo + + width: parent.width + + SectionHeader { + //% "Orientation" + text: qsTrId("settings_display-he-orientation") + } + + ComboBox { + id: orientationLockCombo + + // postpone change until menu is closed so that transition doesn't happen during orientation change + property int pendingChange: -1 + onCurrentIndexChanged: { + pendingChange = currentIndex + changeTimer.restart() + } + + //% "Orientation" + label: qsTrId("settings_display-la-orientation") + menu: ContextMenu { + onClosed: orientationLockCombo.applyChange() + + Repeater { + model: orientationLockModel + MenuItem { + text: qsTrId(label) + } + } + } + //% "If you want to disable orientation switching temporarily, select the Automatic option and " + //% "keep your finger on the screen while turning the device." + description: qsTrId("settings_display-la-orientation_automatic") + + function applyChange() { + changeTimer.stop() + if (orientationLockCombo.pendingChange >= 0) { + displaySettings.orientationLock = orientationLockModel.get(orientationLockCombo.pendingChange).value + orientationLockCombo.pendingChange = -1 + } + } + + Timer { + id: changeTimer + interval: 1000 + onTriggered: orientationLockCombo.applyChange() + } + } +} diff --git a/usr/share/jolla-settings/pages/display/display.qml b/usr/share/jolla-settings/pages/display/display.qml index aba10fa0..dd4a5204 100644 --- a/usr/share/jolla-settings/pages/display/display.qml +++ b/usr/share/jolla-settings/pages/display/display.qml @@ -5,8 +5,9 @@ import com.jolla.settings.system 1.0 import org.nemomobile.systemsettings 1.0 Page { - SilicaFlickable { + property alias displaySettings: displaySettings + SilicaFlickable { anchors.fill: parent contentHeight: content.height + Theme.paddingLarge @@ -23,8 +24,10 @@ Page { QT_TRID_NOOP("settings_display-me-5_minutes") //% "10 minutes" QT_TRID_NOOP("settings_display-me-10_minutes") - //% "Dynamic" - QT_TRID_NOOP("settings_display-me-dynamic") + //% "1 hour" + QT_TRID_NOOP("settings_display-me-1_hour") + //% "Automatic" + QT_TRID_NOOP("settings_display-me-automatic") //% "Portrait" QT_TRID_NOOP("settings_display-me-portrait") //% "Landscape" @@ -35,6 +38,7 @@ Page { ListModel { id: timeoutModel + ListElement { label: "settings_display-me-15_seconds" value: 15 @@ -55,12 +59,17 @@ Page { label: "settings_display-me-10_minutes" value: 600 } + ListElement { + label: "settings_display-me-1_hour" + value: 3600 + } } ListModel { id: orientationLockModel + ListElement { - label: "settings_display-me-dynamic" + label: "settings_display-me-automatic" value: "dynamic" } ListElement { @@ -146,6 +155,7 @@ Page { checked: displaySettings.inhibitMode === DisplaySettings.InhibitStayOnWithCharger //% "Keep display on while charging" text: qsTrId("settings_display-la-display_on_charger") + visible: deviceInfo.hasFeature(DeviceInfo.FeatureBattery) onClicked: displaySettings.inhibitMode = checked ? DisplaySettings.InhibitOff : DisplaySettings.InhibitStayOnWithCharger //% "Prevent the display from blanking while the charger is connected" @@ -165,50 +175,17 @@ Page { description: qsTrId("settings_display-la-lid_sensor_description") } - SectionHeader { - //% "Orientation" - text: qsTrId("settings_display-he-orientation") - } - - ComboBox { - id: orientationLockCombo - - // postpone change until menu is closed so that transition doesn't happen during orientation change - property int pendingChange: -1 - onCurrentIndexChanged: { - pendingChange = currentIndex - changeTimer.restart() - } - - //% "Orientation" - label: qsTrId("settings_display-la-orientation") - menu: ContextMenu { - onClosed: orientationLockCombo.applyChange() + Loader { + id: orientationLockComboLoader - Repeater { - model: orientationLockModel - MenuItem { - text: qsTrId(label) - } - } - } - //% "If you want to disable orientation switching temporarily, select the Dynamic option and " - //% "keep your finger on the screen while turning the device." - description: qsTrId("settings_display-la-orientation_dynamic") - - function applyChange() { - changeTimer.stop() - if (orientationLockCombo.pendingChange >= 0) { - displaySettings.orientationLock = orientationLockModel.get(orientationLockCombo.pendingChange).value - orientationLockCombo.pendingChange = -1 + function setCurrentIndex(currentIndex) { + if (item && item.orientationLockCombo) { + item.orientationLockCombo.currentIndex = currentIndex } } - Timer { - id: changeTimer - interval: 1000 - onTriggered: orientationLockCombo.applyChange() - } + width: parent.width + source: Qt.resolvedUrl("OrientationSettings.qml") } SectionHeader { @@ -238,8 +215,10 @@ Page { } } } + DisplaySettings { id: displaySettings + function timeoutIndex(value) { for (var i = 0; i < timeoutModel.count; ++i) { if (value <= timeoutModel.get(i).value) { @@ -248,6 +227,7 @@ Page { } return timeoutModel.count-1 } + function orientationLockIndex(value) { for (var i = 0; i < orientationLockModel.count; ++i) { if (value == orientationLockModel.get(i).value) { @@ -256,13 +236,15 @@ Page { } return 0 } + onDimTimeoutChanged: dimCombo.currentIndex = timeoutIndex(dimTimeout) - onOrientationLockChanged: orientationLockCombo.currentIndex = orientationLockIndex(orientationLock) + onOrientationLockChanged: orientationLockComboLoader.setCurrentIndex(orientationLockIndex(orientationLock)) Component.onCompleted: { dimCombo.currentIndex = timeoutIndex(dimTimeout) - orientationLockCombo.currentIndex = orientationLockIndex(orientationLock) + orientationLockComboLoader.setCurrentIndex(orientationLockIndex(orientationLock)) } } + DeviceInfo { id: deviceInfo } diff --git a/usr/share/jolla-settings/pages/encryption/CopyService.qml b/usr/share/jolla-settings/pages/encryption/CopyService.qml new file mode 100644 index 00000000..83c0e28d --- /dev/null +++ b/usr/share/jolla-settings/pages/encryption/CopyService.qml @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2021 Jolla Ltd. + * + * License: Proprietary + */ + +import QtQuick 2.0 +import Nemo.DBus 2.0 +import Nemo.FileManager 1.0 + +DBusInterface { + id: copyService + + bus: DBus.SystemBus + service: "org.sailfishos.HomeCopyService" + path: "/org/sailfishos/HomeCopyService" + iface: "org.sailfishos.HomeCopyService" + signalsEnabled: true + // Prevents automatic introspection but simplifies the code otherwise + watchServiceStatus: true + + // This introspects the interface. Thus, starting the dbus service. + readonly property DBusInterface introspectAtStart: DBusInterface { + bus: DBus.SystemBus + service: copyService.service + path: copyService.path + iface: "org.freedesktop.DBus.Introspectable" + Component.onCompleted: call("Introspect") + } + + + function setCopyDev(dev) { + call("setCopyDevice", [dev]) + } + + function copyHome(dev) { + setCopyDev(dev) + call("copyHome", []) + } + + signal copied(bool success) + + function copyDone(value) { + console.log("SD copied:", value) + copied(value) + } + +} diff --git a/usr/share/jolla-settings/pages/encryption/EncryptionService.qml b/usr/share/jolla-settings/pages/encryption/EncryptionService.qml new file mode 100644 index 00000000..ce5cbf37 --- /dev/null +++ b/usr/share/jolla-settings/pages/encryption/EncryptionService.qml @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2019 Jolla Ltd. + * + * License: Proprietary + */ + +import QtQuick 2.0 +import Nemo.DBus 2.0 +import Nemo.FileManager 1.0 +import Nemo.Configuration 1.0 +import Sailfish.Encryption 1.0 + +DBusInterface { + id: encryptionService + + bus: DBus.SystemBus + service: "org.sailfishos.EncryptionService" + path: "/org/sailfishos/EncryptionService" + iface: "org.sailfishos.EncryptionService" + signalsEnabled: true + // Prevents automatic introspection but simplifies the code otherwise + watchServiceStatus: true + + property string errorString + property string errorMessage + property int encryptionStatus + property bool serviceSeen + readonly property bool encryptionWanted: encryptHome.exists && (status !== DBusInterface.Unavailable || serviceSeen) + readonly property bool available: encryptHome.exists && (status === DBusInterface.Available || serviceSeen) + readonly property bool busy: encryptionWanted && encryptionStatus == EncryptionStatus.Busy + + onStatusChanged: if (status === DBusInterface.Available) serviceSeen = true + + // DBusInterface is a QObject so no child items + property FileWatcher encryptHome: FileWatcher { + id: encryptHome + fileName: "/var/lib/sailfish-device-encryption/encrypt-home" + } + + // This introspects the interface. Thus, starting the dbus service. + readonly property DBusInterface introspectAtStart: DBusInterface { + bus: DBus.SystemBus + service: encryptionService.service + path: encryptionService.path + iface: "org.freedesktop.DBus.Introspectable" + Component.onCompleted: call("Introspect") + } + + onAvailableChanged: { + // Move to busy state right after service is available. So that + // user do not see text change from Idle to Busy (encryption is started + // when we hit the PleaseWaitPage). + if (available) { + encryptionStatus = EncryptionStatus.Busy + } + } + + function encrypt() { + call("BeginEncryption", undefined, + function() { + encryptionStatus = EncryptionStatus.Busy + }, + function(error, message) { + errorString = error + errorMessage = message + encryptionStatus = EncryptionStatus.Error + } + ) + } + + function finalize() { + call("FinalizeEncryption") + } + + function prepare(passphrase, overwriteType) { + call("PrepareToEncrypt", [passphrase, overwriteType]) + } + + function encryptionFinished(success, error) { + encryptionStatus = success ? EncryptionStatus.Encrypted : EncryptionStatus.Error + } +} diff --git a/usr/share/jolla-settings/pages/encryption/HomeEncryptionDisclaimer.qml b/usr/share/jolla-settings/pages/encryption/HomeEncryptionDisclaimer.qml new file mode 100644 index 00000000..013e7260 --- /dev/null +++ b/usr/share/jolla-settings/pages/encryption/HomeEncryptionDisclaimer.qml @@ -0,0 +1,175 @@ +/* + * Copyright (c) 2019 Jolla Ltd. + * + * License: Proprietary + */ + +import QtQuick 2.2 +import Sailfish.Silica 1.0 +import org.nemomobile.devicelock 1.0 +import org.nemomobile.systemsettings 1.0 + +Dialog { + id: dialog + + readonly property int batteryThreshold: 50 + readonly property bool batteryChargeOk: battery.chargePercentage > batteryThreshold + + property EncryptionSettings encryptionSettings + + function createBackupLink() { + //: A link to Settings | System | Backup + //: Action or verb that can be used for %1 in settings_encryption-la-encrypt_user_data_warning and + //: settings_encryption-la-encrypt_user_data_description + //: Strongly proposing user to do a backup. + //% "Back up" + var backup = qsTrId("settings_encryption-la-back_up") + return "" + backup + "" + } + + acceptDestination: "com.jolla.settings.system.MandatoryDeviceLockInputPage" + acceptDestinationAction: PageStackAction.Replace + acceptDestinationProperties: { + "authorization": encryptionSettings.authorization + } + + canAccept: (batteryChargeOk || battery.chargerStatus === BatteryStatus.Connected) + && encryptionSettings + && encryptionSettings.authorization.status === Authorization.ChallengeIssued + + Component.onCompleted: encryptionSettings.authorization.requestChallenge() + + onAccepted: acceptDestinationInstance.authenticate() + + BatteryStatus { + id: battery + } + + SecurityCodeSettings { + id: securityCode + } + + SilicaFlickable { + contentHeight: content.height + anchors.fill: parent + + Column { + id: content + + width: parent.width + + DialogHeader { + dialog: dialog + } + + Item { + id: batteryWarning + + width: parent.width - 2*Theme.horizontalPageMargin + height: Math.max(batteryIcon.height, batteryText.height) + Theme.paddingLarge + x: Theme.horizontalPageMargin + visible: !dialog.batteryChargeOk + + Image { + id: batteryIcon + anchors { + verticalCenter: parent.verticalCenter + verticalCenterOffset: -Theme.paddingLarge / 2 + } + source: "image://theme/icon-l-battery" + } + + Label { + id: batteryText + + anchors { + left: batteryIcon.right + leftMargin: Theme.paddingMedium + right: parent.right + verticalCenter: parent.verticalCenter + verticalCenterOffset: -Theme.paddingLarge / 2 + } + font.pixelSize: Theme.fontSizeMedium + color: Theme.highlightColor + wrapMode: Text.Wrap + text: battery.chargerStatus === BatteryStatus.Connected + ? //: Battery low warning for device encryption when charger is attached. + //% "Battery level low. Do not remove the charger." + qsTrId("settings_encryption-la-battery_charging") + : //: Battery low warning for device encryption when charger is not attached. + //% "Battery level too low." + qsTrId("settings_encryption-la-battery_level_low") + } + } + + Label { + x: Theme.horizontalPageMargin + width: parent.width - 2*Theme.horizontalPageMargin + height: implicitHeight + Theme.paddingLarge + font { + family: Theme.fontFamilyHeading + pixelSize: Theme.fontSizeExtraLarge + } + color: Theme.highlightColor + + //% "Did you remember to backup?" + text: qsTrId("settings_encryption-he-backup_reminder") + wrapMode: Text.Wrap + } + + Label { + x: Theme.horizontalPageMargin + width: parent.width - 2*Theme.horizontalPageMargin + height: implicitHeight + Theme.paddingLarge + font.pixelSize: Theme.fontSizeExtraSmall + color: Theme.highlightColor + linkColor: Theme.primaryColor + textFormat: Text.AutoText + + //: Takes "Back up" (settings_encryption-la-back_up) formatted hyperlink as parameter. + //: This is done because we're creating programmatically a hyperlink for it. + //% "Accepting this will erase all user data on the device. " + //% "This means losing user data that you have added to the device (e.g. reverts apps to clean state, accounts, contacts, photos and other media). " + //% "%1 user data to memory card.

" + //% "This is an irreversible change and you will need to enter a security code on all future boots in order to access your data.

" + //% "If you accept the device will reboot automatically and you will be prompted for the security code before encrypting user data." + text: qsTrId("settings_encryption-la-encrypt_user_data_warning").arg(createBackupLink()) + wrapMode: Text.Wrap + + onLinkActivated: pageStack.animatorPush("Sailfish.Vault.MainPage") + } + + Label { + //% "How do I make a backup and restore it?" + readonly property string title: qsTrId("settings_encryption-la-make_backup_and_restore_it") + readonly property string url: "https://sailfishos.org/article/backup-and-restore" + + x: Theme.horizontalPageMargin + width: parent.width - 2*Theme.horizontalPageMargin + height: implicitHeight + Theme.paddingLarge + font.pixelSize: Theme.fontSizeExtraSmall + color: Theme.highlightColor + linkColor: Theme.primaryColor + textFormat: Text.AutoText + text: "" + title +"" + wrapMode: Text.Wrap + onLinkActivated: Qt.openUrlExternally(link) + } + + Label { + x: Theme.horizontalPageMargin + width: parent.width - 2*Theme.horizontalPageMargin + height: implicitHeight + Theme.paddingLarge + font.pixelSize: Theme.fontSizeExtraSmall + color: Theme.highlightColor + text: securityCode.set + //% "You will need to enter your security code before user data can be encrypted." + ? qsTrId("settings_encryption-la-encrypt_user_data_security_code_notice") + //% "You will need to set a security code before user data can be encrypted." + : qsTrId("settings_encryption-la-encrypt_user_data_create_security_code_notice") + wrapMode: Text.Wrap + } + } + } +} + diff --git a/usr/share/jolla-settings/pages/encryption/LayoutTranslations.qml b/usr/share/jolla-settings/pages/encryption/LayoutTranslations.qml new file mode 100644 index 00000000..5de893ce --- /dev/null +++ b/usr/share/jolla-settings/pages/encryption/LayoutTranslations.qml @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2019 Jolla Ltd. + * + * License: Proprietary + */ + +import QtQuick 2.0 + +Item { + // providing dummy translations that can be used on settings layout files + function qsTrIdString() { + //% "Do you want to encrypt user data?" + QT_TRID_NOOP("settings_encryption-la-encrypt_user_data_confirmation") + + // Restoration UI translations + //% "OK" + QT_TRID_NOOP("settings_encryption-la-ok") + //% "Restoring user data" + QT_TRID_NOOP("settings_encryption-la-restoring-data") + //% "Restoring user data failed" + QT_TRID_NOOP("settings_encryption-la-restore-fail-summary") + //% "Data is kept on memory card" + QT_TRID_NOOP("settings_encryption-la-restore-fail-body") + } +} diff --git a/usr/share/jolla-settings/pages/encryption/SDCopyFailed.qml b/usr/share/jolla-settings/pages/encryption/SDCopyFailed.qml new file mode 100644 index 00000000..47473c1b --- /dev/null +++ b/usr/share/jolla-settings/pages/encryption/SDCopyFailed.qml @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2021 Jolla Ltd. + * + * License: Proprietary + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 + +Page { + Label { + x: Theme.horizontalPageMargin + width: parent.width - 2*x + wrapMode: Text.Wrap + + //% "Copying data failed. Aborting encryption" + text: qsTrId("settings_encryption-la-copy-failed") + color: Theme.errorColor + + } +} diff --git a/usr/share/jolla-settings/pages/encryption/SDCopyPage.qml b/usr/share/jolla-settings/pages/encryption/SDCopyPage.qml new file mode 100644 index 00000000..c039a658 --- /dev/null +++ b/usr/share/jolla-settings/pages/encryption/SDCopyPage.qml @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2021 Jolla Ltd. + * + * License: Proprietary + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Sailfish.Lipstick 1.0 +import Sailfish.Silica.private 1.0 + +Page { + id: sdCopyPage + backNavigation: false + + WindowGestureOverride { + id: gestureOverride + active: true + } + + BusyLabel { + running: true + //% "Saving data to SD card" + text: qsTrId("settings_encryption-la-saving-data") + } +} diff --git a/usr/share/jolla-settings/pages/encryption/encryption.qml b/usr/share/jolla-settings/pages/encryption/encryption.qml new file mode 100644 index 00000000..983c3873 --- /dev/null +++ b/usr/share/jolla-settings/pages/encryption/encryption.qml @@ -0,0 +1,406 @@ +/* + * Copyright (c) 2019 Jolla Ltd. + * Copyright (c) 2019 Open Mobile Platform LLC. + * + * License: Proprietary + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Nemo.DBus 2.0 +import Sailfish.Encryption 1.0 +import org.nemomobile.devicelock 1.0 +import org.nemomobile.systemsettings 1.0 + +Page { + id: page + + // Device lock autentication + + // threshold above which we may reset without charger + readonly property int batteryThreshold: 15 + readonly property bool batteryChargeOk: battery.chargePercentage > batteryThreshold + // To be checked + readonly property bool applicationActive: Qt.application.active + // external storage data + property string selectedDevPath: "tmp" + property bool selectedDevSuitable: false + property bool hasHomeCopy: copyHelper.hasHomeCopyService() + + property EncryptionService encryptionService + property CopyService copyService + + function createBackupLink() { + //: A link to Settings | System | Backup + //: Action or verb that can be used for %1 in settings_encryption-la-encrypt_user_data_warning and + //: settings_encryption-la-encrypt_user_data_description + //: Strongly proposing user to do a backup. + //% "Back up" + var backup = qsTrId("settings_encryption-la-back_up") + return "" + backup + "" + } + + function devPath() { + return sdSwitch.checked ? selectedDevPath : "tmp" + } + + BatteryStatus { + id: battery + } + + USBSettings { + id: usbSettings + } + + CopyHelper { + id: copyHelper + } + + EncryptionSettings { + id: encryptionSettings + onEncryptingHome: lipstick.startEncryptionPreparation() + onEncryptHomeError: console.warn("Home encryption failed. Maybe token expired.") + } + + Component { + id: encryptionServiceComponent + EncryptionService {} + } + + Component { + id: copyServiceComponent + CopyService {} + } + + DBusInterface { + id: dsmeDbus + bus: DBus.SystemBus + service: "com.nokia.dsme" + path: "/com/nokia/dsme/request" + iface: "com.nokia.dsme.request" + } + + DBusInterface { + id: lipstick + bus: DBus.SystemBus + service: "org.nemomobile.lipstick" + path: "/shutdown" + iface: "org.nemomobile.lipstick" + + function startEncryptionPreparation() { + lipstick.call("setShutdownMode", ["reboot"], + function(success) { + prepareEncryption.running = true + }, + function(error, message) { + console.info("Error occured when entering to reboot mode:", error, "message:", message) + } + ) + } + } + + Timer { + id: prepareEncryption + + property string securityCode + + interval: 3000 + onTriggered: { + page.encryptionService = encryptionServiceComponent.createObject(root) + page.encryptionService.prepare(securityCode, "zero") + } + } + + SilicaFlickable { + anchors.fill: parent + contentHeight: content.height + + VerticalScrollDecorator {} + + Column { + id: content + width: parent.width + spacing: Theme.paddingLarge + + PageHeader { + title: encryptionSettings.homeEncrypted + ? //% "Encryption information" + qsTrId("settings_encryption-he-encryption_information") + : //% "Encryption" + qsTrId("settings_encryption-he-encryption") + } + + Item { + id: batteryWarning + + width: parent.width - 2*Theme.horizontalPageMargin + height: Math.max(batteryIcon.height, batteryText.height) + x: Theme.horizontalPageMargin + visible: !page.batteryChargeOk && !encryptionSettings.homeEncrypted + + Image { + id: batteryIcon + anchors.verticalCenter: parent.verticalCenter + source: "image://theme/icon-l-battery" + } + + Label { + id: batteryText + + anchors { + left: batteryIcon.right + leftMargin: Theme.paddingMedium + right: parent.right + verticalCenter: parent.verticalCenter + } + font.pixelSize: Theme.fontSizeMedium + color: Theme.highlightColor + wrapMode: Text.Wrap + text: battery.chargerStatus === BatteryStatus.Connected + ? //: Battery low warning for device reset when charger is attached. Same as settings_reset-la-battery_charging + //% "Battery level low. Do not remove the charger." + qsTrId("settings_encryption-la-battery_charging") + : //: Battery low warning for device reset when charger is not attached. Same as settings_reset-la-battery_level_low + //% "Battery level too low." + qsTrId("settings_encryption-la-battery_level_low") + } + } + + Item { + id: mtpWarning + + width: parent.width - 2*Theme.horizontalPageMargin + height: Math.max(mtpIcon.height, mtpText.height) + x: Theme.horizontalPageMargin + visible: usbSettings.currentMode == usbSettings.MODE_MTP && !encryptionSettings.homeEncrypted + + Image { + id: mtpIcon + anchors.verticalCenter: parent.verticalCenter + source: "image://theme/icon-m-usb" + } + + Label { + id: mtpText + + anchors { + left: mtpIcon.right + leftMargin: Theme.paddingMedium + right: parent.right + verticalCenter: parent.verticalCenter + } + font.pixelSize: Theme.fontSizeMedium + color: Theme.highlightColor + wrapMode: Text.Wrap + text: //: USB MTP mode disconnect warning + //% "Media transfer (MTP) will be disconnected." + qsTrId("settings_encryption-la-mtp_disconnect") + } + } + + Label { + x: Theme.horizontalPageMargin + width: parent.width - 2*Theme.horizontalPageMargin + wrapMode: Text.Wrap + font.pixelSize: Theme.fontSizeSmall + color: Theme.highlightColor + linkColor: Theme.primaryColor + textFormat: Text.AutoText + visible: !encryptionSettings.homeEncrypted + + //: Takes "Back up" (settings_encryption-la-back_up) formatted hyperlink as parameter. + //: This is done because we're creating programmatically a hyperlink for it. + //% "This will erase all user data from the device. " + //% "This means losing user data that you have added to the device, reverts apps to clean state, accounts, contacts, photos and other media.

" + //% "%1 user data to memory card before encrypting the device." + text: qsTrId("settings_encryption-la-encrypt_user_data_description").arg(createBackupLink()) + + onLinkActivated: pageStack.animatorPush("Sailfish.Vault.MainPage") + } + + Button { + anchors.horizontalCenter: parent.horizontalCenter + preferredWidth: Theme.buttonWidthMedium + visible: !encryptionSettings.homeEncrypted + + //% "Encrypt" + text: qsTrId("settings_encryption-bt-encrypt") + onClicked: { + var obj = pageStack.animatorPush(Qt.resolvedUrl("HomeEncryptionDisclaimer.qml"), { + "encryptionSettings": encryptionSettings + }) + var mandatoryDeviceLock + obj.pageCompleted.connect(function(p) { + p.accepted.connect(function() { + mandatoryDeviceLock = p.acceptDestinationInstance + p.acceptDestinationInstance.authenticated.connect(function(authenticationToken) { + prepareEncryption.securityCode = mandatoryDeviceLock.securityCode + if (hasHomeCopy) + page.copyService = copyServiceComponent.createObject(root) + if (sdSwitch.checked) { + var copyPage = pageStack.animatorPush(Qt.resolvedUrl("SDCopyPage.qml")) + page.copyService.copyHome(selectedDevPath) + page.copyService.copied.connect(function(success) { + if (success) { + encryptionSettings.encryptHome(authenticationToken) + } else { + pageStack.pop(page) + completeAnimation() + pageStack.animatorPush(Qt.resolvedUrl("SDCopyFailed.qml")) + page.copyService.setCopyDev("") + } + }) + } else { + if (hasHomeCopy) + page.copyService.setCopyDev("") + encryptionSettings.encryptHome(authenticationToken) + } + }) + p.acceptDestinationInstance.canceled.connect(function() { + pageStack.pop(page) + }) + }) + p.canceled.connect(function() { + pageStack.pop(page) + }) + }) + } + enabled: (page.batteryChargeOk || battery.chargerStatus === BatteryStatus.Connected) + && !encryptionSettings.homeEncrypted + && (!sdSwitch.checked || selectedDevSuitable) + } + TextSwitch { + id: sdSwitch + //% "Copy user data to memory card" + text: qsTrId("settings_encryption-la-use_card") + //% "An encrypted memory card can be used to keep user data." + description: qsTrId("settings_encryption-la-use_card_description") + visible: !encryptionSettings.homeEncrypted && hasHomeCopy && copyHelper.memorycard + } + + ComboBox { + id: sdComboBox + enabled: sdSwitch.checked + visible: !encryptionSettings.homeEncrypted && hasHomeCopy && copyHelper.memorycard + //% "Encrypt using:" + label: qsTrId("settings_encryption-la-encrypt_using") + + menu: ContextMenu { + id: sdMenu + property bool firstUpdate: true + + Repeater { + id: sdRepeater + model: PartitionModel { + id: partitionModel + storageTypes: PartitionModel.External | PartitionModel.ExcludeParents + } + + MenuItem { + //% "Memory card: (%1)" + text: qsTrId("settings_encryption-memory_card").arg(Format.formatFileSize(bytesTotal)) + onClicked: { + update() + } + + Component.onCompleted: { + if (sdMenu.firstUpdate) { + sdMenu.firstUpdate = false + update() + } + } + function update() { + page.selectedDevSuitable = ((copyHelper.homeBytes() < bytesFree) && (mountPath !== "") + && copyHelper.checkWritable(mountPath)) + + sdComboBox.description = descriptionString((copyHelper.homeBytes() < bytesFree), (mountPath !== ""), + copyHelper.checkWritable(mountPath), isCryptoDevice) + page.selectedDevPath = devicePath + } + + function descriptionString(homeFits, mounted, writable, encrypted) { + if (!mounted) { //% "Card must be mounted" + return qsTrId("settings_encryption-la-card_not_mounted") + } else if (!homeFits) { //% "User data doesn't fit SD card" + return qsTrId("settings_encryption-la-data_doesnt_fit_to_card") + } else if (!writable) { //% "Selected card unwritable" + return qsTrId("settings_encryption-la-card_unwritable") + } else if (!encrypted) { //% "Card must be encrypted" + return qsTrId("settings_encryption-la-card_not_encrypted") + } else { + return "" + } + } + } + } + } + descriptionColor: Theme.errorColor + description: "" + } + + Column { + id: homeInfoColumn + width: parent.width + visible: encryptionSettings.homeEncrypted + + Label { + x: Theme.horizontalPageMargin + width: parent.width - 2*x + wrapMode: Text.Wrap + font.pixelSize: Theme.fontSizeSmall + color: Theme.highlightColor + + //: Shown in the Settings -> Encryption page when user data is already encrypted. + //% "Your user data is encrypted which means that only authorized users can access it. Users are authenticated with security code." + text: qsTrId("settings_encryption-la-encryption_user_data_description") + } + + SectionHeader { + //% "User data" + text: qsTrId("settings_encryption-la-encryption_user_data") + } + + DetailItem { + //% "Encryption" + label: qsTrId("settings_encryption-la-encryption") + //% "Enabled" + value: qsTrId("settings_encryption-la-enabled") + } + + DetailItem { + visible: homeInfo.type + //% "Device type" + label: qsTrId("settings_encryption-la-device_type") + value: homeInfo.type + } + + DetailItem { + visible: homeInfo.version + //% "Version" + label: qsTrId("settings_encryption-la-version") + value: homeInfo.version + } + + DetailItem { + visible: homeInfo.size > 0 + //% "Size" + label: qsTrId("settings_encryption-la-size") + value: Format.formatFileSize(homeInfo.size) + } + } + + Loader { + id: homeInfo + + readonly property string type: item && item.type || "" + readonly property string version: item && item.version || "" + readonly property double size: item && item.size || 0 + + active: encryptionSettings.homeEncrypted + sourceComponent: Component { + HomeInfo {} + } + } + } + } +} diff --git a/usr/share/jolla-settings/pages/ethernet/AddNetworkDialog.qml b/usr/share/jolla-settings/pages/ethernet/AddNetworkDialog.qml new file mode 100644 index 00000000..e544c40c --- /dev/null +++ b/usr/share/jolla-settings/pages/ethernet/AddNetworkDialog.qml @@ -0,0 +1,43 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Sailfish.Pickers 1.0 +import Sailfish.Settings.Networking 1.0 +import Connman 0.2 + +Dialog { + id: root + + property NetworkManager networkManager + canAccept: true + + property string path + onAccepted: { + root.forceActiveFocus() // proxy and ipv4 fields update on focus lost + path = networkManager.createServiceSync(network.json()) + } + + + SilicaFlickable { + anchors.fill: parent + contentHeight: column.height + Theme.paddingLarge + Column { + id: column + + width: parent.width + DialogHeader { + dialog: root + //% "Add network" + title: qsTrId("settings_network-he-ethernet-add_network") + //% "Save" + acceptText: qsTrId("settings_network-he-ethernet-save") + } + + AdvancedSettingsColumn { + id: advancedSettingsColumn + network: root.network + } + } + + VerticalScrollDecorator {} + } +} diff --git a/usr/share/jolla-settings/pages/ethernet/AdvancedSettingsPage.qml b/usr/share/jolla-settings/pages/ethernet/AdvancedSettingsPage.qml new file mode 100644 index 00000000..7e0242e1 --- /dev/null +++ b/usr/share/jolla-settings/pages/ethernet/AdvancedSettingsPage.qml @@ -0,0 +1,76 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Connman 0.2 +import Sailfish.Pickers 1.0 +import Sailfish.Settings.Networking 1.0 +import "../netproxy" + +Dialog { + id: root + + forwardNavigation: false + canNavigateForward: false + + property QtObject network + + SilicaFlickable { + anchors.fill: parent + contentHeight: content.height + Theme.paddingLarge + + PullDownMenu { + MenuItem { + //% "Forget network" + text: qsTrId("settings_network-me-ethernet-forget_network") + enabled: root.network + onClicked: { + var network = root.network + pageStack.pop() + network.autoConnect = false; + network.requestDisconnect() + network.remove() + root.network = null + } + } + } + + Column { + id: content + + width: parent.width + + DialogHeader { + id: dialogHeader + acceptText: "" + + //% "Save" + cancelText: qsTrId("settings_network-he-ethernet-save") + + Label { + parent: dialogHeader.extraContent + text: root.network ? root.network.name : "Testing testing" + color: Theme.highlightColor + width: parent.width + truncationMode: TruncationMode.Fade + font { + pixelSize: Theme.fontSizeLarge + family: Theme.fontFamilyHeading + } + anchors { + right: parent.right + rightMargin: -Theme.horizontalPageMargin + verticalCenter: parent.verticalCenter + } + + horizontalAlignment: Qt.AlignRight + } + } + + AdvancedSettingsColumn { + id: advancedSettingsColumn + network: root.network + globalProxyConfigPage: Qt.resolvedUrl("../advanced-networking/mainpage.qml") + } + } + VerticalScrollDecorator {} + } +} diff --git a/usr/share/jolla-settings/pages/ethernet/EthernetItem.qml b/usr/share/jolla-settings/pages/ethernet/EthernetItem.qml new file mode 100644 index 00000000..00840c6a --- /dev/null +++ b/usr/share/jolla-settings/pages/ethernet/EthernetItem.qml @@ -0,0 +1,117 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Sailfish.Settings.Networking 1.0 + +ListItem { + id: root + + property bool connected: networkService.state === "online" || (ready && connectCompletionTimer.running) + property bool ready: networkService.state === "ready" + property string previousState + property string currentState: networkService.state + + function getText(state) { + if (!root.enabled) { + return "" + } else if (connected) { + //% "Connected" + return qsTrId("settings_network-la-ethernet-connected_state") + } else if (state === "ready") { + //% "Limited connectivity" + return qsTrId("settings_network-la-ethernet-limited_state") + } else if (previousState === "online" && state === "association") { + // need previous state as well + // as connman signals 'association' on disconnect as well + //% "Disconnecting..." + return qsTrId("settings_network-la-ethernet-disconnecting_state") + } else if (state === "association" || state === "configuration") { + //% "Connecting..." + return qsTrId("settings_network-la-ethernet-connecting_state") + } else { + //% "Idle state" + return qsTrId("settings_network-la-ethernet-idle_state") + } + } + + enabled: !managed + contentHeight: textSwitch.height + highlighted: textSwitch.down || menuOpen || connected || ready + visible: networkService.type === "ethernet" + _backgroundColor: "transparent" + openMenuOnPressAndHold: false + menu: Component { + ContextMenu { + MenuItem { + //% "Connect" + text: qsTrId("settings_network-me-ethernet-connect") + visible: !networkService.connected && networkService.available + onClicked: networkService.requestConnect() + } + MenuItem { + //% "Disconnect" + text: qsTrId("settings_network-me-ethernet-disconnect") + visible: networkService.connected && !networkService.autoConnect + onClicked: networkService.requestDisconnect() + } + // The entry will be re-created by ConnMan as non-saved when cleared + // TODO: We may need to devise means to remove the others that are + // not tied to the particular adapter. + MenuItem { + //% "Clear settings" + text: qsTrId("settings_network-me-ethernet-clear-settings") + + onClicked: { + var network = networkService + //% "Cleared settings" + remorseAction(qsTrId("settings_network-la-ethernet-cleared-settings"), + function () { + network.autoConnect = false; + network.requestDisconnect() + network.remove() + }) + } + } + MenuItem { + //% "Details" + text: qsTrId("settings_network-me-ethernet-details") + onClicked: pageStack.animatorPush("NetworkDetailsPage.qml", {"network": networkService}) + } + + onActiveChanged: mainPage.suppressScan = active + } + } + + onCurrentStateChanged: { + if (previousState === "configuration" && currentState === "ready") + connectCompletionTimer.start() + else + connectCompletionTimer.stop() + + textSwitch.description = getText(currentState) + previousState = currentState + } + + ListView.onRemove: animateRemoval() + Component.onCompleted: textSwitch.description = getText(currentState) + + IconTextSwitch { + id: textSwitch + + enabled: root.enabled + icon.source: "image://theme/icon-m-lan" + automaticCheck: false + checked: networkService.autoConnect + highlighted: root.highlighted + text: networkService.name + onClicked: networkService.autoConnect = !networkService.autoConnect + onPressAndHold: root.openMenu() + } + + Timer { + id: connectCompletionTimer + + interval: 12000 + repeat: false + onTriggered: textSwitch.description = getText(currentState) + } +} diff --git a/usr/share/jolla-settings/pages/ethernet/EthernetSwitch.qml b/usr/share/jolla-settings/pages/ethernet/EthernetSwitch.qml new file mode 100644 index 00000000..712672fc --- /dev/null +++ b/usr/share/jolla-settings/pages/ethernet/EthernetSwitch.qml @@ -0,0 +1,96 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Sailfish.Policy 1.0 +import Sailfish.Settings.Networking 1.0 as Networking +import Connman 0.2 +import Nemo.Configuration 1.0 +import Nemo.DBus 2.0 +import com.jolla.connection 1.0 +import com.jolla.settings 1.0 + +SettingsToggle { + id: ethernetSwitch + + //% "Ethernet" + name: qsTrId("settings_network-la-ethernet") + activeText: networkManager.connectedEthernet ? networkManager.connectedEthernet.name : "" + icon.source: active && networkManager.connectedEthernet + ? "image://theme/icon-m-lan" : "image://theme/icon-m-lan" + + available: AccessPolicy.ethernetToggleEnabled + active: ethernetTechnology && ethernetTechnology.connected + checked: ethernetTechnology.powered + busy: busyTimer.running + + menu: ContextMenu { + SettingsMenuItem { + onClicked: ethernetSwitch.goToSettings() + } + + MenuItem { + //% "Connect to internet" + text: qsTrId("settings_network-me-ethernet-connect_to_internet") + onClicked: connectionSelector.openConnection() + } + } + + onToggled: { + // No accesspolicy for ethernet yet + //if (!AccessPolicy.ethernetToggleEnabled) { + // errorNotification.notify(SettingsControlError.BlockedByAccessPolicy) + if (networkManager.technologiesList().indexOf("ethernet") < 0) { + errorNotification.notify(SettingsControlError.NoEthernetDevice) + } else { + ethernetTechnology.powered = !ethernetTechnology.powered + if (ethernetTechnology.powered) { + busyTimer.stop() + } else if (connDialogConfig.rise) { + busyTimer.restart() + } + } + } + + Timer { + id: busyTimer + interval: connDialogConfig.scanningWait + onTriggered: connectionSelector.openConnection() + onRunningChanged: { + if (running) { + ethernetTechnology.connectedChanged.connect(stop) + } else { + ethernetTechnology.connectedChanged.disconnect(stop) + } + } + } + + ConfigurationGroup { + id: connDialogConfig + + // TODO: separate for ethernet? + path: "/apps/jolla-settings/wlan_fav_switch_connection_dialog" + + property bool rise: true + property int scanningWait: 5000 + } + + NetworkTechnology { + id: ethernetTechnology + path: networkManager.EthernetTechnology + } + + NetworkManager { id: networkManager } + + ConnectionAgent { id: connectionAgent } + + DBusInterface { + id: connectionSelector + + service: "com.jolla.lipstick.ConnectionSelector" + path: "/" + iface: "com.jolla.lipstick.ConnectionSelectorIf" + + function openConnection() { + call('openConnectionNow', 'ethernet') + } + } +} diff --git a/usr/share/jolla-settings/pages/ethernet/NetworkDetailsPage.qml b/usr/share/jolla-settings/pages/ethernet/NetworkDetailsPage.qml new file mode 100644 index 00000000..daf04499 --- /dev/null +++ b/usr/share/jolla-settings/pages/ethernet/NetworkDetailsPage.qml @@ -0,0 +1,112 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Connman 0.2 +import Sailfish.Settings.Networking 1.0 + +Page { + id: detailsPage + property QtObject network + allowedOrientations: Orientation.All + + SilicaFlickable { + anchors.fill: parent + contentHeight: column.height + Theme.paddingLarge + + PullDownMenu { + MenuItem { + //% "Edit" + text: qsTrId("settings_network-me-ethernet-edit") + onClicked: pageStack.animatorPush("AdvancedSettingsPage.qml", {"network": network}) + } + } + + Column { + id: column + width: parent.width + + PageHeader { + title: network.name + } + SectionHeader { + text: { + switch (network.state) { + case "online": + //% "Connected" + return qsTrId("settings_network-la-ethernet-connected_state") + case "ready": + //% "Limited connectivity" + return qsTrId("settings_network-la-ethernet-limited_state") + default: + //% "Not connected" + return qsTrId("settings_network-la-ethernet-not_connected") + } + } + } + + DetailItem { + //% "Hardware Address" + label: qsTrId("settings_network-la-ethernet-hardware_address") + value: network.ethernet["Address"] || "-" + } + + // Ethernet does not yet have setting for this but could be added later on + DetailItem { + property real speed: network.maxRate/1000000.0 + property string speedString: (speed).toLocaleString(Qt.locale(), 'f', 1) + + //% "Maximum speed" + label: qsTrId("settings_network-la-ethernet-maximum_speed") + + //: Megabits per second + //% "%1 Mb/s" + value: qsTrId("settings_network-la-ethernet-megabits_per_second").arg(speedString) + visible: speed != 0.0 + } + + Column { + width: parent.width + visible: network.ipv4["Address"] !== undefined || network.ipv6["Address"] !== undefined + + SectionHeader { + //% "Addresses" + text: qsTrId("settings_network-la-ethernet-addresses") + } + + DetailItem { + //% "IPv4 address" + label: qsTrId("settings_network-la-ethernet-ipv4_address") + value: network.ipv4["Address"] + visible: network.ipv4["Address"] !== undefined + } + + DetailItem { + //% "IPv4 Netmask" + label: qsTrId("settings_network-la-ethernet-ipv4_netmask") + value: network.ipv4["Netmask"] || "-" + visible: network.ipv4["Address"] !== undefined + } + + DetailItem { + //% "IPv4 Gateway" + label: qsTrId("settings_network-la-ethernet-ipv4_gateway") + value: network.ipv4["Gateway"] || "-" + visible: network.ipv4["Address"] !== undefined + } + + DetailItem { + //% "IPv6 address" + label: qsTrId("settings_network-la-ethernet-ipv6_address") + value: network.ipv6["Address"] + "/" + network.ipv6["PrefixLength"] + visible: network.ipv6["Address"] !== undefined + } + + DetailItem { + //% "DNS servers" + label: qsTrId("settings_network-la-ethernet-dns_servers") + value: network.nameservers ? network.nameservers.join("\n") : "-" + } + } + } + VerticalScrollDecorator { } + } +} diff --git a/usr/share/jolla-settings/pages/ethernet/mainpage.qml b/usr/share/jolla-settings/pages/ethernet/mainpage.qml new file mode 100644 index 00000000..d6dd1a02 --- /dev/null +++ b/usr/share/jolla-settings/pages/ethernet/mainpage.qml @@ -0,0 +1,255 @@ +import QtQuick 2.0 +import Connman 0.2 +import Sailfish.Silica 1.0 +import Sailfish.Policy 1.0 +import com.jolla.settings 1.0 +import Sailfish.Settings.Networking 1.0 +import com.jolla.connection 1.0 +import Nemo.DBus 2.0 +import org.nemomobile.systemsettings 1.0 + +Page { + id: mainPage + + property bool suppressScan + property var _errorPlaceholder + property bool showAddNetworkDialog + property bool pageReady + property bool techExists: false + + onStatusChanged: { + if (status == PageStatus.Active) { + pageReady = true + if (showAddNetworkDialog) { + showAddNetworkDialog = false + + var addNetworkProperties = networkHelper.readSettings() + var dialog = pageStack.push(Qt.resolvedUrl("AddNetworkDialog.qml"), { networkManager: networkManager }, PageStackAction.Immediate) + + dialog.accepted.connect(function() { + networkSetupLoader.active = true + networkSetupLoader.item.setup(dialog.path) + }) + } + } + } + + AboutSettings { + id: aboutSettings + } + + AddNetworkHelper { + id: networkHelper + } + + SilicaListView { + id: listView + + anchors.fill: parent + + PullDownMenu { + MenuItem { + // Menu item for opening the advanced network configuration page + //% "Advanced" + text: qsTrId("settings_network-me-ethernet_advanced") + onClicked: pageStack.animatorPush(Qt.resolvedUrl("../advanced-networking/mainpage.qml")) + } + MenuItem { + id: connectMenuItem + //% "Connect to internet" + text: qsTrId("settings_network-me-ethernet-connect_to_internet") + enabled: ethernetListModel.powered + onClicked: connectionSelector.openConnection() + } + } + + header: Column { + width: parent.width + enabled: true //AccessPolicy.ethernetToggleEnabled + + PageHeader { + //% "Ethernet" + title: qsTrId("settings_network-ph-ethernet") + } + + ListItem { + id: enableItem + + visible: techExists && ethernetListModel.available + contentHeight: ethernetSwitch.height + openMenuOnPressAndHold: false + + IconTextSwitch { + id: ethernetSwitch + + property string entryPath: "system_settings/connectivity/ethernet/enable_switch" + + // label + padding + ethernet icon + screen edge padding + automaticCheck: false + icon.source: "image://theme/icon-m-lan" + checked: ethernetListModel.available && ethernetListModel.powered + //% "Ethernet" + text: qsTrId("settings_network-la-ethernet") + enabled: true //AccessPolicy.ethernetToggleEnabled + //% "Fixed ethernet connection" + description: qsTrId("settings_network-la-ethernet_description") //{ +// if (!AccessPolicy.ethernetToggleEnabled) { +// if (checked) { + //: %1 is an operating system name without the OS suffix + //% "Enabled by %1 Device Manager" +// return qsTrId("settings_network-la-ethernet-enabled_by_mdm") +// .arg(aboutSettings.baseOperatingSystemName) + // } else { + //: %1 is an operating system name without the OS suffix + //% "Disabled by %1 Device Manager" +// return qsTrId("settings_network-la-ethernet-disabled_by_mdm") +// .arg(aboutSettings.baseOperatingSystemName) +// } +// } else { +// return "" +// } +// } + + onClicked: { + ethernetListModel.powered = !ethernetListModel.powered + } + } + } + } + + section.property: "managed" + section.delegate: Column { + property bool managed: section === "true" + + width: parent.width + visible: techExists && ethernetListModel.available + enabled: true //AccessPolicy.ethernetToggleEnabled + + SectionHeader { + text: { + if (managed) { + //% "Managed networks" + return qsTrId("settings_network-he-ethernet-managed_networks") + } else { + //% "Saved networks" + return qsTrId("settings_network-he-ethernet-saved_networks") + } + } + } + Label { + visible: techExists && managed + wrapMode: Text.Wrap + color: Theme.secondaryHighlightColor + font.pixelSize: Theme.fontSizeSmall + //: %1 is an operating system name without the OS suffix + //% "Networks added by %1 Device Manager" + text: qsTrId("settings_network-la-ethernet-networks_added_by_mdm") + .arg(aboutSettings.baseOperatingSystemName) + x: Theme.horizontalPageMargin + width: parent.width - 2*x + } + } + + model: (techExists && ethernetListModel.available) ? savedServices : null + + delegate: EthernetItem { width: parent.width } + + // This is shown when there is no ethernet adapter. + // ConnMan requires the adapter device to be present in order to allow + // changes to tech and/or service(s). + Component { + id: errorPlaceholderComponent + ViewPlaceholder { + opacity: (techExists && ethernetListModel.available) || !pageReady ? 0 : 1 + visible: opacity > 0.0 + //% "Ethernet can be managed only when adapter is connected. Please connect adapter." + text: qsTrId("settings_network-la-ethernet_unavailable") + Behavior on opacity { FadeAnimation {} } + } + } + + ViewPlaceholder { + //% "Pull down to connect to internet" + text: qsTrId("settings_network-ph-ethernet-connect") + enabled: ethernetListModel.available && listView.count == 0 && connectMenuItem.enabled && !startupTimer.running + } + + VerticalScrollDecorator {} + } + + Timer { + id: startupTimer + interval: 1000 + running: true + } + + TechnologyModel { + id: ethernetListModel + + name: "ethernet" + + onAvailableChanged: maybeCreateErrorPlaceHolder() + Component.onCompleted: maybeCreateErrorPlaceHolder() + + function maybeCreateErrorPlaceHolder() { + if ((!techExists || !ethernetListModel.available) && !_errorPlaceholder) { + _errorPlaceholder = errorPlaceholderComponent.createObject(listView) + } + } + } + + SavedServiceModel { + id: savedServices + name: "ethernet" + sort: true + groupByCategory: true + } + + NetworkTechnology { + id: ethernetTechnology + path: networkManager.EthernetTechnology + } + + NetworkManager { + id: networkManager + + onTechnologiesChanged: checkEthernet() + + function checkEthernet() { + // NOTE: something caches the values here as staying in the same view + // there always exists a tech with "ethernet" name once it has been + // connected. But when returning to main settings it is detected + // correctly as NULL after removal is done. libconnman-qt does send + // the signal correclty after removing the technology. Problem is + // thus, somewhere in QML caching. + techExists = networkManager.technologiesList().indexOf("ethernet") >= 0 + + if (!techExists && !_errorPlaceholder) { + _errorPlaceholder = errorPlaceholderComponent.createObject(listView) + } + } + } + + ConnectionAgent { id: connectionAgent } + + Loader { + id: networkSetupLoader + sourceComponent: AddNetworkNotifications { + onAvailableChanged: if (available) requestConnect() // a bit broken responsibility... + timeout: true + } + active: false + } + + DBusInterface { + id: connectionSelector + + service: "com.jolla.lipstick.ConnectionSelector" + path: "/" + iface: "com.jolla.lipstick.ConnectionSelectorIf" + + function openConnection() { + call('openConnectionNow', 'ethernet') + } + } +} diff --git a/usr/share/jolla-settings/pages/events/events.qml b/usr/share/jolla-settings/pages/events/events.qml index 71ea44b8..d85164e1 100644 --- a/usr/share/jolla-settings/pages/events/events.qml +++ b/usr/share/jolla-settings/pages/events/events.qml @@ -43,7 +43,8 @@ Page { Label { x: Theme.horizontalPageMargin width: parent.width - 2*x - //: List of Events widgets that can be installed from store. %1 is replaced with a localised concatenation of widget names e.g: "Weather and Calendar". + //: List of Events widgets that can be installed from store. %1 is replaced with + //: a localised concatenation of widget names e.g: "Weather and Calendar". //% "Install %1 from Store." text: qsTrId("settings_events-la-install_from_store").arg(unavailableWidgets.value) visible: unavailableWidgets.value.length > 0 @@ -83,7 +84,8 @@ Page { text: qsTrId("settings_events-la-access_events_when_device_locked") onClicked: lipstickSettings.lock_screen_events = !lipstickSettings.lock_screen_events - //% "When enabled you can check weather, upcoming events and notifications from Lock Screen without unlocking the device" + //% "When enabled you can check upcoming events and notifications " + //% "from Lock Screen without unlocking the device" description: qsTrId("settings_events-la-access_events_when_device_locked_description") ConfigurationGroup { diff --git a/usr/share/jolla-settings/pages/flight/FlightMode.qml b/usr/share/jolla-settings/pages/flight/FlightMode.qml index 70c760cc..7327f516 100644 --- a/usr/share/jolla-settings/pages/flight/FlightMode.qml +++ b/usr/share/jolla-settings/pages/flight/FlightMode.qml @@ -1,6 +1,6 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 -import MeeGo.Connman 0.2 +import Connman 0.2 import Sailfish.Settings.Networking 1.0 import Nemo.KeepAlive 1.2 diff --git a/usr/share/jolla-settings/pages/gestures/gestures.qml b/usr/share/jolla-settings/pages/gestures/gestures.qml index a497246b..3a2cdd58 100644 --- a/usr/share/jolla-settings/pages/gestures/gestures.qml +++ b/usr/share/jolla-settings/pages/gestures/gestures.qml @@ -1,5 +1,5 @@ /* - * Copyright (c) 2013 – 2019 Jolla Ltd. + * Copyright (c) 2013 - 2022 Jolla Ltd. * Copyright (c) 2019 Open Mobile Platform LLC. * * License: Proprietary @@ -10,12 +10,15 @@ import Sailfish.Silica 1.0 import com.jolla.settings 1.0 import com.jolla.settings.system 1.0 import org.nemomobile.systemsettings 1.0 -import org.nemomobile.configuration 1.0 +import Nemo.Configuration 1.0 import org.nemomobile.lipstick 0.1 Page { id: page + readonly property bool hasOrientationSensor: (deviceInfo.hasFeature(DeviceInfo.FeatureAccelerationSensor) + || deviceInfo.hasFeature(DeviceInfo.FeatureGyroSensor)) + readonly property bool flipoverGestureSupported: hasOrientationSensor SilicaFlickable { anchors.fill: parent @@ -58,11 +61,13 @@ Page { } SectionHeader { + visible: flipoverGestureSupported //% "Sensor gestures" text: qsTrId("settings_shortcuts-la-sensor_gestures") } TextSwitch { + visible: flipoverGestureSupported automaticCheck: false checked: displaySettings.flipoverGestureEnabled //% "Flip to silence calls and alarms" @@ -161,4 +166,6 @@ Page { } DisplaySettings { id: displaySettings } + + DeviceInfo { id: deviceInfo } } diff --git a/usr/share/jolla-settings/pages/jolla-camera/SettingsPage.qml b/usr/share/jolla-settings/pages/jolla-camera/SettingsPage.qml index 4ca399bf..80730f16 100644 --- a/usr/share/jolla-settings/pages/jolla-camera/SettingsPage.qml +++ b/usr/share/jolla-settings/pages/jolla-camera/SettingsPage.qml @@ -2,7 +2,7 @@ import QtQuick 2.0 import QtMultimedia 5.0 import Sailfish.Silica 1.0 import com.jolla.camera 1.0 -import org.nemomobile.configuration 1.0 +import Nemo.Configuration 1.0 import org.nemomobile.systemsettings 1.0 import com.jolla.settings 1.0 import com.jolla.settings.system 1.0 diff --git a/usr/share/jolla-settings/pages/jolla-contacts/contactssettings.qml b/usr/share/jolla-settings/pages/jolla-contacts/contactssettings.qml index ab012a22..50615aaa 100644 --- a/usr/share/jolla-settings/pages/jolla-contacts/contactssettings.qml +++ b/usr/share/jolla-settings/pages/jolla-contacts/contactssettings.qml @@ -1,10 +1,10 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 import Sailfish.Telephony 1.0 -import org.nemomobile.configuration 1.0 +import Nemo.Configuration 1.0 import Nemo.DBus 2.0 import org.nemomobile.ofono 1.0 -import MeeGo.QOfono 0.2 +import QOfono 0.2 import com.jolla.contacts.settings 1.0 import com.jolla.settings 1.0 diff --git a/usr/share/jolla-settings/pages/jolla-email/email.qml b/usr/share/jolla-settings/pages/jolla-email/email.qml new file mode 100644 index 00000000..55039f6a --- /dev/null +++ b/usr/share/jolla-settings/pages/jolla-email/email.qml @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2013 – 2019 Jolla Ltd. + * Copyright (c) 2019 – 2020 Open Mobile Platform LLC. + * + * License: Proprietary + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Nemo.Configuration 1.0 +import Nemo.Email 0.1 +import com.jolla.email.settings.translations 1.0 +import com.jolla.settings 1.0 + +ApplicationSettings { + //: Email settings page header + //% "Mail" + applicationName: qsTrId("settings_email-he-email") + + EmailAccountListModel { + id: mailAccountListModel + onlyTransmitAccounts: true + } + + TextSwitch { + //% "Download images automatically" + text: qsTrId("settings_email-la-default_download_images") + //: Description informing the user that downloading images automatically might subject his mailbox to spam + //% "Automatically downloading images might subject your mailbox to spam." + description: qsTrId("settings_email-la-default_download_images_description") + checked: downloadImagesConfig.value + + onCheckedChanged: downloadImagesConfig.value = checked + } + + Loader { + // crypto.qml is installed by the crypto-gnupg subpackage. + active: emailAppCryptoEnabled + source: "crypto.qml" + width: parent.width + } + + ComboBox { + id: readReceiptsPolicy + //% "Send read receipts policy" + label: qsTrId("settings_email-la-default_send_read_receipts") + //: Description informing the user that email client will send read receipts automatically without any additional indications if it was requested by a sender + //% "What should be done when read receipt requested?" + description: qsTrId("settings_email-la-default_send_read_receipts_description") + currentIndex: sendReadReceiptsConfig.value + menu: ContextMenu { + MenuItem { + //% "Always ask" + text: qsTrId("settings_email-la-always_ask_read_receipt") + } + MenuItem { + //% "Always send" + text: qsTrId("settings_email-la-always_send_read_receipt") + } + MenuItem { + //% "Always ignore" + text: qsTrId("settings_email-la-always_ignore_read_receipt") + } + } + + onCurrentIndexChanged: sendReadReceiptsConfig.value = currentIndex + } + + ComboBox { + visible: mailAccountListModel.numberOfAccounts > 1 + currentIndex: Math.max(0, mailAccountListModel.indexFromAccountId(defaultAccountConfig.value)) + //% "Default sending account" + label: qsTrId("settings_email-la-default_sending_account") + menu: ContextMenu { + Repeater { + model: mailAccountListModel + MenuItem { + text: displayName != "" ? displayName : emailAddress + onClicked: defaultAccountConfig.value = mailAccountId + } + } + } + } + + ConfigurationValue { + id: defaultAccountConfig + key: "/apps/jolla-email/settings/default_account" + defaultValue: mailAccountListModel.numberOfAccounts > 1 ? mailAccountListModel.accountId(0) : 0 + } + + ConfigurationValue { + id: downloadImagesConfig + key: "/apps/jolla-email/settings/downloadImages" + defaultValue: false + } + ConfigurationValue { + id: sendReadReceiptsConfig + key: "/apps/jolla-email/settings/sendReadReceipts" + defaultValue: 0 + onValueChanged: { + readReceiptsPolicy.currentIndex = value + } + } +} diff --git a/usr/share/jolla-settings/pages/jolla-messages/SimCardMessagingSettings.qml b/usr/share/jolla-settings/pages/jolla-messages/SimCardMessagingSettings.qml index 4b2d0fcc..e97a2b04 100644 --- a/usr/share/jolla-settings/pages/jolla-messages/SimCardMessagingSettings.qml +++ b/usr/share/jolla-settings/pages/jolla-messages/SimCardMessagingSettings.qml @@ -9,11 +9,11 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 import Sailfish.AccessControl 1.0 import com.jolla.messages.settings.translations 1.0 -import org.nemomobile.configuration 1.0 -import org.nemomobile.notifications 1.0 +import Nemo.Configuration 1.0 +import Nemo.Notifications 1.0 import org.nemomobile.ofono 1.0 -import MeeGo.QOfono 0.2 -import MeeGo.Connman 0.2 +import QOfono 0.2 +import Connman 0.2 Column { id: simCardMessagingSettings diff --git a/usr/share/jolla-settings/pages/jolla-messages/messages.qml b/usr/share/jolla-settings/pages/jolla-messages/messages.qml index c653f467..f19cf580 100644 --- a/usr/share/jolla-settings/pages/jolla-messages/messages.qml +++ b/usr/share/jolla-settings/pages/jolla-messages/messages.qml @@ -1,7 +1,7 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 import Sailfish.Telephony 1.0 -import MeeGo.QOfono 0.2 +import QOfono 0.2 import com.jolla.messages.settings.translations 1.0 import org.nemomobile.ofono 1.0 import com.jolla.settings 1.0 diff --git a/usr/share/jolla-settings/pages/jolla-notes/notes.qml b/usr/share/jolla-settings/pages/jolla-notes/notes.qml new file mode 100644 index 00000000..fc1f6be5 --- /dev/null +++ b/usr/share/jolla-settings/pages/jolla-notes/notes.qml @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2015 - 2016 Jolla Ltd. + * Copyright (C) 2020 Open Mobile Platform LLC. + * + * License: Proprietary + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Nemo.Configuration 1.0 +import com.jolla.settings 1.0 +import com.jolla.notes.settings.translations 1.0 + +ApplicationSettings { + ComboBox { + id: transferFormatCombo + //% "Sharing format" + label: qsTrId("settings_notes-he-sharing_format") + currentIndex: transferAsVNoteConfig.value == false ? 0 : 1 + onCurrentIndexChanged: transferAsVNoteConfig.value = currentIndex == 0 ? false : true + menu: ContextMenu { + id: transferFormatComboMenu + MenuItem { + id: transferAsPTextMenu + //: Whether to transfer notes as plain text files + //% "Plain-text" + text: qsTrId("settings_notes-la-plain-text") + } + MenuItem { + id: transferAsVNoteMenu + //: Whether to transfer notes as vNote files + //% "vNote" + text: qsTrId("settings_notes-la-vnote") + } + } + } + + Label { + id: vnoteWarningLabel + anchors.left: transferFormatCombo.left + anchors.leftMargin: transferFormatCombo.labelMargin + anchors.right: parent.right + anchors.rightMargin: Theme.horizontalPageMargin + visible: !transferFormatComboMenu._open && transferFormatCombo.currentIndex == 1 + color: Theme.secondaryHighlightColor + font.pixelSize: Theme.fontSizeExtraSmall + wrapMode: Text.Wrap + opacity: (!transferFormatComboMenu._open && transferFormatCombo.currentIndex == 1) ? 1.0 : 0.0 + Behavior on opacity { FadeAnimation {} } + height: visible ? implicitHeight : 0 + Behavior on height { NumberAnimation {} } + //: Description informing the user of the disadvantes of using vNote format for sharing + //% "Notes sent in vNote format may not be readable by the recipient" + text: qsTrId("settings_notes-la-vnote_description") + } + + ConfigurationValue { + id: transferAsVNoteConfig + key: "/apps/jolla-notes/settings/transferAsVNote" + defaultValue: false + } +} diff --git a/usr/share/jolla-settings/pages/jolla-voicecall/CallBarring.qml b/usr/share/jolla-settings/pages/jolla-voicecall/CallBarring.qml index 9e1cf405..1164df5f 100644 --- a/usr/share/jolla-settings/pages/jolla-voicecall/CallBarring.qml +++ b/usr/share/jolla-settings/pages/jolla-voicecall/CallBarring.qml @@ -3,7 +3,7 @@ import Sailfish.Silica 1.0 import com.jolla.settings 1.0 import com.jolla.settings.system 1.0 import com.jolla.voicecall.settings.translations 1.0 -import MeeGo.QOfono 0.2 +import QOfono 0.2 Page { id: root diff --git a/usr/share/jolla-settings/pages/jolla-voicecall/CallCounters.qml b/usr/share/jolla-settings/pages/jolla-voicecall/CallCounters.qml index 3d329159..bd95000e 100644 --- a/usr/share/jolla-settings/pages/jolla-voicecall/CallCounters.qml +++ b/usr/share/jolla-settings/pages/jolla-voicecall/CallCounters.qml @@ -1,7 +1,7 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 -import org.nemomobile.dbus 2.0 -import org.nemomobile.time 1.0 +import Nemo.DBus 2.0 +import Nemo.Time 1.0 import com.jolla.voicecall.settings.translations 1.0 Column { diff --git a/usr/share/jolla-settings/pages/jolla-voicecall/CallForwarding.qml b/usr/share/jolla-settings/pages/jolla-voicecall/CallForwarding.qml index 963c8750..a31c2b61 100644 --- a/usr/share/jolla-settings/pages/jolla-voicecall/CallForwarding.qml +++ b/usr/share/jolla-settings/pages/jolla-voicecall/CallForwarding.qml @@ -3,7 +3,7 @@ import Sailfish.Silica 1.0 import com.jolla.settings 1.0 import com.jolla.settings.system 1.0 import com.jolla.voicecall.settings.translations 1.0 -import MeeGo.QOfono 0.2 +import QOfono 0.2 Page { id: root diff --git a/usr/share/jolla-settings/pages/jolla-voicecall/CallSettings.qml b/usr/share/jolla-settings/pages/jolla-voicecall/CallSettings.qml index 4b867e02..c5135955 100644 --- a/usr/share/jolla-settings/pages/jolla-voicecall/CallSettings.qml +++ b/usr/share/jolla-settings/pages/jolla-voicecall/CallSettings.qml @@ -1,7 +1,7 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 import com.jolla.voicecall.settings.translations 1.0 -import MeeGo.QOfono 0.2 +import QOfono 0.2 Item { id: callSettings diff --git a/usr/share/jolla-settings/pages/jolla-voicecall/RecordedCallsPage.qml b/usr/share/jolla-settings/pages/jolla-voicecall/RecordedCallsPage.qml index 967a50ce..19ec78cc 100644 --- a/usr/share/jolla-settings/pages/jolla-voicecall/RecordedCallsPage.qml +++ b/usr/share/jolla-settings/pages/jolla-voicecall/RecordedCallsPage.qml @@ -4,7 +4,7 @@ import Sailfish.Contacts 1.0 import Sailfish.Share 1.0 import org.nemomobile.voicecall 1.0 import org.nemomobile.contacts 1.0 -import org.nemomobile.dbus 2.0 +import Nemo.DBus 2.0 Page { id: root @@ -224,7 +224,7 @@ Page { Label { id: timeStampLabel - text: Format.formatDate(model.modified, Formatter.TimepointRelativeCurrentDay) + text: Format.formatDate(model.modified, Formatter.TimepointRelative) font.pixelSize: Theme.fontSizeExtraSmall anchors.right: parent.right anchors.baseline: parent.baseline diff --git a/usr/share/jolla-settings/pages/jolla-voicecall/SimCardCallSettings.qml b/usr/share/jolla-settings/pages/jolla-voicecall/SimCardCallSettings.qml index 55a238c0..fa84b8e3 100644 --- a/usr/share/jolla-settings/pages/jolla-voicecall/SimCardCallSettings.qml +++ b/usr/share/jolla-settings/pages/jolla-voicecall/SimCardCallSettings.qml @@ -4,8 +4,8 @@ import com.jolla.voicecall.settings.translations 1.0 import Nemo.Configuration 1.0 import org.nemomobile.ofono 1.0 import com.jolla.settings.system 1.0 -import MeeGo.QOfono 0.2 -import MeeGo.Connman 0.2 +import QOfono 0.2 +import Connman 0.2 Column { id: simCallSettings diff --git a/usr/share/jolla-settings/pages/jolla-voicecall/VoiceMail.qml b/usr/share/jolla-settings/pages/jolla-voicecall/VoiceMail.qml index 3c95eb30..75dbd7fa 100644 --- a/usr/share/jolla-settings/pages/jolla-voicecall/VoiceMail.qml +++ b/usr/share/jolla-settings/pages/jolla-voicecall/VoiceMail.qml @@ -1,9 +1,9 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 import com.jolla.voicecall.settings.translations 1.0 -import org.nemomobile.notifications 1.0 +import Nemo.Notifications 1.0 import Nemo.Configuration 1.0 -import MeeGo.QOfono 0.2 +import QOfono 0.2 Column { id: root diff --git a/usr/share/jolla-settings/pages/jolla-voicecall/voicecall.qml b/usr/share/jolla-settings/pages/jolla-voicecall/voicecall.qml index db821b59..7f446d82 100644 --- a/usr/share/jolla-settings/pages/jolla-voicecall/voicecall.qml +++ b/usr/share/jolla-settings/pages/jolla-voicecall/voicecall.qml @@ -14,8 +14,8 @@ import com.jolla.voicecall.settings.translations 1.0 import Nemo.Configuration 1.0 import org.nemomobile.ofono 1.0 import org.nemomobile.systemsettings 1.0 -import MeeGo.QOfono 0.2 -import MeeGo.Connman 0.2 +import QOfono 0.2 +import Connman 0.2 import com.jolla.settings 1.0 ApplicationSettings { diff --git a/usr/share/jolla-settings/pages/keys/DataCorruptionView.qml b/usr/share/jolla-settings/pages/keys/DataCorruptionView.qml new file mode 100644 index 00000000..41276cfd --- /dev/null +++ b/usr/share/jolla-settings/pages/keys/DataCorruptionView.qml @@ -0,0 +1,13 @@ +import QtQuick 2.6 +import Sailfish.Silica 1.0 +import "." + +SecretsResetView { + //: Appears when data corruption has been detected by the secrets daemon. + //% "Data corruption detected. Please reset your secrets data. Affects your keys, collections, etc." + text: qsTrId("secrets_ui-la-data_corruption_detected") + header: PageHeader { + //% "Keys" + title: qsTrId("secrets_ui-he-keys") + } +} diff --git a/usr/share/jolla-settings/pages/keys/SecretsResetView.qml b/usr/share/jolla-settings/pages/keys/SecretsResetView.qml new file mode 100644 index 00000000..32197045 --- /dev/null +++ b/usr/share/jolla-settings/pages/keys/SecretsResetView.qml @@ -0,0 +1,67 @@ +import QtQuick 2.6 +import Sailfish.Silica 1.0 +import Sailfish.Secrets 1.0 +import Sailfish.Secrets.Ui 1.0 +import org.nemomobile.systemsettings 1.0 + +SilicaFlickable { + id: secretsResetView + + property alias text: infoLabel.text + property alias header: colHeader.children + signal success + signal error + signal started + + contentHeight: secretsResetColumn.height + width: parent.width + + SecretsResetter { + id: secretsResetter + + onSuccess: secretsResetView.success() + onError: secretsResetView.error() + } + + Column { + id: secretsResetColumn + width: parent.width + padding: Theme.horizontalPageMargin + topPadding: Theme.paddingLarge + spacing: Theme.paddingLarge + + Item { + id: colHeader + width: parent.width - parent.padding + // Assuming only a single child for this item + height: children.length === 0 ? 0 : children[0].height + } + + Label { + id: infoLabel + wrapMode: Text.Wrap + width: parent.width - parent.padding + color: Theme.highlightColor + font.pixelSize: Theme.fontSizeMedium + + //: Generic information presented to the user about resetting secrets data. + //% "Affects all secrets data, including your keys, collectons, etc." + text: qsTrId("secrets_ui-la-reset_secrets_data") + } + + Button { + //: Button that can clear all the secrets data. + //% "Clear secrets data" + text: qsTrId("secrets_ui-bt-reset_secrets_data") + anchors.horizontalCenter: parent.horizontalCenter + enabled: secretsResetView.enabled + + onClicked: { + secretsResetView.started() + secretsResetter.resetSecretsData() + } + } + } + + VerticalScrollDecorator {} +} diff --git a/usr/share/jolla-settings/pages/keys/SigningSharePage.qml b/usr/share/jolla-settings/pages/keys/SigningSharePage.qml new file mode 100644 index 00000000..a05f243d --- /dev/null +++ b/usr/share/jolla-settings/pages/keys/SigningSharePage.qml @@ -0,0 +1,169 @@ +/**************************************************************************************** +** +** Copyright (c) 2018 - 2021 Jolla Ltd. +** Copyright (c) 2021 Open Mobile Platform LLC. +** All rights reserved. +** +** License: Proprietary. +** +****************************************************************************************/ +import QtQuick 2.6 +import Sailfish.Silica 1.0 +import Sailfish.Secrets 1.0 +import Sailfish.Secrets.Ui 1.0 +import Sailfish.Crypto 1.0 +import Sailfish.FileManager 1.0 as SailfishFileManager +import Sailfish.Gallery 1.0 +import Sailfish.TransferEngine 1.0 +import Sailfish.Share 1.0 +import Sailfish.Lipstick 1.0 +import Nemo.Notifications 1.0 +import Nemo.FileManager 1.0 +import Nemo.DBus 2.0 + +Page { + id: root + + property var shareActionConfiguration + property var _fileToSign + + signal signed + + onSigned: { + signer.shareToEmail(root._fileToSign, signer.getSignaturePath(root._fileToSign)) + } + + Component.onCompleted: { + shareAction.loadConfiguration(shareActionConfiguration) + _fileToSign = shareAction.resources[0] || "" + } + + ShareAction { + id: shareAction + } + + BusyPlaceholder { + id: busyPlaceholder + anchors.centerIn: parent + indicatorSize: BusyIndicatorSize.Large + active: signer.busy + spacing: Theme.paddingMedium + + text: { + if (signer.busy) { + //% "Signing, this might take a while" + return qsTrId("secrets_ui-la_signing_busy_state") + } + return "" + } + } + + ProgressBar { + anchors { + top: busyPlaceholder.bottom + topMargin: Theme.paddingMedium + } + minimumValue: 0 + maximumValue: signer.totalBytesToProcess + value: signer.processedBytes + width: parent.width + visible: opacity > 0 + opacity: signer.busy ? 1.0 : 0.0 + Behavior on opacity { FadeAnimation {}} + } + + CryptoManager { + id: cryptoMgr + } + + SecretManager { + id: secretMgr + } + + Signer { + id: signer + cryptoManager: cryptoMgr + onSigningDone: page.signed() + } + + SecretPluginsModel { + id: secretPlugins + secretManager: secretMgr + filters: SecretPluginsModel.EncryptedStorage + + onError: secretsErrorNotification.show(error) + } + + StorageNotification { + id: storageErrorNotification + } + + SecretsErrorNotification { + id: secretsErrorNotification + } + + SilicaListView { + anchors.fill: parent + enabled: !busyPlaceholder.active && secretPlugins.count > 0 + opacity: enabled ? 1.0 : 0.0 + Behavior on opacity { FadeAnimator { duration: 400 }} + model: !secretPlugins.masterLocked ? secretPlugins : null + + header: Column { + width: parent.width + + PageHeader { + //% "Sign" + title: qsTrId("secrets_ui-he-sign") + } + + SailfishFileManager.FileInfoItem { + fileInfo: FileInfo { url: root._fileToSign } + } + + Label { + //% "Select existing or add new key to use to digitally sign the document" + text: qsTrId("secrets_ui-la_select_or_import_key_to_sign") + font.pixelSize: Theme.fontSizeSmall + x: Theme.horizontalPageMargin + width: parent.width - 2 * x + wrapMode: Text.Wrap + color: Theme.secondaryHighlightColor + //storedKeysModel.count > 0 + visible: false + } + + SectionHeader { + //% "Keys" + text: qsTrId("secrets_ui-he-keys") + } + + MasterLockHeader { + secrets: secretPlugins + } + + Item { + width: 1 + height: Theme.paddingMedium + } + } + + ViewPlaceholder { + //% "Import or generate key to use to sign the document" + text: qsTrId("secrets_ui-la-import_or_generate_key_to_sign") + visible: false + } + + delegate: PluginKeysItem { + populated: secretPlugins.populated + cryptoManager: cryptoMgr + secretManager: secretMgr + onPluginLockCodeRequest: secretPlugins.pluginLockCodeRequest(pluginName, requestType) + onStorageError: storageErrorNotification.show(error) + onError: secretsErrorNotification.show(error) + onClicked: signer.sign(root._fileToSign, key, digest) + } + + VerticalScrollDecorator {} + } +} diff --git a/usr/share/jolla-settings/pages/keys/keys.qml b/usr/share/jolla-settings/pages/keys/keys.qml new file mode 100644 index 00000000..03f3e642 --- /dev/null +++ b/usr/share/jolla-settings/pages/keys/keys.qml @@ -0,0 +1,193 @@ +import QtQuick 2.6 +import org.nemomobile.systemsettings 1.0 +import Sailfish.Silica 1.0 +import Sailfish.Secrets 1.0 as Secrets +import Sailfish.Crypto 1.0 as Crypto +import Sailfish.Secrets.Ui 1.0 + +Page { + id: page + + readonly property bool healthCheckDone: (healthCheckReq.status === Secrets.Request.Finished) && (healthCheckReq.result.code === Secrets.Result.Succeeded) + readonly property alias healthCheckOk: healthCheckReq.isHealthy + + Component.onCompleted: { + healthCheckReq.startRequest() + } + + Crypto.CryptoManager { + id: cryptoMgr + } + + Secrets.SecretManager { + id: secretMgr + } + + StorageNotification { + id: storageErrorNotification + } + + SecretsErrorNotification { + id: secretsErrorNotification + } + + Secrets.HealthCheckRequest { + id: healthCheckReq + manager: secretMgr + + onStatusChanged: { + if (status === Secrets.Request.Finished) { + console.log("salt data health:", saltDataHealth) + console.log("masterlock health:", masterlockHealth) + } + } + onResultChanged: { + if (status === Secrets.Request.Finished) { + if (result.code !== Secrets.Result.Succeeded) { + console.warn("error during health check, resultcode:", result.code, "errorcode:", result.errorCode) + } + + // If this was after a reset, re-enable the reset item + if (dataCorruptionViewLoader.item !== null) { + dataCorruptionViewLoader.item.enabled = true; + } + } + } + } + + Connections { + target: Qt.application + onStateChanged: { + if (Qt.application.state === Qt.ApplicationActive) { + console.log("app activated, starting health check") + healthCheckReq.startRequest() + } + } + } + + SecretPluginsModel { + id: secretPlugins + secretManager: secretMgr + filters: SecretPluginsModel.EncryptedStorage + + onError: secretsErrorNotification.show(error) + } + + InfoLabel { + anchors.centerIn: parent + enabled: !healthCheckDone && (healthCheckReq.status === Secrets.Request.Finished) && (healthCheckReq.result.code !== Secrets.Result.Succeeded) + opacity: enabled ? 1.0 : 0.0 + Behavior on opacity { FadeAnimator { duration: 400 }} + text: { + //: Shown when the initial health check fails when the user opens the Keys page in Settings. + //% "Error during secrets health check." + return qsTrId("secrets_ui-la-healthcheck_error") + } + } + + InfoLabel { + anchors.centerIn: parent + enabled: healthCheckDone && healthCheckOk && (secretPlugins.count === 0) + opacity: enabled ? 1.0 : 0.0 + Behavior on opacity { FadeAnimator { duration: 400 }} + // A bit misleading text is no plugins but IMO close enough for now. + text: { + //% "No keys" + return qsTrId("secrets_ui-la-no_keys") + } + } + + SilicaListView { + anchors.fill: parent + enabled: healthCheckDone && healthCheckOk && (secretPlugins.count > 0) + opacity: enabled ? 1.0 : 0.0 + Behavior on opacity { FadeAnimator { duration: 400 }} + + model: !secretPlugins.masterLocked ? secretPlugins : null + footer: Item { + width: 1 + height: Theme.paddingLarge + } + + InfoLabel { + anchors.verticalCenter: parent.verticalCenter + //% "Oops, something went wrong. No secret storages installed on the device" + text: qsTrId("secrets_ui-la-secrets_ui-la-no_secret_storages") + opacity: secretPlugins.ready && secretPlugins.count === 0 ? 1.0 : 0.0 + Behavior on opacity { FadeAnimator { duration: 400 }} + } + + header: Column { + id: column + + width: parent.width + + PageHeader { + //% "Keys" + title: qsTrId("secrets_ui-he-keys") + } + + MasterLockHeader { + secrets: secretPlugins + } + + Item { + width: 1 + height: Theme.paddingMedium + } + } + + delegate: PluginKeysItem { + populated: secretPlugins.populated + openMenuOnClick: true + editMode: true + cryptoManager: cryptoMgr + secretManager: secretMgr + onPluginLockCodeRequest: secretPlugins.pluginLockCodeRequest(pluginName, requestType) + onStorageError: storageErrorNotification.show(error) + onError: secretsErrorNotification.show(error) + } + + VerticalScrollDecorator {} + } + + Timer { + id: healthCheckTimer + interval: 2000 + onTriggered: { + // This will check if everything is OK and trigger the necessary properties to change, + // so the proper UI will appear after this. + healthCheckReq.startRequest() + } + } + + Loader { + id: dataCorruptionViewLoader + asynchronous: true + enabled: healthCheckDone && !healthCheckOk + active: enabled + opacity: enabled ? 1.0 : 0.0 + Behavior on opacity { FadeAnimator { duration: 400 }} + width: parent.width + source: "DataCorruptionView.qml" + onActiveChanged: { + if (active) { + // Should never destroy the created item once it becomes active + active = true + } + } + onItemChanged: { + if (item !== null) { + item.success.connect(function() { + // Give some time for the secrets service to restart properly before we perform the health check + console.log("secrets data reset successfully, starting health check in", healthCheckTimer.interval, "ms") + healthCheckTimer.start() + }); + item.started.connect(function() { + console.log("secrets data reset started") + item.enabled = false + }); + } + } + } +} diff --git a/usr/share/jolla-settings/pages/lockscreen/lockscreen.qml b/usr/share/jolla-settings/pages/lockscreen/lockscreen.qml index 6d757611..f2f9595c 100644 --- a/usr/share/jolla-settings/pages/lockscreen/lockscreen.qml +++ b/usr/share/jolla-settings/pages/lockscreen/lockscreen.qml @@ -3,7 +3,7 @@ import Sailfish.Silica 1.0 import com.jolla.settings 1.0 import com.jolla.settings.system 1.0 import org.nemomobile.systemsettings 1.0 -import org.nemomobile.configuration 1.0 +import Nemo.Configuration 1.0 import org.nemomobile.lipstick 0.1 Page { diff --git a/usr/share/jolla-settings/pages/mobile/EditMobileNetworkPage.qml b/usr/share/jolla-settings/pages/mobile/EditMobileNetworkPage.qml index eb463f73..6ba2a8d8 100644 --- a/usr/share/jolla-settings/pages/mobile/EditMobileNetworkPage.qml +++ b/usr/share/jolla-settings/pages/mobile/EditMobileNetworkPage.qml @@ -1,5 +1,5 @@ import QtQuick 2.0 -import MeeGo.QOfono 0.2 +import QOfono 0.2 import Sailfish.Silica 1.0 import Sailfish.Policy 1.0 import Sailfish.Settings.Networking 1.0 diff --git a/usr/share/jolla-settings/pages/mobile/ImsStatus.qml b/usr/share/jolla-settings/pages/mobile/ImsStatus.qml index f902010e..4edf5242 100644 --- a/usr/share/jolla-settings/pages/mobile/ImsStatus.qml +++ b/usr/share/jolla-settings/pages/mobile/ImsStatus.qml @@ -1,6 +1,6 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 -import MeeGo.QOfono 0.2 +import QOfono 0.2 import Sailfish.Settings.Networking 1.0 Column { @@ -11,7 +11,7 @@ Column { checked: ims.registration > OfonoIpMultimediaSystem.RegistrationDisabled //: Text switch that controls whether the 4G voice calls are possible - //% "4G calling (beta)" + //% "4G calling (VoLTE)" text: qsTrId("settings_network-bt-4g_voicecall") //% "Registered" diff --git a/usr/share/jolla-settings/pages/mobile/NetworkItemDelegate.qml b/usr/share/jolla-settings/pages/mobile/NetworkItemDelegate.qml index f5102a81..7571a288 100644 --- a/usr/share/jolla-settings/pages/mobile/NetworkItemDelegate.qml +++ b/usr/share/jolla-settings/pages/mobile/NetworkItemDelegate.qml @@ -1,6 +1,6 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 -import MeeGo.QOfono 0.2 +import QOfono 0.2 ListItem { id: root diff --git a/usr/share/jolla-settings/pages/mobile/SelectNetworkPage.qml b/usr/share/jolla-settings/pages/mobile/SelectNetworkPage.qml index a3c860d2..1ea51da3 100644 --- a/usr/share/jolla-settings/pages/mobile/SelectNetworkPage.qml +++ b/usr/share/jolla-settings/pages/mobile/SelectNetworkPage.qml @@ -1,5 +1,5 @@ import QtQuick 2.0 -import MeeGo.QOfono 0.2 +import QOfono 0.2 import Sailfish.Silica 1.0 import Sailfish.Settings.Networking 1.0 import com.jolla.settings 1.0 diff --git a/usr/share/jolla-settings/pages/mobile/SimMobileNetworkSettings.qml b/usr/share/jolla-settings/pages/mobile/SimMobileNetworkSettings.qml index f74a95a4..d940ee46 100644 --- a/usr/share/jolla-settings/pages/mobile/SimMobileNetworkSettings.qml +++ b/usr/share/jolla-settings/pages/mobile/SimMobileNetworkSettings.qml @@ -1,6 +1,6 @@ import QtQuick 2.0 -import MeeGo.QOfono 0.2 -import MeeGo.Connman 0.2 +import QOfono 0.2 +import Connman 0.2 import Sailfish.Silica 1.0 import Sailfish.Policy 1.0 import Sailfish.Settings.Networking 1.0 @@ -240,6 +240,13 @@ Column { menu: ContextMenu { id: networkModeMenu + MenuItem { + readonly property string tech: "nr" + //: Network mode settings ComboBox item for preferring 5G + //% "Prefer 5G" + text: qsTrId("settings_network-me-network_mode_prefer_5G") + visible: radioSettings.availableTechnologies.indexOf(tech) >= 0 + } MenuItem { readonly property string tech: "lte" //: Network mode settings ComboBox item for preferring 4G/LTE diff --git a/usr/share/jolla-settings/pages/mobile/mainpage.qml b/usr/share/jolla-settings/pages/mobile/mainpage.qml index f4308e22..5c558288 100644 --- a/usr/share/jolla-settings/pages/mobile/mainpage.qml +++ b/usr/share/jolla-settings/pages/mobile/mainpage.qml @@ -1,12 +1,12 @@ import QtQuick 2.0 -import MeeGo.QOfono 0.2 +import QOfono 0.2 import Sailfish.Silica 1.0 import Sailfish.Policy 1.0 import Sailfish.Telephony 1.0 import com.jolla.settings.system 1.0 import org.nemomobile.ofono 1.0 import Sailfish.Settings.Networking 1.0 -import MeeGo.Connman 0.2 +import Connman 0.2 Page { id: root @@ -45,7 +45,7 @@ Page { SimActivationPullDownMenu { id: pullDownMenu - multiSimManager: sailfishSimManager + enabled: !disabledByMdmBanner.active visible: true diff --git a/usr/share/jolla-settings/pages/nfc/NfcConfig.qml b/usr/share/jolla-settings/pages/nfc/NfcConfig.qml new file mode 100644 index 00000000..42f3e2e5 --- /dev/null +++ b/usr/share/jolla-settings/pages/nfc/NfcConfig.qml @@ -0,0 +1,109 @@ +import QtQml 2.0 +import Nemo.DBus 2.0 + +QtObject { + id: nfc + + property bool nfcEnabled + property bool nfcBluetoothStaticHandoverEnabled + property bool nfcBluetoothStaticHandoverSupported + property bool busy: changeTimer.running + property bool neardBusy: neardChangeTimer.running + + function refreshSettings() { + nfcSettingsDbus.getEnabled() + neardSettingsDbus.getBluetoothStaticHandover() + } + + function toggleNfcEnabled() { + nfcSettingsDbus.setEnabled(!nfcEnabled) + } + + function toggleNfcBluetoothStaticHandoverEnabled() { + neardSettingsDbus.setBluetoothStaticHandover(!nfcBluetoothStaticHandoverEnabled) + } + + property Timer changeTimer: Timer { + interval: 2000 + } + + property Timer neardChangeTimer: Timer { + interval: 2000 + } + + property QtObject nfcSettingsDbus: DBusInterface { + bus: DBus.SystemBus + service: 'org.sailfishos.nfc.settings' + path: '/' + iface: 'org.sailfishos.nfc.Settings' + signalsEnabled: true + + function enabledChanged(enabled) { + changeTimer.stop() + nfc.nfcEnabled = enabled + } + + function getEnabled() { + changeTimer.restart() + call("GetEnabled", undefined, function (enabled) { + // Success state + changeTimer.stop() + nfc.nfcEnabled = enabled + }, function() { + // Failure state + nfcEnabled = false + changeTimer.stop() + }) + } + + function setEnabled(enabled) { + changeTimer.restart() + call("SetEnabled", enabled, undefined, function () { + // Failure state + nfcEnabled = false + changeTimer.stop() + }) + } + } + + property QtObject neardSettingsDbus: DBusInterface { + bus: DBus.SystemBus + service: 'org.neard' + path: '/' + iface: 'org.sailfishos.neard.Settings' + signalsEnabled: true + + function bluetoothStaticHandoverChanged(enabled) { + neardChangeTimer.stop() + nfc.nfcBluetoothStaticHandoverEnabled = enabled + } + + function getBluetoothStaticHandover() { + neardChangeTimer.restart() + call("GetBluetoothStaticHandover", undefined, function (enabled) { + // Success state + neardChangeTimer.stop() + nfc.nfcBluetoothStaticHandoverEnabled = enabled + nfcBluetoothStaticHandoverSupported = true + }, function() { + // Failure state + nfcBluetoothStaticHandoverEnabled = false + nfcBluetoothStaticHandoverSupported = false + neardChangeTimer.stop() + }) + } + + function setBluetoothStaticHandover(enabled) { + neardChangeTimer.restart() + call("SetBluetoothStaticHandover", enabled, undefined, function () { + // Failure state + nfcBluetoothStaticHandoverEnabled = false + neardChangeTimer.stop() + }) + } + } + + Component.onCompleted: { + refreshSettings() + } +} diff --git a/usr/share/jolla-settings/pages/nfc/NfcSwitch.qml b/usr/share/jolla-settings/pages/nfc/NfcSwitch.qml new file mode 100644 index 00000000..643777bb --- /dev/null +++ b/usr/share/jolla-settings/pages/nfc/NfcSwitch.qml @@ -0,0 +1,18 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Nemo.DBus 2.0 +import com.jolla.settings.system 1.0 +import com.jolla.settings 1.0 + +SettingsToggle { + //% "NFC" + name: qsTrId("settings_nfc_switch-la-nfc") + icon.source: "image://theme/icon-m-nfc" + onToggled: nfcConfig.toggleNfcEnabled() + checked: nfcConfig.nfcEnabled + busy: nfcConfig.busy + + NfcConfig { + id: nfcConfig + } +} diff --git a/usr/share/jolla-settings/pages/nfc/nfc.qml b/usr/share/jolla-settings/pages/nfc/nfc.qml new file mode 100644 index 00000000..020f9e84 --- /dev/null +++ b/usr/share/jolla-settings/pages/nfc/nfc.qml @@ -0,0 +1,52 @@ +import QtQuick 2.6 +import Sailfish.Silica 1.0 +import Nemo.DBus 2.0 +import com.jolla.settings.system 1.0 +import com.jolla.settings 1.0 + +Page { + id: root + + SilicaFlickable { + anchors.fill: parent + contentHeight: content.height + + Column { + id: content + width: parent.width + + PageHeader { + //% "NFC" + title: qsTrId("settings_nfc-he-nfc") + } + + IconTextSwitch { + //% "Near Field Communication (NFC)" + text: qsTrId("settings_nfc-la-nfc") + //% "Allow device to detect NFC tags and other devices when touched. This feature consumes some battery power." + description: qsTrId("settings_nfc-la-nfc_switch_description") + icon.source: "image://theme/icon-m-nfc" + onClicked: nfcConfig.toggleNfcEnabled() + checked: nfcConfig.nfcEnabled + automaticCheck: false + busy: nfcConfig.busy + } + + TextSwitch { + //% "Bluetooth Secure Simple Pairing" + text: qsTrId("settings_nfc-la-bluetooth_simple_pairing") + //% "Allow automatic Bluetooth pairing via NFC." + description: qsTrId("settings_nfc-la-bluetooth_simple_pairing_switch_description") + onClicked: nfcConfig.toggleNfcBluetoothStaticHandoverEnabled() + checked: nfcConfig.nfcBluetoothStaticHandoverEnabled + automaticCheck: false + busy: nfcConfig.neardBusy + visible: nfcConfig.nfcBluetoothStaticHandoverSupported && nfcConfig.nfcEnabled + } + } + } + + NfcConfig { + id: nfcConfig + } +} diff --git a/usr/share/jolla-settings/pages/packages/packages.qml b/usr/share/jolla-settings/pages/packages/packages.qml new file mode 100644 index 00000000..97a53ba2 --- /dev/null +++ b/usr/share/jolla-settings/pages/packages/packages.qml @@ -0,0 +1,61 @@ +import QtQuick 2.0 +import QtDocGallery 5.0 +import Sailfish.Silica 1.0 +import Sailfish.FileManager 1.0 +import Nemo.FileManager 1.0 + +Page { + DocumentGalleryModel { + id: fileModel + + properties: ["url", "fileName"] + sortProperties: ["+fileName"] + rootType: DocumentGallery.File + autoUpdate: true + filter: GalleryFilterUnion { + GalleryEqualsFilter { property: "fileExtension"; value: "rpm" } + GalleryEqualsFilter { property: "fileExtension"; value: "apk" } + } + + } + + SilicaListView { + model: fileModel + anchors.fill: parent + header: PageHeader { + //% "Install package" + title: qsTrId("settings_packages-he-install_package") + } + + delegate: BackgroundItem { + height: fileItem.height + + onClicked: Qt.openUrlExternally(url) + + FileItem { + id: fileItem + fileName: model.fileName + mimeType: fileInfo.mimeType + size: fileInfo.size + modified: fileInfo.lastModified + + FileInfo { + id: fileInfo + url: model.url + } + } + } + + ViewPlaceholder { + enabled: fileModel.count == 0 && (fileModel.status === DocumentGalleryModel.Finished || fileModel.status === DocumentGalleryModel.Idle) + + //% "No installable packages found" + text: qsTrId("settings_packages-la-no_installable_packages_found") + + //% "Copy RPM or APK files to the internal memory or connected storage device" + hintText: qsTrId("settings_packages-la-copy_package_files_to_device") + } + + VerticalScrollDecorator {} + } +} diff --git a/usr/share/jolla-settings/pages/pin/ModemPin.qml b/usr/share/jolla-settings/pages/pin/ModemPin.qml index 5825f80a..1c02190a 100644 --- a/usr/share/jolla-settings/pages/pin/ModemPin.qml +++ b/usr/share/jolla-settings/pages/pin/ModemPin.qml @@ -7,8 +7,9 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 -import org.nemomobile.notifications 1.0 -import MeeGo.QOfono 0.2 +import Sailfish.Settings.Networking 1.0 +import Nemo.Notifications 1.0 +import QOfono 0.2 Column { id: root diff --git a/usr/share/jolla-settings/pages/pin/PinInputPage.qml b/usr/share/jolla-settings/pages/pin/PinInputPage.qml index e9fd8507..15992275 100644 --- a/usr/share/jolla-settings/pages/pin/PinInputPage.qml +++ b/usr/share/jolla-settings/pages/pin/PinInputPage.qml @@ -2,7 +2,7 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 import Sailfish.Telephony 1.0 import com.jolla.settings.system 1.0 -import MeeGo.QOfono 0.2 +import QOfono 0.2 Page { id: pinInputPage diff --git a/usr/share/jolla-settings/pages/pin/pin.qml b/usr/share/jolla-settings/pages/pin/pin.qml index 32b81b9a..e4faea25 100644 --- a/usr/share/jolla-settings/pages/pin/pin.qml +++ b/usr/share/jolla-settings/pages/pin/pin.qml @@ -1,10 +1,11 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 import Sailfish.Telephony 1.0 +import Sailfish.Settings.Networking 1.0 import com.jolla.settings.system 1.0 import org.nemomobile.systemsettings 1.0 import org.nemomobile.ofono 1.0 -import MeeGo.QOfono 0.2 +import QOfono 0.2 Page { id: root diff --git a/usr/share/jolla-settings/pages/reset/reset.qml b/usr/share/jolla-settings/pages/reset/reset.qml index 717f414d..a9117919 100644 --- a/usr/share/jolla-settings/pages/reset/reset.qml +++ b/usr/share/jolla-settings/pages/reset/reset.qml @@ -2,9 +2,9 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 import Sailfish.Policy 1.0 import com.jolla.settings.system 1.0 -import org.nemomobile.dbus 2.0 +import Nemo.DBus 2.0 import org.nemomobile.devicelock 1.0 -import org.nemomobile.configuration 1.0 +import Nemo.Configuration 1.0 import org.nemomobile.systemsettings 1.0 Page { diff --git a/usr/share/jolla-settings/pages/sailfish-weather/SettingsPage.qml b/usr/share/jolla-settings/pages/sailfish-weather/SettingsPage.qml new file mode 100644 index 00000000..27a2fb58 --- /dev/null +++ b/usr/share/jolla-settings/pages/sailfish-weather/SettingsPage.qml @@ -0,0 +1,45 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import org.sailfishos.weather.settings 1.0 +import Nemo.Configuration 1.0 +import com.jolla.settings 1.0 + +ApplicationSettings { + id: root + ConfigurationValue { + id: temperatureUnitValue + key: "/sailfish/weather/temperature_unit" + defaultValue: "celsius" + } + + ComboBox { + //% "Temperature units" + label: qsTrId("weather_settings-la-temperature_units") + Component.onCompleted: { + switch (temperatureUnitValue.value) { + case "celsius": + currentIndex = 0 + break + case "fahrenheit": + currentIndex = 1 + break + default: + console.log("WeatherSettings: Invalid temperature unit value", temperatureUnitValue.value) + break + } + } + + menu: ContextMenu { + MenuItem { + //% "Celsius" + text: qsTrId("weather_settings-me-celsius") + onClicked: temperatureUnitValue.value = "celsius" + } + MenuItem { + //% "Fahrenheit" + text: qsTrId("weather_settings-me-fahrenheit") + onClicked: temperatureUnitValue.value = "fahrenheit" + } + } + } +} diff --git a/usr/share/jolla-settings/pages/sailfishos/UpgradeDetails.qml b/usr/share/jolla-settings/pages/sailfishos/UpgradeDetails.qml index dd0652d6..a70b7d70 100644 --- a/usr/share/jolla-settings/pages/sailfishos/UpgradeDetails.qml +++ b/usr/share/jolla-settings/pages/sailfishos/UpgradeDetails.qml @@ -98,7 +98,7 @@ Column { //: beginning of a sentence, thus a colon is needed after "Last checked". //% "Last checked: %1" text: qsTrId("settings_sailfishos-la-last_checked").arg( - Format.formatDate(storeIf.lastChecked, Formatter.DurationElapsed)) + Format.formatDate(storeIf.lastChecked, Formatter.TimeElapsed)) color: Theme.secondaryHighlightColor font.pixelSize: Theme.fontSizeExtraSmall visible: (storeIf.updateStatus === StoreInterface.UpToDate || diff --git a/usr/share/jolla-settings/pages/sailfishos/UpgradeSettings.qml b/usr/share/jolla-settings/pages/sailfishos/UpgradeSettings.qml index 276fa4cf..b65f2a3f 100644 --- a/usr/share/jolla-settings/pages/sailfishos/UpgradeSettings.qml +++ b/usr/share/jolla-settings/pages/sailfishos/UpgradeSettings.qml @@ -8,7 +8,7 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 import Sailfish.Policy 1.0 -import org.nemomobile.configuration 1.0 +import Nemo.Configuration 1.0 import com.jolla.settings.sailfishos 1.0 import org.nemomobile.ofono 1.0 diff --git a/usr/share/jolla-settings/pages/sailfishos/mainpage.qml b/usr/share/jolla-settings/pages/sailfishos/mainpage.qml index c99b6f96..de1c86b9 100644 --- a/usr/share/jolla-settings/pages/sailfishos/mainpage.qml +++ b/usr/share/jolla-settings/pages/sailfishos/mainpage.qml @@ -2,7 +2,7 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 import Sailfish.Vault 1.0 import Sailfish.Policy 1.0 -import org.nemomobile.configuration 1.0 +import Nemo.Configuration 1.0 import org.nemomobile.systemsettings 1.0 import com.jolla.settings.sailfishos 1.0 import com.jolla.settings.system 1.0 as MdmBanner @@ -49,7 +49,6 @@ Page { && updateProgress === 0 readonly property bool haveDetails: updateStatus === StoreInterface.UpdateAvailable || updateStatus === StoreInterface.PreparingForUpdate - readonly property bool downloading: updateProgress > 0 && updateProgress < 100 readonly property bool downloaded: updateProgress === 100 readonly property bool ssuRndModeRequiresRegistration: ssu.deviceMode & Ssu.RndMode && !ssu.registered readonly property bool ssuCbetaRequiresRegistration: ssu.domain === "cbeta" && !ssu.registered diff --git a/usr/share/jolla-settings/pages/sounds/DoNotDisturbSwitch.qml b/usr/share/jolla-settings/pages/sounds/DoNotDisturbSwitch.qml index 27543064..8f3d154e 100644 --- a/usr/share/jolla-settings/pages/sounds/DoNotDisturbSwitch.qml +++ b/usr/share/jolla-settings/pages/sounds/DoNotDisturbSwitch.qml @@ -1,7 +1,7 @@ import QtQuick 2.6 import Sailfish.Silica 1.0 import com.jolla.settings 1.0 -import org.nemomobile.configuration 1.0 +import Nemo.Configuration 1.0 SettingsToggle { //% "Do not disturb" diff --git a/usr/share/jolla-settings/pages/sounds/RingtoneVolumeSettingsSlider.qml b/usr/share/jolla-settings/pages/sounds/RingtoneVolumeSettingsSlider.qml new file mode 100644 index 00000000..2fa74bc6 --- /dev/null +++ b/usr/share/jolla-settings/pages/sounds/RingtoneVolumeSettingsSlider.qml @@ -0,0 +1,24 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import com.jolla.settings 1.0 + +SettingsControl { + id: root + + contentHeight: slider.height + + VolumeSliderController { + slider: slider + } + + SettingsSlider { + id: slider + + width: root.width + + onPressAndHold: { + slider.cancel() + root.openMenu() + } + } +} diff --git a/usr/share/jolla-settings/pages/sounds/RingtoneVolumeSlider.qml b/usr/share/jolla-settings/pages/sounds/RingtoneVolumeSlider.qml new file mode 100644 index 00000000..4362b0d8 --- /dev/null +++ b/usr/share/jolla-settings/pages/sounds/RingtoneVolumeSlider.qml @@ -0,0 +1,10 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 + +Slider { + id: slider + + VolumeSliderController { + slider: slider + } +} diff --git a/usr/share/jolla-settings/pages/sounds/SoundsPage.qml b/usr/share/jolla-settings/pages/sounds/SoundsPage.qml index 30180186..653dd378 100644 --- a/usr/share/jolla-settings/pages/sounds/SoundsPage.qml +++ b/usr/share/jolla-settings/pages/sounds/SoundsPage.qml @@ -3,7 +3,7 @@ import Sailfish.Silica 1.0 import com.jolla.settings 1.0 import com.jolla.settings.system 1.0 import org.nemomobile.systemsettings 1.0 -import org.nemomobile.configuration 1.0 +import Nemo.Configuration 1.0 import QtFeedback 5.0 Page { @@ -90,6 +90,10 @@ Page { } } + RingtoneVolumeSlider { + width: parent.width + } + VolumeSlider { width: parent.width } @@ -146,7 +150,7 @@ Page { } SectionHeader { - //% "Do not disturb" + //% "Do not disturb mode" text: qsTrId("settings_sounds-la-do_not_disturb") } TextSwitch { @@ -159,11 +163,81 @@ Page { onClicked: doNotDisturb.value = !doNotDisturb.value } + ComboBox { + id: dndRingtoneCombobox + + //% "Ringtone for incoming calls" + label: qsTrId("settings_sounds-la-do_not_disturb_ringtone") + menu: ContextMenu { + MenuItem { + property string value: "off" + //: No ringtone on incoming calls on do no disturb mode + //% "Off" + text: qsTrId("settings_sounds-la-do_not_disturb_ringtone_off") + } + MenuItem { + property string value: "favorites" + //: Favorite contacts have ringtone on incoming calls on do no disturb mode + //% "Only favorite contacts" + text: qsTrId("settings_sounds-la-do_not_disturb_ringtone_favorites") + } + MenuItem { + property string value: "contacts" + //: Known contacts have ringtone on incoming calls on do no disturb mode + //% "Only contacts" + text: qsTrId("settings_sounds-la-do_not_disturb_ringtone_contacts") + } + MenuItem { + property string value: "on" + //: Ringtone plays on incoming calls on do no disturb mode + //% "On" + text: qsTrId("settings_sounds-la-do_not_disturb_ringtone_on") + } + } + + //% "Allow some incoming calls to play ringtones as exceptions to ‘Do not disturb’ mode" + description: qsTrId("settings_sounds-la-do_not_disturb_ringtone_exceptions") + + onCurrentItemChanged: { + if (currentItem) { + doNotDisturbRingtone.value = currentItem.value + } + } + Component.onCompleted: updateIndex() + + + function updateIndex() { + currentIndex = valueToIndex(doNotDisturbRingtone.value) + } + + function valueToIndex(config) { + switch(config) { + case "off": + return 0 + case "favorites": + return 1 + case "contacts": + return 2 + case "on": + case "default": + return 3 + } + } + } + ConfigurationValue { id: doNotDisturb defaultValue: false key: "/lipstick/do_not_disturb" } + + ConfigurationValue { + id: doNotDisturbRingtone + + defaultValue: "on" + key: "/lipstick/do_not_disturb_ringtone" + onValueChanged: dndRingtoneCombobox.updateIndex() + } } VerticalScrollDecorator {} } diff --git a/usr/share/jolla-settings/pages/sounds/VolumeSettingsSlider.qml b/usr/share/jolla-settings/pages/sounds/VolumeSettingsSlider.qml index 2fa74bc6..c316c391 100644 --- a/usr/share/jolla-settings/pages/sounds/VolumeSettingsSlider.qml +++ b/usr/share/jolla-settings/pages/sounds/VolumeSettingsSlider.qml @@ -9,6 +9,9 @@ SettingsControl { VolumeSliderController { slider: slider + volumeControlActive: true + stepSize: 10 + maximumValue: maximumVolume * stepSize } SettingsSlider { diff --git a/usr/share/jolla-settings/pages/sounds/VolumeSlider.qml b/usr/share/jolla-settings/pages/sounds/VolumeSlider.qml index 4362b0d8..07b2d656 100644 --- a/usr/share/jolla-settings/pages/sounds/VolumeSlider.qml +++ b/usr/share/jolla-settings/pages/sounds/VolumeSlider.qml @@ -6,5 +6,8 @@ Slider { VolumeSliderController { slider: slider + volumeControlActive: true + stepSize: 10 + maximumValue: maximumVolume * stepSize } } diff --git a/usr/share/jolla-settings/pages/sounds/VolumeSliderController.qml b/usr/share/jolla-settings/pages/sounds/VolumeSliderController.qml index 1e512058..9852b31c 100644 --- a/usr/share/jolla-settings/pages/sounds/VolumeSliderController.qml +++ b/usr/share/jolla-settings/pages/sounds/VolumeSliderController.qml @@ -5,6 +5,8 @@ import Nemo.Ngf 1.0 import com.jolla.settings 1.0 import com.jolla.settings.system 1.0 import org.nemomobile.systemsettings 1.0 +import Nemo.Configuration 1.0 +import org.nemomobile.lipstick 0.1 Item { id: root @@ -12,14 +14,19 @@ Item { property QtObject slider property bool externalChange property bool propertiesUpdating - readonly property bool play: slider.down && !playDelay.running + readonly property bool play: slider.down && !playDelay.running && !volumeControlActive + property bool volumeControlActive + property int stepSize: 20 + property int maximumValue: 100 + property alias maximumVolume: volumeControl.maximumVolume function updateSliderValue() { if (propertiesUpdating) { return } externalChange = true - slider.value = (profileControl.profile == "silent") ? 0 : profileControl.ringerVolume + slider.value = volumeControlActive ? volumeControl.volume * 10 : + (profileControl.profile == "silent") ? 0 : profileControl.ringerVolume externalChange = false } @@ -29,17 +36,32 @@ Item { name: "default" PropertyChanges { + id: sliderProperties + readonly property real maxDragX: slider.width - slider.rightMargin - slider._highlightItem.width/2 + readonly property real stepWidth: slider._grooveWidth / volumeControl.maximumVolume + property bool restrictedMaxDragXBinding: volumeControl.restrictedVolume !== volumeControl.maximumVolume + target: slider // assuming Slider's internal paddings allow label to be nicely shown if a bit extra is reserved height: slider.implicitHeight + valueLabel.height + Theme.paddingSmall - //% "Ringtone volume" - label: qsTrId("settings_sounds_la_volume") - maximumValue: 100 + label: volumeControlActive ? + //% "Volume" + qsTrId("settings_sounds_la_media_volume") : + //% "Ringtone volume" + qsTrId("settings_sounds_la_volume") + maximumValue: root.maximumValue minimumValue: 0 - stepSize: 20 + stepSize: root.stepSize onDownChanged: { + if (volumeControlActive && sliderProperties.restrictedMaxDragXBinding) { + slider.drag.maximumX = Qt.binding(function() { + return sliderProperties.maxDragX - sliderProperties.stepWidth * (volumeControl.maximumVolume - volumeControl.currentMax) + }) + sliderProperties.restrictedMaxDragXBinding = false + } + if (slider.down) { playDelay.restart() } @@ -48,8 +70,12 @@ Item { onValueChanged: { if (!root.externalChange) { root.propertiesUpdating = true // don't update slider until new values of ringVolume + profile are both known - profileControl.ringerVolume = slider.value - profileControl.profile = (slider.value > 0) ? "general" : "silent" + if (volumeControlActive) { + volumeControl.volume = slider.value / 10 + } else { + profileControl.ringerVolume = slider.value + profileControl.profile = (slider.value > 0) ? "general" : "silent" + } root.propertiesUpdating = false } } @@ -70,6 +96,11 @@ Item { slider.animateValue = true } + ConfigurationValue { + key: "/jolla/sound/sw_volume_slider/active" + value: slider.down + } + NonGraphicalFeedback { id: feedback event: "ringtone" @@ -83,11 +114,13 @@ Item { SliderValueLabel { id: valueLabel + property int scaledVolume: Math.round(slider.value * 10 / maximumVolume) + parent: root.slider slider: root.slider //% "%1%" - text: slider.value > 0 ? qsTrId("settings_sounds-la-percentage_format").arg(slider.value) + text: slider.value > 0 ? qsTrId("settings_sounds-la-percentage_format").arg(volumeControlActive ? scaledVolume : slider.value) : "" scale: slider.pressed ? Theme.fontSizeLarge / Theme.fontSizeMedium : 1.0 font.pixelSize: Theme.fontSizeMedium @@ -103,6 +136,16 @@ Item { Behavior on scale { NumberAnimation { duration: 80 } } } + VolumeControl { + id: volumeControl + + readonly property int currentMax: (restrictedVolume !== maximumVolume) ? restrictedVolume : maximumVolume + + onVolumeChanged: { + root.updateSliderValue() + } + } + ProfileControl { id: profileControl diff --git a/usr/share/jolla-settings/pages/storage/DiskUsageModel.qml b/usr/share/jolla-settings/pages/storage/DiskUsageModel.qml index d3bcc109..c64d6fe3 100644 --- a/usr/share/jolla-settings/pages/storage/DiskUsageModel.qml +++ b/usr/share/jolla-settings/pages/storage/DiskUsageModel.qml @@ -78,7 +78,7 @@ ListModel { { //% "Android™ apps" label: qsTrId("settings_about-li-disk_usage-android_apps"), - path: '/home/.android/data/app/', + path: ':apkd:app', storageType: 'user', position: 0, androidDataDirectory: true @@ -86,7 +86,7 @@ ListModel { { //% "Android™ app data files" label: qsTrId("settings_about-li-disk_usage-android_app_data_files"), - path: ':apkd:', + path: ':apkd:data', storageType: 'user', position: 0, androidDataDirectory: true @@ -97,7 +97,8 @@ ListModel { path: StandardPaths.home + '/android_storage/', storageType: 'user', position: 0, - androidDataDirectory: true + androidDataDirectory: true, + pathAllowed: true }, { //: %1 is operating system name @@ -111,10 +112,7 @@ ListModel { { //% "Android™ runtime" label: qsTrId("settings_about-li-disk_usage-android_runtime"), - // NOTE: We can not use /opt/alien/ as there is lots of bind mounts - // there that mess up the calculations, also the amount of data outside - // system dir is not making much difference. - path: '/opt/alien/system/', + path: ':apkd:runtime', storageType: 'system', position: 0, androidDataDirectory: true diff --git a/usr/share/jolla-settings/pages/storage/DiskUsagePage.qml b/usr/share/jolla-settings/pages/storage/DiskUsagePage.qml index 45627d5c..f30b5cdf 100644 --- a/usr/share/jolla-settings/pages/storage/DiskUsagePage.qml +++ b/usr/share/jolla-settings/pages/storage/DiskUsagePage.qml @@ -1,7 +1,7 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 import org.nemomobile.systemsettings 1.0 -import org.nemomobile.dbus 2.0 +import Nemo.DBus 2.0 Page { id: diskUsagePage diff --git a/usr/share/jolla-settings/pages/sync/sync.qml b/usr/share/jolla-settings/pages/sync/sync.qml new file mode 100644 index 00000000..f5ebde4d --- /dev/null +++ b/usr/share/jolla-settings/pages/sync/sync.qml @@ -0,0 +1,6 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import com.jolla.settings.sync 1.0 + +SyncSettingsPage { +} diff --git a/usr/share/jolla-settings/pages/tethering/BluetoothTethering.qml b/usr/share/jolla-settings/pages/tethering/BluetoothTethering.qml new file mode 100644 index 00000000..ecc950d0 --- /dev/null +++ b/usr/share/jolla-settings/pages/tethering/BluetoothTethering.qml @@ -0,0 +1,41 @@ +import QtQuick 2.0 +import Sailfish.Settings.Networking 1.0 +import Nemo.Connectivity 1.0 +import Nemo.DBus 2.0 +import Connman 0.2 +import com.jolla.connection 1.0 + +Item { + readonly property alias busy: delayedTetheringSwitch.running + property alias active: btTechnology.tethering + property alias powered: btTechnology.powered + + function stopTethering() { + delayedTetheringSwitch.start() + connectionAgent.stopTethering("bluetooth", true) + } + + function startTethering() { + delayedTetheringSwitch.start() + connectionAgent.startTethering("bluetooth") + } + + Timer { + id: delayedTetheringSwitch + interval: 15000 + } + + ConnectionAgent { + id: connectionAgent + onBluetoothTetheringFinished: delayedTetheringSwitch.stop() + } + + NetworkManager { + id: networkManager + } + + NetworkTechnology { + id: btTechnology + path: networkManager.BluetoothTechnology + } +} diff --git a/usr/share/jolla-settings/pages/tethering/MobileDataWifiTethering.qml b/usr/share/jolla-settings/pages/tethering/MobileDataWifiTethering.qml index c7685c19..44a6b19e 100644 --- a/usr/share/jolla-settings/pages/tethering/MobileDataWifiTethering.qml +++ b/usr/share/jolla-settings/pages/tethering/MobileDataWifiTethering.qml @@ -2,7 +2,7 @@ import QtQuick 2.0 import Sailfish.Settings.Networking 1.0 import Nemo.Connectivity 1.0 import Nemo.DBus 2.0 -import MeeGo.Connman 0.2 +import Connman 0.2 import com.jolla.connection 1.0 Item { @@ -18,7 +18,7 @@ Item { function stopTethering() { delayedTetheringSwitch.start() - connectionAgent.stopTethering() + connectionAgent.stopTethering("wifi") } function startTethering() { @@ -61,7 +61,7 @@ Item { ConnectionAgent { id: connectionAgent - onTetheringFinished: delayedTetheringSwitch.stop() + onWifiTetheringFinished: delayedTetheringSwitch.stop() } NetworkManager { diff --git a/usr/share/jolla-settings/pages/tethering/mainpage.qml b/usr/share/jolla-settings/pages/tethering/mainpage.qml index 2c04c3eb..80883c4d 100644 --- a/usr/share/jolla-settings/pages/tethering/mainpage.qml +++ b/usr/share/jolla-settings/pages/tethering/mainpage.qml @@ -2,11 +2,11 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 import Sailfish.Silica.private 1.0 import Sailfish.Policy 1.0 -import Nemo.Ssu 1.1 as Ssu import com.jolla.settings 1.0 import com.jolla.settings.system 1.0 import Sailfish.Settings.Networking 1.0 import com.jolla.settings.system 1.0 +import org.nemomobile.systemsettings 1.0 Page { id: page @@ -36,6 +36,10 @@ Page { return ok } + DeviceInfo { + id: deviceInfo + } + SilicaFlickable { id: content anchors.fill: parent @@ -49,6 +53,8 @@ Page { SimActivationPullDownMenu { id: pullDownMenu + + showSimActivation: false // only for flight mode checking } SimViewPlaceholder { @@ -73,9 +79,8 @@ Page { active: !AccessPolicy.internetSharingEnabled } - // WLAN hotspot #/# users ListItem { - id: hotspotItem + id: wlanHotspotItem contentHeight: wlanSwitch.height openMenuOnPressAndHold: false _backgroundColor: "transparent" @@ -83,18 +88,16 @@ Page { IconTextSwitch { id: wlanSwitch - property string entryPath: "system_settings/connectivity/tethering/wlan_hotspot_switch" - //% "WLAN hotspot" text: qsTrId("settings_network-la-wlan-hotspot") - //% "Share device's mobile connection through WLAN network others can join" - description: qsTrId("settings_network-me-share_mobile_connection") + //% "Share device's mobile connection via WLAN" + description: qsTrId("settings_network-me-share_mobile_connection_wlan") icon.source: "image://theme/icon-m-wlan-hotspot" busy: wifiTethering.busy automaticCheck: false checked: wifiTethering.active - highlighted: hotspotItem.highlighted + highlighted: wlanHotspotItem.highlighted enabled: content.enabled && !wifiTethering.offlineMode && passwordInput.text.length > 0 && networkNameInput.text.length > 0 && !wlanSwitch.busy @@ -153,7 +156,7 @@ Page { opacity: content.enabled ? 1.0 : Theme.opacityLow maximumLength: 32 - text: wifiTethering.identifier.length === 0 ? Ssu.DeviceInfo.displayName(Ssu.DeviceInfo.DeviceModel) : wifiTethering.identifier + text: wifiTethering.identifier.length === 0 ? deviceInfo.prettyName : wifiTethering.identifier //% "Network name (SSID)" label: qsTrId("settings_network-la-tethering_network_name") EnterKey.iconSource: "image://theme/icon-m-enter-next" @@ -184,10 +187,57 @@ Page { //% "Minimum length for passphrase is 8 characters" description: errorHighlight ? qsTrId("settings-la-passphrase-length") : "" } + + // BT hotspot + SectionHeader { + //% "Bluetooth" + text: qsTrId("settings_network-he-bluetooth") + visible: deviceInfo.hasFeature(DeviceInfo.FeatureBluetoothTethering) + } + + ListItem { + id: btHotspotItem + contentHeight: btSwitch.height + openMenuOnPressAndHold: false + _backgroundColor: "transparent" + visible: deviceInfo.hasFeature(DeviceInfo.FeatureBluetoothTethering) + + IconTextSwitch { + id: btSwitch + + //% "Bluetooth network sharing" + text: qsTrId("settings_network-la-bt-hotspot") + //% "Allow paired devices to use the internet connection when Bluetooth is on" + description: qsTrId("settings_network-me-share_network_connection_bt") + icon.source: "image://theme/icon-m-bluetooth" + + busy: btTethering.busy + automaticCheck: false + checked: btTethering.active + highlighted: btHotspotItem.highlighted + enabled: content.enabled + && !btSwitch.busy + onClicked: { + if (btTethering.busy) { + return + } + + if (btTethering.active) { + btTethering.stopTethering() + } else { + btTethering.startTethering() + } + } + } + } } } MobileDataWifiTethering { id: wifiTethering } + + BluetoothTethering { + id: btTethering + } } diff --git a/usr/share/jolla-settings/pages/text_input/textinput.qml b/usr/share/jolla-settings/pages/text_input/textinput.qml index afeb17e0..665b9803 100644 --- a/usr/share/jolla-settings/pages/text_input/textinput.qml +++ b/usr/share/jolla-settings/pages/text_input/textinput.qml @@ -26,7 +26,7 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 -import org.nemomobile.configuration 1.0 +import Nemo.Configuration 1.0 import com.jolla.keyboard 1.0 import com.jolla.keyboard.translations 1.0 diff --git a/usr/share/jolla-settings/pages/text_input/xt9pinyin.qml b/usr/share/jolla-settings/pages/text_input/xt9pinyin.qml index fba9b928..8bd42b7a 100644 --- a/usr/share/jolla-settings/pages/text_input/xt9pinyin.qml +++ b/usr/share/jolla-settings/pages/text_input/xt9pinyin.qml @@ -1,6 +1,6 @@ import QtQuick 2.6 import Sailfish.Silica 1.0 -import org.nemomobile.configuration 1.0 +import Nemo.Configuration 1.0 Column { width: parent.width diff --git a/usr/share/jolla-settings/pages/topmenu/topmenu.qml b/usr/share/jolla-settings/pages/topmenu/topmenu.qml index 3b28583d..9e5aae17 100644 --- a/usr/share/jolla-settings/pages/topmenu/topmenu.qml +++ b/usr/share/jolla-settings/pages/topmenu/topmenu.qml @@ -3,7 +3,7 @@ import Sailfish.Silica 1.0 import com.jolla.settings 1.0 import com.jolla.settings.system 1.0 import org.nemomobile.systemsettings 1.0 -import org.nemomobile.configuration 1.0 +import Nemo.Configuration 1.0 Page { id: root diff --git a/usr/share/jolla-settings/pages/transferui/mainpage.qml b/usr/share/jolla-settings/pages/transferui/mainpage.qml index 579df3ef..b3f5d21f 100644 --- a/usr/share/jolla-settings/pages/transferui/mainpage.qml +++ b/usr/share/jolla-settings/pages/transferui/mainpage.qml @@ -1,3 +1,39 @@ +/**************************************************************************************** +** Copyright (c) 2013 - 2023 Jolla Ltd. +** +** All rights reserved. +** +** This file is part of Sailfish Transfer Engine component package. +** +** You may use this file under the terms of BSD license as follows: +** +** Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are met: +** +** 1. Redistributions of source code must retain the above copyright notice, this +** list of conditions and the following disclaimer. +** +** 2. Redistributions in binary form must reproduce the above copyright notice, +** this list of conditions and the following disclaimer in the documentation +** and/or other materials provided with the distribution. +** +** 3. Neither the name of the copyright holder nor the names of its +** contributors may be used to endorse or promote products derived from +** this software without specific prior written permission. +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +** AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +** IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +** DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +** FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +** DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +** SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +** CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +** OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +** +****************************************************************************************/ + import QtQuick 2.0 import Sailfish.TransferEngine 1.0 diff --git a/usr/share/jolla-settings/pages/vpn/EnableSwitch.qml b/usr/share/jolla-settings/pages/vpn/EnableSwitch.qml index ed91d2c7..bff80bbd 100644 --- a/usr/share/jolla-settings/pages/vpn/EnableSwitch.qml +++ b/usr/share/jolla-settings/pages/vpn/EnableSwitch.qml @@ -7,16 +7,18 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 -import MeeGo.Connman 0.2 +import Connman 0.2 import Nemo.DBus 2.0 import com.jolla.settings 1.0 -import org.nemomobile.systemsettings 1.0 +import Nemo.Connectivity 1.0 SettingsToggle { id: root property string _connectionName - readonly property bool waitForConnection: (!networkManager.connected || (SettingsVpnModel.bestState <= VpnConnection.Configuration)) && SettingsVpnModel.autoConnect + readonly property bool waitForConnection: (!networkManager.connected + || (SettingsVpnModel.bestState <= VpnConnection.Association)) + && SettingsVpnModel.autoConnect function _updateConnectionName() { var connectionName = "" @@ -37,9 +39,11 @@ SettingsToggle { active: SettingsVpnModel.bestState == VpnConnection.Ready checked: (SettingsVpnModel.bestState != VpnConnection.Idle - && SettingsVpnModel.bestState != VpnConnection.Failure) || waitForConnection + && SettingsVpnModel.bestState != VpnConnection.Failure) || waitForConnection - busy: (SettingsVpnModel.bestState == VpnConnection.Configuration || SettingsVpnModel.bestState == VpnConnection.Disconnect) + busy: (SettingsVpnModel.bestState == VpnConnection.Association || + SettingsVpnModel.bestState == VpnConnection.Configuration || + SettingsVpnModel.bestState == VpnConnection.Disconnect) && !waitForConnection menu: ContextMenu { diff --git a/usr/share/jolla-settings/pages/vpn/VpnItem.qml b/usr/share/jolla-settings/pages/vpn/VpnItem.qml index 5aad243d..1a5f1ca7 100644 --- a/usr/share/jolla-settings/pages/vpn/VpnItem.qml +++ b/usr/share/jolla-settings/pages/vpn/VpnItem.qml @@ -7,9 +7,9 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 -import MeeGo.Connman 0.2 +import Connman 0.2 import Sailfish.Policy 1.0 -import org.nemomobile.systemsettings 1.0 +import Nemo.Connectivity 1.0 import Sailfish.Settings.Networking.Vpn 1.0 ListItem { @@ -84,7 +84,7 @@ ListItem { automaticCheck: false checked: connection ? connection.autoConnect : false highlighted: root.highlighted - busy: connection ? (connection.state === VpnConnection.Configuration || connection.state === VpnConnection.Disconnect) : false + busy: connection ? (connection.state === VpnConnection.Configuration || connection.state === VpnConnection.Association || connection.state === VpnConnection.Disconnect) : false text: connection ? connection.name : '' description: { var state @@ -99,7 +99,7 @@ ListItem { } else if (connection.state == VpnConnection.Disconnect) { //% "Disconnecting..." state = qsTrId("settings_network-la-disconnecting_state") - } else if (connection.state == VpnConnection.Configuration) { + } else if (connection.state == VpnConnection.Configuration || connection.state == VpnConnection.Association) { //% "Connecting..." state = qsTrId("settings_network-la-connecting_state") } else if (checked && !root.networkOnline) { diff --git a/usr/share/jolla-settings/pages/vpn/mainpage.qml b/usr/share/jolla-settings/pages/vpn/mainpage.qml index d6cc7806..202ee593 100644 --- a/usr/share/jolla-settings/pages/vpn/mainpage.qml +++ b/usr/share/jolla-settings/pages/vpn/mainpage.qml @@ -8,10 +8,11 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 import Sailfish.Policy 1.0 -import MeeGo.Connman 0.2 +import Connman 0.2 import com.jolla.settings.system 1.0 import Sailfish.Settings.Networking.Vpn 1.0 import org.nemomobile.systemsettings 1.0 +import Nemo.Connectivity 1.0 import Qt.labs.folderlistmodel 2.1 Page { diff --git a/usr/share/jolla-settings/pages/wlan/AddNetworkDialog.qml b/usr/share/jolla-settings/pages/wlan/AddNetworkDialog.qml index 73d06f06..4095621c 100644 --- a/usr/share/jolla-settings/pages/wlan/AddNetworkDialog.qml +++ b/usr/share/jolla-settings/pages/wlan/AddNetworkDialog.qml @@ -2,7 +2,7 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 import Sailfish.Pickers 1.0 import Sailfish.Settings.Networking 1.0 -import MeeGo.Connman 0.2 +import Connman 0.2 Dialog { id: root @@ -191,7 +191,6 @@ Dialog { AdvancedSettingsColumn { id: advancedSettingsColumn network: root.network - globalProxyButtonVisible: false } } diff --git a/usr/share/jolla-settings/pages/wlan/AdvancedSettingsPage.qml b/usr/share/jolla-settings/pages/wlan/AdvancedSettingsPage.qml index bc44e419..d47394d1 100644 --- a/usr/share/jolla-settings/pages/wlan/AdvancedSettingsPage.qml +++ b/usr/share/jolla-settings/pages/wlan/AdvancedSettingsPage.qml @@ -1,12 +1,16 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 -import MeeGo.Connman 0.2 +import Connman 0.2 import Sailfish.Pickers 1.0 import Sailfish.Settings.Networking 1.0 +import "../netproxy" -Page { +Dialog { id: root + forwardNavigation: false + canNavigateForward: false + property QtObject network onStatusChanged: { @@ -21,12 +25,6 @@ Page { contentHeight: content.height + Theme.paddingLarge PullDownMenu { - MenuItem { - //% "Details" - text: qsTrId("settings_network-me-details") - onClicked: pageStack.animatorPush("NetworkDetailsPage.qml", {network: network}) - - } MenuItem { //% "Forget network" text: qsTrId("settings_network-me-forget_network") @@ -45,7 +43,32 @@ Page { width: parent.width - PageHeader { title: root.network ? root.network.name : "" } + DialogHeader { + id: dialogHeader + acceptText: "" + + //% "Save" + cancelText: qsTrId("settings_network-he-save") + + Label { + parent: dialogHeader.extraContent + text: root.network ? root.network.name : "" + color: Theme.highlightColor + width: parent.width + truncationMode: TruncationMode.Fade + font { + pixelSize: Theme.fontSizeLarge + family: Theme.fontFamilyHeading + } + anchors { + right: parent.right + rightMargin: -Theme.horizontalPageMargin + verticalCenter: parent.verticalCenter + } + + horizontalAlignment: Qt.AlignRight + } + } EncryptionComboBox { network: root.network @@ -116,6 +139,7 @@ Page { AdvancedSettingsColumn { id: advancedSettingsColumn network: root.network + globalProxyConfigPage: Qt.resolvedUrl("../advanced-networking/mainpage.qml") } } VerticalScrollDecorator {} diff --git a/usr/share/jolla-settings/pages/wlan/AutoProxyForm.qml b/usr/share/jolla-settings/pages/wlan/AutoProxyForm.qml deleted file mode 100644 index 16e37137..00000000 --- a/usr/share/jolla-settings/pages/wlan/AutoProxyForm.qml +++ /dev/null @@ -1,77 +0,0 @@ -import QtQuick 2.0 -import Sailfish.Silica 1.0 -import Sailfish.Settings.Networking 1.0 - -Column { - id: form - - property QtObject network - - property bool updating - property bool _completed - property bool _updateRequired - - Connections { - target: network - onProxyConfigChanged: timer.restart() - } - - Timer { - id: timer - - interval: 1 - onTriggered: { - form.updating = false - if (form._updateRequired) - updateAutoProxy() - } - } - - function updateAutoProxyIfAcceptable() { - if (!_completed) - return - - if (urlField.acceptableInput) - _updateRequired = true - - if (!updating && _updateRequired) - updateAutoProxy() - } - - function updateAutoProxy() { - var proxyConfig = network.proxyConfig - - proxyConfig["Method"] = "auto" - proxyConfig["Servers"] = [] - - if (urlField.validProtocol) { - proxyConfig["URL"] = urlField.text - } else { - proxyConfig["URL"] = "https://" + urlField.text - } - - updating = true - _updateRequired = false - network.proxyConfig = proxyConfig - } - - Component.onCompleted: _completed = true - - NetworkAddressField { - id: urlField - - focus: true - text: network.proxyConfig["URL"] ? network.proxyConfig["URL"] : "" - onActiveFocusChanged: if (!activeFocus) updateAutoProxyIfAcceptable() - - //: Keep short, placeholder label that cannot wrap - //% "E.g. https://example.com/proxy.pac" - placeholderText: qsTrId("settings_network-la-automatic_proxy_address_example") - - //% "Proxy address" - label: qsTrId("settings_network-la-proxy_address") - - EnterKey.iconSource: "image://theme/icon-m-enter-close" - EnterKey.onClicked: parent.focus = true - } -} diff --git a/usr/share/jolla-settings/pages/wlan/ManualProxyForm.qml b/usr/share/jolla-settings/pages/wlan/ManualProxyForm.qml deleted file mode 100644 index a875cd13..00000000 --- a/usr/share/jolla-settings/pages/wlan/ManualProxyForm.qml +++ /dev/null @@ -1,313 +0,0 @@ -import QtQuick 2.0 -import Sailfish.Silica 1.0 -import Sailfish.Settings.Networking 1.0 - -Column { - id: form - - property QtObject network - - property bool updating - property bool _completed - property bool _updateRequired - - Connections { - target: network - onProxyConfigChanged: { - if (proxyServersChanged()) { - setServersModel() - } - if (proxyExcludesChanged()) { - setExcludes() - } - - timer.restart() - } - } - - Timer { - id: timer - - interval: 1 - onTriggered: { - form.updating = false - if (form._updateRequired) - updateManualProxy() - } - } - - function remove(index) { - var item = repeater.itemAt(index) - if (item) { - repeater.model.remove(index) - updateManualProxy() - } - } - - function updateManualProxyIfValid(addressField, portField) { - if (!_completed) - return - - if (addressField.acceptableInput && portField.acceptableInput) - _updateRequired = true - - if (!updating && _updateRequired) - updateManualProxy() - } - - function updateManualProxyExcludesIfValid(addressField) { - if (!_completed) - return - - if (addressField.acceptableInput) - _updateRequired = true - - if (!updating && _updateRequired) - updateManualProxy() - } - - function updateManualProxy() { - var proxyServer - var proxyExcludes - var proxyConfig = network.proxyConfig - - proxyConfig["Method"] = "manual" - proxyConfig["Servers"] = [] - - var addProxy = function(addressField, portField) { - if (addressField.acceptableInput && portField.acceptableInput) { - if (addressField.validProtocol) { - proxyServer = addressField.text - } else { - proxyServer = "http://" + addressField.text - } - - proxyServer = proxyServer + ":" + parseInt(portField.text, 10) - proxyConfig["Servers"].push(proxyServer) - } - } - - for (var i = 0; i < repeater.count; i++) { - var item = repeater.itemAt(i) - addProxy(item.addressField, item.portField) - } - - if (proxyExcludesField.acceptableInput) { - proxyConfig["Excludes"] = proxyExcludesField.text.replace(" ", "").split(",") - } else { - proxyConfig["Excludes"] = [] - } - - if (proxyConfig["Servers"].length > 0) { - updating = true - _updateRequired = false - network.proxyConfig = proxyConfig - } - } - - function proxyServersChanged() { - var changed = false; - var servers = network.proxyConfig["Servers"] - - if (!servers) { - if (repeater.model.count !== 0) { - changed = true - } - } else if (servers.length !== repeater.model.count) { - changed = true - } else { - for (var i = 0; i < servers.length; i++) { - var item = repeater.itemAt(i) - var serverConfig = servers[i].split(":") - var address = serverConfig[0] + ":" + serverConfig[1] - var port = serverConfig[2] - - if (item.addressField.text !== address) { - changed = true - } - - if (item.portField.text !== port) { - changed = true - } - } - } - return changed - } - - function setServersModel() { - var servers = network.proxyConfig["Servers"] - repeater.model.clear() - - if (!servers) { - repeater.model.append({}) - } else { - for (var i = 0; i < servers.length; i++) { - repeater.model.append({}) - var item = repeater.itemAt(i) - var serverConfig = servers[i].split(":") - - item.addressField.text = serverConfig[0] + ":" + serverConfig[1] - if (serverConfig.length > 2) { - item.portField.text = serverConfig[2] - } - } - } - } - - Component.onCompleted: _completed = true - - Repeater { - id: repeater - model: ListModel {} - - Component.onCompleted: { - setServersModel() - } - - ListItem { - id: proxyItem - - property bool initialized: model.index === 0 - property alias addressField: addressField - property alias portField: portField - - width: parent.width - openMenuOnPressAndHold: false - contentHeight: initialized ? column.height : 0 - opacity: initialized ? 1.0 : 0.0 - _backgroundColor: "transparent" - Behavior on opacity { FadeAnimation {}} - Behavior on contentHeight { NumberAnimation { duration: 200; easing.type: Easing.InOutQuad }} - - Component.onCompleted: initialized = true - ListView.onRemove: animateRemoval() - - menu: ContextMenu { - MenuItem { - onClicked: remove(index) - //% "Delete" - text: qsTrId("settings_network-me-delete") - } - } - - Column { - id: column - - width: parent.width - - SectionHeader { - visible: model.index > 0 - //% "Proxy %1" - text: qsTrId("settings_network-he-proxy_number").arg(model.index + 1) - } - - NetworkAddressField { - id: addressField - - _suppressPressAndHoldOnText: true - focusOutBehavior: FocusBehavior.KeepFocus - focusOnClick: false - onClicked: forceActiveFocus() - onPressAndHold: if (repeater.model.count > 1) openMenu() - focus: model.index === 0 - highlighted: activeFocus || menuOpen - onActiveFocusChanged: if (!activeFocus) updateManualProxyIfValid(addressField, portField) - - //: Keep short, placeholder label that cannot wrap - //% "E.g. http://proxy.example.com" - placeholderText: qsTrId("settings_network-la-manual_proxy_address_example") - - //% "Proxy address" - label: qsTrId("settings_network-la-proxy_address") - - EnterKey.iconSource: "image://theme/icon-m-enter-next" - EnterKey.onClicked: portField.focus = true - } - - IpPortField { - id: portField - - _suppressPressAndHoldOnText: true - focusOutBehavior: FocusBehavior.KeepFocus - focusOnClick: false - onClicked: forceActiveFocus() - onPressAndHold: if (repeater.model.count > 1) proxyItem.openMenu() - highlighted: activeFocus || menuOpen - onActiveFocusChanged: if (!activeFocus) updateManualProxyIfValid(addressField, portField) - - EnterKey.iconSource: "image://theme/icon-m-enter-next" - EnterKey.onClicked: { - if (model.index + 1 < repeater.count) { - repeater.itemAt(model.index + 1).addressField.focus = true - } else { - proxyExcludesField.focus = true - } - } - } - } - } - } - - function proxyExcludesChanged() { - var changed = false; - var excludes = WlanUtils.maybeJoin(network.proxyConfig["Excludes"]); - - if (excludes !== proxyExcludesField.text) { - changed = true - } - - return changed - } - - function setExcludes() { - proxyExcludesField.text = WlanUtils.maybeJoin(network.proxyConfig["Excludes"]) - } - - - NetworkField { - id: proxyExcludesField - - regExp: new RegExp( /^[\w- \.,]*$/ ) - Component.onCompleted: setExcludes() - onActiveFocusChanged: if (!activeFocus) updateManualProxyExcludesIfValid(proxyExcludesField) - - //: Keep short, placeholder label that cannot wrap - //% "E.g. example.com, domain.com" - placeholderText: qsTrId("settings_network-la-exclude_domains_example") - - //% "Exclude domains" - label: qsTrId("settings_network-la-exclude_domains") - - //% "List valid domain names separated by commas" - description: errorHighlight ? qsTrId("settings_network_la-exclude_domains_error") : "" - - EnterKey.iconSource: "image://theme/icon-m-enter-close" - EnterKey.onClicked: parent.focus = true - } - - BackgroundItem { - id: addProxyItem - - onClicked: repeater.model.append({}) - Image { - id: addIcon - x: Theme.paddingLarge - anchors.verticalCenter: parent.verticalCenter - source: "image://theme/icon-m-add" + (addProxyItem.highlighted ? "?" + Theme.highlightColor : "") - } - Label { - id: serviceName - - //% "Add another proxy" - text: qsTrId("settings_network-bt-add_another_proxy") - anchors { - left: addIcon.right - leftMargin: Theme.paddingSmall - verticalCenter: parent.verticalCenter - right: parent.right - rightMargin: Theme.paddingLarge - } - color: addProxyItem.highlighted ? Theme.highlightColor : Theme.primaryColor - } - } -} diff --git a/usr/share/jolla-settings/pages/wlan/NetworkDetailsPage.qml b/usr/share/jolla-settings/pages/wlan/NetworkDetailsPage.qml index 94695b58..1e32d0db 100644 --- a/usr/share/jolla-settings/pages/wlan/NetworkDetailsPage.qml +++ b/usr/share/jolla-settings/pages/wlan/NetworkDetailsPage.qml @@ -1,6 +1,6 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 -import MeeGo.Connman 0.2 +import Connman 0.2 import Sailfish.Settings.Networking 1.0 Page { @@ -12,13 +12,24 @@ Page { anchors.fill: parent contentHeight: column.height + Theme.paddingLarge + PullDownMenu { + MenuItem { + //% "Edit" + text: qsTrId("settings_network-me-edit") + onClicked: pageStack.animatorPush("AdvancedSettingsPage.qml", {"network": network}) + } + } + Column { id: column width: parent.width PageHeader { title: network.name - description: { + } + + SectionHeader { + text: { switch (network.state) { case "online": //% "Connected" @@ -33,6 +44,12 @@ Page { } } + DetailItem { + //% "Hardware Address" + label: qsTrId("settings_network-la-hardware_address") + value: network.ethernet["Address"] || "-" + } + DetailItem { //% "Security" label: qsTrId("settings_network-la-security") @@ -125,7 +142,7 @@ Page { Column { width: parent.width - visible: ipv4Address.value.length > 0 || ipv6Address.value.length > 0 + visible: network.ipv4["Address"] !== undefined || network.ipv6["Address"] !== undefined SectionHeader { //% "Addresses" @@ -140,25 +157,37 @@ Page { } DetailItem { - id: ipv4Address //% "IPv4 address" label: qsTrId("settings_network-la-ipv4_address") value: network.ipv4["Address"] - visible: value.length > 0 + visible: network.ipv4["Address"] !== undefined + } + + DetailItem { + //% "IPv4 Netmask" + label: qsTrId("settings_network-la-ipv4_netmask") + value: network.ipv4["Netmask"] || "-" + visible: network.ipv4["Address"] !== undefined + } + + DetailItem { + //% "IPv4 Gateway" + label: qsTrId("settings_network-la-ipv4_gateway") + value: network.ipv4["Gateway"] || "-" + visible: network.ipv4["Address"] !== undefined } DetailItem { - id: ipv6Address //% "IPv6 address" label: qsTrId("settings_network-la-ipv6_address") - value: network.ipv6["Address"] - visible: value.length > 0 + value: network.ipv6["Address"] + "/" + network.ipv6["PrefixLength"] + visible: network.ipv6["Address"] !== undefined } + DetailItem { //% "DNS servers" label: qsTrId("settings_network-la-dns_servesr") - value: network.nameservers ? network.nameservers.join(" ") : "" - visible: value.length > 0 + value: network.nameservers ? network.nameservers.join("\n") : "-" } } } diff --git a/usr/share/jolla-settings/pages/wlan/PeapComboBox.qml b/usr/share/jolla-settings/pages/wlan/PeapComboBox.qml index dc9b483c..fa5229e5 100644 --- a/usr/share/jolla-settings/pages/wlan/PeapComboBox.qml +++ b/usr/share/jolla-settings/pages/wlan/PeapComboBox.qml @@ -1,6 +1,6 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 -import MeeGo.Connman 0.2 +import Connman 0.2 ComboBox { //% "PEAP version" diff --git a/usr/share/jolla-settings/pages/wlan/ProxyForm.qml b/usr/share/jolla-settings/pages/wlan/ProxyForm.qml deleted file mode 100644 index fd15eae6..00000000 --- a/usr/share/jolla-settings/pages/wlan/ProxyForm.qml +++ /dev/null @@ -1,108 +0,0 @@ -import QtQuick 2.6 -import Sailfish.Silica 1.0 - -Column { - id: root - property QtObject network - property alias currentIndex: proxyCombo.currentIndex - property alias proxyLoader: proxyLoader - - width: parent.width - opacity: enabled ? 1.0 : Theme.opacityLow - - function methodStringToInteger(method) { - if (method === "manual") { - return 1 - } else if (method === "auto") { - return 2 - } else { - return 0 - } - } - - Connections { - target: network - onProxyConfigChanged: { - var configIndex = methodStringToInteger(network.proxyConfig["Method"]) - if (proxyCombo.currentIndex !== configIndex) { - proxyLoader.item.updating = true - proxyLoader.focus = false - proxyCombo.currentIndex = configIndex - } - } - } - - ComboBox { - id: proxyCombo - - onCurrentIndexChanged: { - var proxyConfig = network.proxyConfig - proxyLoader.item.updating = true - - if (currentIndex === 0) { - proxyConfig["Method"] = "direct" - network.proxyConfig = proxyConfig - } - } - - Component.onCompleted: { - var method = network.proxyConfig["Method"] - - currentIndex = methodStringToInteger(method) - } - - //: Referring to the network proxy method to use for this connection - //% "Proxy configuration" - label: qsTrId("settings_network-la-proxy_configuration") - menu: ContextMenu { - MenuItem { - //% "No proxies" - text: qsTrId("settings_network-me-no_proxies") - } - MenuItem { - //% "Manual" - text: qsTrId("settings_network-me-manual") - } - MenuItem { - //% "Automatic" - text: qsTrId("settings_network-me-automatic") - } - } - } - - Loader { - id: proxyLoader - width: parent.width - sourceComponent: { - var index = proxyCombo.currentIndex - if (index === 0) { - return fakeEmptyItem - } else if (index === 1) { - return manualProxy - } else if (index === 2) { - return autoProxy - } - } - } - - // this is a workaround for Loader not reseting its height when sourceComponent is undefined - Component { - id: fakeEmptyItem - - Item { - property bool updating: false - } - } - - Component { - id: manualProxy - - ManualProxyForm { network: root.network } - } - - Component { - id: autoProxy - - AutoProxyForm { network: root.network } - } -} diff --git a/usr/share/jolla-settings/pages/wlan/NetworkItemDelegate.qml b/usr/share/jolla-settings/pages/wlan/WlanItem.qml similarity index 96% rename from usr/share/jolla-settings/pages/wlan/NetworkItemDelegate.qml rename to usr/share/jolla-settings/pages/wlan/WlanItem.qml index 7db54b75..047dfb20 100644 --- a/usr/share/jolla-settings/pages/wlan/NetworkItemDelegate.qml +++ b/usr/share/jolla-settings/pages/wlan/WlanItem.qml @@ -87,10 +87,11 @@ ListItem { } } MenuItem { - //% "Edit" - text: qsTrId("settings_network-me-edit") - onClicked: pageStack.animatorPush("AdvancedSettingsPage.qml", {"network": networkService}) + //% "Details" + text: qsTrId("settings_network-me-details") + onClicked: pageStack.animatorPush("NetworkDetailsPage.qml", {"network": networkService}) } + onActiveChanged: mainPage.suppressScan = active } } diff --git a/usr/share/jolla-settings/pages/wlan/EnableSwitch.qml b/usr/share/jolla-settings/pages/wlan/WlanSwitch.qml similarity index 97% rename from usr/share/jolla-settings/pages/wlan/EnableSwitch.qml rename to usr/share/jolla-settings/pages/wlan/WlanSwitch.qml index 99838673..3bce9e23 100644 --- a/usr/share/jolla-settings/pages/wlan/EnableSwitch.qml +++ b/usr/share/jolla-settings/pages/wlan/WlanSwitch.qml @@ -2,7 +2,7 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 import Sailfish.Policy 1.0 import Sailfish.Settings.Networking 1.0 as Networking -import MeeGo.Connman 0.2 +import Connman 0.2 import Nemo.Configuration 1.0 import Nemo.DBus 2.0 import com.jolla.connection 1.0 @@ -39,7 +39,7 @@ SettingsToggle { if (!AccessPolicy.wlanToggleEnabled) { errorNotification.notify(SettingsControlError.BlockedByAccessPolicy) } else if (wifiTechnology.tethering) { - connectionAgent.stopTethering(true) + connectionAgent.stopTethering("wifi", true) } else { wifiTechnology.powered = !wifiTechnology.powered if (wifiTechnology.powered) { diff --git a/usr/share/jolla-settings/pages/wlan/mainpage.qml b/usr/share/jolla-settings/pages/wlan/mainpage.qml index 6ed65eec..4516366a 100644 --- a/usr/share/jolla-settings/pages/wlan/mainpage.qml +++ b/usr/share/jolla-settings/pages/wlan/mainpage.qml @@ -1,5 +1,5 @@ import QtQuick 2.0 -import MeeGo.Connman 0.2 +import Connman 0.2 import Sailfish.Silica 1.0 import Sailfish.Policy 1.0 import com.jolla.settings 1.0 @@ -132,7 +132,7 @@ Page { onClicked: { if (wifiTechnology.tethering) - connectionAgent.stopTethering(true) + connectionAgent.stopTethering("wifi", true) else wifiListModel.powered = !wifiListModel.powered } @@ -183,7 +183,7 @@ Page { model: wifiListModel.available ? savedNetworks : null - delegate: NetworkItemDelegate { width: parent.width } + delegate: WlanItem { width: parent.width } // This is shown when connman is completely broken Component { diff --git a/usr/share/jolla-settings/settings.qml b/usr/share/jolla-settings/settings.qml index 528ef8cc..e9b63bd2 100644 --- a/usr/share/jolla-settings/settings.qml +++ b/usr/share/jolla-settings/settings.qml @@ -2,8 +2,8 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 // Load translations import com.jolla.settings 1.0 -import org.nemomobile.dbus 2.0 -import org.nemomobile.notifications 1.0 +import Nemo.DBus 2.0 +import Nemo.Notifications 1.0 import "./pages" ApplicationWindow { diff --git a/usr/share/jolla-startupwizard-pre-user-session/main.qml b/usr/share/jolla-startupwizard-pre-user-session/main.qml index 7ba6b604..e58690da 100644 --- a/usr/share/jolla-startupwizard-pre-user-session/main.qml +++ b/usr/share/jolla-startupwizard-pre-user-session/main.qml @@ -12,7 +12,8 @@ import Sailfish.Lipstick 1.0 import com.jolla.settings.system 1.0 import com.jolla.startupwizard 1.0 import org.nemomobile.systemsettings 1.0 -import org.nemomobile.configuration 1.0 +import Nemo.Configuration 1.0 +import QtQuick.Window 2.2 as QtQuick // Don't use ApplicationWindow as it involves covers and other features that can't be handled // before the user session begins. @@ -132,8 +133,8 @@ Window { pageStack.animatorPush(welcomeComponent) } - width: Screen.width - height: Screen.height + width: root.QtQuick.Screen.primaryOrientation === Qt.PortraitOrientation ? Screen.width : Screen.height + height: root.QtQuick.Screen.primaryOrientation === Qt.PortraitOrientation ? Screen.height : Screen.width StartupWizardManager { id: wizardManager diff --git a/usr/share/jolla-startupwizard/DeviceLockDialog.qml b/usr/share/jolla-startupwizard/DeviceLockDialog.qml index 35e90f75..c0ee6766 100644 --- a/usr/share/jolla-startupwizard/DeviceLockDialog.qml +++ b/usr/share/jolla-startupwizard/DeviceLockDialog.qml @@ -36,8 +36,6 @@ MandatoryDeviceLockInputPage { //% "User data encrypted" subTitleText: homeEncrypted && !lockCodeSet ? qsTrId("startupwizard-la-user_data_encrypted") : "" - //% "Skip" - cancelText: qsTrId("startupwizard-la-skip_security_code") showCancelButton: !homeEncrypted onStatusChanged: { diff --git a/usr/share/jolla-startupwizard/SUWAccountCreationManager.qml b/usr/share/jolla-startupwizard/SUWAccountCreationManager.qml new file mode 100644 index 00000000..37f44102 --- /dev/null +++ b/usr/share/jolla-startupwizard/SUWAccountCreationManager.qml @@ -0,0 +1,15 @@ +/* + * Copyright (c) 2022 Jolla Ltd. + * + * License: Proprietary + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import com.jolla.settings.accounts 1.0 + +AccountCreationManager { + endDestination: _pageAfterAccountSetup + endDestinationAction: PageStackAction.Replace + endDestinationReplaceTarget: null +} diff --git a/usr/share/jolla-startupwizard/SUWAccountFactory.qml b/usr/share/jolla-startupwizard/SUWAccountFactory.qml new file mode 100644 index 00000000..94127b37 --- /dev/null +++ b/usr/share/jolla-startupwizard/SUWAccountFactory.qml @@ -0,0 +1,10 @@ +/* + * Copyright (c) 2022 Jolla Ltd. + * + * License: Proprietary + */ + +import QtQuick 2.0 +import com.jolla.settings.accounts 1.0 + +AccountFactory {} diff --git a/usr/share/jolla-startupwizard/SUWWizardPostAccountCreationDialog.qml b/usr/share/jolla-startupwizard/SUWWizardPostAccountCreationDialog.qml new file mode 100644 index 00000000..8ad0d553 --- /dev/null +++ b/usr/share/jolla-startupwizard/SUWWizardPostAccountCreationDialog.qml @@ -0,0 +1,16 @@ +/* + * Copyright (c) 2022 Jolla Ltd. + * + * License: Proprietary + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import com.jolla.startupwizard 1.0 + +WizardPostAccountCreationDialog { + endDestination: _pageAfterAccountSetup + endDestinationAction: PageStackAction.Replace + endDestinationReplaceTarget: null + backNavigation: false +} diff --git a/usr/share/jolla-startupwizard/main.qml b/usr/share/jolla-startupwizard/main.qml index 5aac3b22..d7d78ab7 100644 --- a/usr/share/jolla-startupwizard/main.qml +++ b/usr/share/jolla-startupwizard/main.qml @@ -8,13 +8,13 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 import Sailfish.Silica.private 1.0 +import Sailfish.Settings.Networking 1.0 import com.jolla.startupwizard 1.0 import com.jolla.settings.system 1.0 -import com.jolla.settings.accounts 1.0 -import org.nemomobile.configuration 1.0 +import Nemo.Configuration 1.0 import org.nemomobile.devicelock 1.0 -import MeeGo.QOfono 0.2 +import QOfono 0.2 import Sailfish.AccessControl 1.0 import Sailfish.Policy 1.0 @@ -26,7 +26,8 @@ ApplicationWindow { property bool _internetConnectionSkipped property int _modemIndex property bool _pinRequested - property variant _fingerprintAuthenticationToken + property var _fingerprintAuthenticationToken + property bool _enteredTutorial property Component _firstPageAfterPinQuery: { if (reachedTutorialConf.value === true) { @@ -46,8 +47,10 @@ ApplicationWindow { readonly property Component tutorialMainComponent: { var tutorial = Qt.createComponent(pageStack.resolveImportPage("Sailfish.Tutorial.TutorialEntryPage")) if (tutorial.status != Component.Ready) { + console.log("No Tutorial installed, skipping.") tutorial = noTutorialComponent } + return tutorial } @@ -55,21 +58,19 @@ ApplicationWindow { ofonoInitTimeout.stop() var pageComponent = showSimPinQuery ? pinQueryComponent : _firstPageAfterPinQuery var page = pageStack.replace(pageComponent, {}, PageStackAction.Immediate) - if (pageComponent == tutorialMainComponent) { - if (page.status === PageStatus.Active) { - // don't allow back navigation from the tutorial - page.backNavigation = false - tutorialExitConf() - } - } } function _accountSetupPage() { if (root._internetConnectionSkipped) { return root._pageAfterAccountSetup - } else if (accountFactory.jollaAccountExists()) { // Store exists for account (TODO JB#47405 : should be fixed for jolla-settings-account) - console.log("User already has a", _accountName, "account, skipping", _accountName, "account creation. (Ignore the upcoming 'Great, your", _accountName, "account was added' message.)") - return storeAccountAlreadyExistsComponent + } else if (accountFactory.item && accountFactory.item.jollaAccountExists()) { + // Store exists for account (TODO JB#47405 : should be fixed for jolla-settings-account) + console.log("User already has a", _accountName, "account, skipping", _accountName, + "account creation. (Ignore the upcoming 'Great, your", _accountName, + "account was added' message.)") + // This is only used if a store account already exists when the SUW is run; otherwise, the + // Settings account creation flow takes care of triggering this flow. + return Qt.createComponent(Qt.resolvedUrl("SUWWizardPostAccountCreationDialog.qml")) } else { return _createStoreAccountPage() } @@ -80,7 +81,9 @@ ApplicationWindow { root._accountPage.destroy() } var props = { "wizardMode": true, "runningFromSettingsApp": false } - root._accountPage = accountCreator.accountCreationPageForProvider(_accountName.toLowerCase(), props) + root._accountPage = accountCreationManager.item + ? accountCreationManager.item.accountCreationPageForProvider(_accountName.toLowerCase(), props) + : null return root._accountPage || root._pageAfterAccountSetup } @@ -99,6 +102,19 @@ ApplicationWindow { initialPage: busyWaitComponent + Connections { + target: pageStack + onCurrentPageChanged: { + // detect if we entered the tutorial + if (!root._enteredTutorial && pageStack.currentPage.hasOwnProperty("allowSystemGesturesBetweenLessons")) { + root._enteredTutorial = true + // don't allow back navigation from the tutorial + pageStack.currentPage.backNavigation = false + tutorialExitConf() + } + } + } + Component { id: busyWaitComponent Page { @@ -116,8 +132,16 @@ ApplicationWindow { id: wizardManager } - AccountFactory { + Loader { + id: accountCreationManager + + source: Qt.resolvedUrl("SUWAccountCreationManager.qml") + } + + Loader { id: accountFactory + + source: Qt.resolvedUrl("SUWAccountFactory.qml") } ScreenBlank { @@ -128,13 +152,6 @@ ApplicationWindow { key: "/apps/jolla-startupwizard/reached_tutorial" } - AccountCreationManager { - id: accountCreator - endDestination: root._pageAfterAccountSetup - endDestinationAction: PageStackAction.Replace - endDestinationReplaceTarget: null - } - PersonalizedNamingSetup { id: personalizedNaming } @@ -216,6 +233,7 @@ ApplicationWindow { Component { id: networkCheckComponent + NetworkCheckDialog { readonly property bool dateTimeSettingsEnabled: AccessPolicy.dateTimeSettingsEnabled && AccessControl.hasGroup(AccessControl.RealUid, "sailfish-datetime") @@ -225,16 +243,19 @@ ApplicationWindow { //% "Setting up your internet connection at this point is highly recommended" headingText: qsTrId("startupwizard-he-internet_connection_heading") - //% "With an internet connection you can set up your Store account and download essential apps now. You'll also be able to access the Store and OS updates immediately after setting up your account." + //% "With an internet connection you can set up your Store account and download essential apps now. " + //% "You'll also be able to access the Store and OS updates immediately after setting up your account." bodyText: _accountPage ? qsTrId("startupwizard-la-internet_connection_body") : "" - skipText: (_accountPage ? - //: Skip text if user doesn't want to set up the internet connection at the moment. (Text surrounded by %1 and %2 is underlined and colored differently) - //% "%1Skip%2 internet connection setup and set up my Store account later" - qsTrId("startupwizard-la-skip_internet_connection_account_specific") : - //: Skip text if user doesn't want to set up the internet connection at the moment. (Text surrounded by %1 and %2 is underlined and colored differently) - //% "%1Skip%2 internet connection setup" - qsTrId("startupwizard-la-skip_internet_connection")) + skipText: (_accountPage + ? //: Skip text if user doesn't want to set up the internet connection at the moment. + //: (Text surrounded by %1 and %2 is underlined and colored differently) + //% "%1Skip%2 internet connection setup and set up my Store account later" + qsTrId("startupwizard-la-skip_internet_connection_account_specific") + : //: Skip text if user doesn't want to set up the internet connection at the moment. + //: (Text surrounded by %1 and %2 is underlined and colored differently) + //% "%1Skip%2 internet connection setup" + qsTrId("startupwizard-la-skip_internet_connection")) .arg("") .arg("") @@ -263,27 +284,6 @@ ApplicationWindow { acceptDestination = root._accountSetupPage() } } - - onAccepted: { - if (acceptDestination == tutorialMainComponent) { - // Entering tutorial so disable backNavigation - acceptDestinationInstance.backNavigation = false - tutorialExitConf() - } - } - } - } - - // This is only used if a store account already exists when the SUW is run; otherwise, the - // Settings account creation flow takes care of triggering this flow. - Component { - id: storeAccountAlreadyExistsComponent - - WizardPostAccountCreationDialog { - endDestination: root._pageAfterAccountSetup - endDestinationAction: PageStackAction.Replace - endDestinationReplaceTarget: null - backNavigation: false } } @@ -334,7 +334,7 @@ ApplicationWindow { Page { onStatusChanged: { - if (status = PageStatus.Active) { + if (status == PageStatus.Active) { tutorialExitConf() // Tutorial not installed so just quit Qt.quit() @@ -342,19 +342,4 @@ ApplicationWindow { } } } - - // ---- the strings below aren't used yet; they have been added to get the translations done in - // ----- in preparation for implementing JB#27908 - - //: Heading when user is asked to provide the current location - //% "Welcome! Where do you live?" - property string _countryPickerHeading: qsTrId("startupwizard-he-welcome_where_do_you_live") - - //: Explains why user's location is being requested. It is used to define the WLAN frequences that can be used by the device. - //% "This information is needed to define the allowed WLAN frequencies." - property string _countryPickerIntro: qsTrId("startupwizard-la-country_requested_for_wlan") - - //: Allows user to choose current location from a list of countries, regions etc. if the automatically-chosen location was incorrect. - //% "If we didn't guess correctly, please select your area below." - property string _countryPickerFallbackExplanation: qsTrId("startupwizard-la-didnt_guess_area_correctly") } diff --git a/usr/share/lipstick-appsupport-ui/PermissionQueryWindow.qml b/usr/share/lipstick-appsupport-ui/PermissionQueryWindow.qml new file mode 100644 index 00000000..aac654e0 --- /dev/null +++ b/usr/share/lipstick-appsupport-ui/PermissionQueryWindow.qml @@ -0,0 +1,248 @@ +/* + * Copyright (c) 2024 Jolla Ltd. + * + * License: Proprietary + */ + +import QtQuick 2.6 +import QtQuick.Window 2.0 as QtQuick +import Sailfish.Silica 1.0 +import Sailfish.Bluetooth 1.0 +import Sailfish.Lipstick 1.0 + +SystemDialog { + id: root + + readonly property int buttonAllow: 1 + readonly property int buttonAllowAlways: 2 + readonly property int buttonAllowForeground: 4 + readonly property int buttonDeny: 8 + readonly property int buttonDenyAndDontAsk: 16 + readonly property int buttonAllowOnce: 32 + readonly property int buttonNoUpgrade: 64 + readonly property int buttonNoUpgradeAndDontAsk: 128 + readonly property int buttonNoUpgradeOnce: 256 + readonly property int buttonNoUpgradeOnceAndDontAsk: 512 + readonly property int buttonLinkToSettings: 1024 + + readonly property int replyLinkedToSettings: -2 + readonly property int replyCanceled: -1 + readonly property int replyGrantedAlways: 0 + readonly property int replyGrantedForegroundOnly: 1 + readonly property int replyDenied: 2 + readonly property int replyDeniedDoNotAsk: 3 + readonly property int replyGrantedOnce: 4 + + property string uuid + property string message + property string detailedMessage + property int buttonVisibility + property string groupName + property var buttonTexts + + property bool windowVisible: visibility != QtQuick.Window.Hidden + && visibility != QtQuick.Window.Minimized + + signal done(string uuid, int result) + + function init(uuid, buttonVisibility, groupName, message, detailedMessage, buttonTexts) { + root.uuid = uuid + root.buttonVisibility = buttonVisibility + root.message = message + root.detailedMessage = detailedMessage + root.groupName = groupName + root.buttonTexts = buttonTexts + + raise() + show() + } + + autoDismiss: true + contentHeight: content.height + + onDismissed: { + root.done(uuid, replyCanceled) + } + + Column { + id: content + width: parent.width + topPadding: Math.max(Theme.paddingLarge * 3, + (root.orientation == Qt.PortraitOrientation && Screen.topCutout.height > 0) + ? Screen.topCutout.height + Theme.paddingSmall : 0) + bottomPadding: Theme.paddingLarge + + Image { + anchors.horizontalCenter: parent.horizontalCenter + horizontalAlignment: Image.AlignHCenter + verticalAlignment: Image.AlignVCenter + fillMode: Image.Pad + + source: { + if (root.groupName === "android.permission-group.STORAGE") + return "image://theme/icon-m-storage" + else if (root.groupName === "android.permission-group.CAMERA") + return "image://theme/icon-m-camera" + else if (root.groupName === "android.permission-group.MICROPHONE") + return "image://theme/icon-m-mic" + else if (root.groupName === "android.permission-group.CONTACTS") + return "image://theme/icon-m-contact" + else if (root.groupName === "android.permission-group.CALENDAR") + return "image://theme/icon-m-alarm" + else if (root.groupName === "android.permission-group.LOCATION") + return "image://theme/icon-m-location" + else if (root.groupName === "android.permission-group.PHONE_CALLS") + return "image://theme/icon-m-call" + else if (root.groupName === "android.permission-group.PHONE") + return "image://theme/icon-m-call" + else if (root.groupName === "android.permission-group.SMS") + return "image://theme/icon-m-sms" + else + return "image://theme/icon-m-question" + } + } + + SystemDialogHeader { + topPadding: 2 * Theme.paddingLarge + // These are received in translated state + title: root.message + description: root.detailedMessage + } + + Column { + width: parent.width + + SystemDialogTextButton { + width: parent.width + visible: root.buttonVisibility & buttonAllow + bottomPadding: topPadding + + text: root.buttonTexts[0] + + onClicked: { + root.done(root.uuid, replyGrantedAlways) + } + } + + SystemDialogTextButton { + width: parent.width + visible: root.buttonVisibility & buttonAllowAlways + bottomPadding: topPadding + + text: root.buttonTexts[1] + + onClicked: { + root.done(root.uuid, replyGrantedAlways) + } + } + + SystemDialogTextButton { + width: parent.width + visible: root.buttonVisibility & buttonAllowForeground + bottomPadding: topPadding + + text: root.buttonTexts[2] + + onClicked: { + root.done(root.uuid, replyGrantedForegroundOnly) + } + } + + SystemDialogTextButton { + width: parent.width + visible: root.buttonVisibility & buttonAllowOnce + bottomPadding: topPadding + + text: root.buttonTexts[5] + + onClicked: { + root.done(root.uuid, replyGrantedOnce) + } + } + + SystemDialogTextButton { + width: parent.width + visible: root.buttonVisibility & buttonDeny + bottomPadding: topPadding + + text: root.buttonTexts[3] + + onClicked: { + root.done(root.uuid, replyDenied) + } + } + + SystemDialogTextButton { + width: parent.width + visible: root.buttonVisibility & buttonDenyAndDontAsk + bottomPadding: topPadding + + text: root.buttonTexts[4] + + onClicked: { + root.done(root.uuid, replyDeniedDoNotAsk) + } + } + + SystemDialogTextButton { + width: parent.width + visible: root.buttonVisibility & buttonNoUpgrade + bottomPadding: topPadding + + text: root.buttonTexts[6] + + onClicked: { + root.done(root.uuid, replyDenied) + } + } + + SystemDialogTextButton { + width: parent.width + visible: root.buttonVisibility & buttonNoUpgradeAndDontAsk + bottomPadding: topPadding + + text: root.buttonTexts[7] + + onClicked: { + root.done(root.uuid, replyDeniedDoNotAsk) + } + } + + SystemDialogTextButton { + width: parent.width + visible: root.buttonVisibility & buttonNoUpgradeOnce + bottomPadding: topPadding + + text: root.buttonTexts[8] + + onClicked: { + root.done(root.uuid, replyDenied) + } + } + + SystemDialogTextButton { + width: parent.width + visible: root.buttonVisibility & buttonNoUpgradeOnceAndDontAsk + bottomPadding: topPadding + + text: root.buttonTexts[9] + + onClicked: { + root.done(root.uuid, replyDeniedDoNotAsk) + } + } + + SystemDialogTextButton { + width: parent.width + visible: root.buttonVisibility & buttonLinkToSettings + bottomPadding: topPadding + + text: root.buttonTexts[10] + + onClicked: { + root.done(root.uuid, replyLinkedToSettings) + } + } + } + } +} diff --git a/usr/share/lipstick-appsupport-ui/main.qml b/usr/share/lipstick-appsupport-ui/main.qml new file mode 100644 index 00000000..0605d900 --- /dev/null +++ b/usr/share/lipstick-appsupport-ui/main.qml @@ -0,0 +1,139 @@ +/* + * Copyright (c) 2024 Jolla Ltd. + * + * License: Proprietary + */ + +import QtQuick 2.6 +import QtQuick.Window 2.0 +import Sailfish.Silica 1.0 +import Sailfish.Lipstick 1.0 +import Nemo.DBus 2.0 + +ApplicationWindow { + id: root + + property PermissionQueryWindow _permissionQueryWindow + property string uuid + property int queryCount + property int queryIndex + property bool replied + + Component.onCompleted: { + delayedQuit.restart() + } + + function _showConsentWindow(uuid, buttons, groupName, message, detailedMessage, buttonTexts) { + root.replied = false + if (!_permissionQueryWindow) { + var comp = Qt.createComponent(Qt.resolvedUrl("PermissionQueryWindow.qml")) + if (comp.status == Component.Error) { + console.log("PermissionQueryWindow.qml error:", comp.errorString()) + return + } + _permissionQueryWindow = comp.createObject(root) + _permissionQueryWindow.done.connect(function(uuid, result) { + if (!root.replied) { + dbusClient.reply(uuid, result) + root.replied = true + } + if (result == _permissionQueryWindow.replyCanceled || root.queryIndex >= root.queryCount - 1) { + root._closeWindow() + delayedQuit.restart() + } + }) + } + _permissionQueryWindow.init(uuid, buttons, groupName, message, detailedMessage, buttonTexts) + } + + function _closeWindow() { + if (_permissionQueryWindow + && _permissionQueryWindow.visibility != Window.Hidden) { + _permissionQueryWindow.lower() + } + } + + allowedOrientations: defaultAllowedOrientations + _defaultPageOrientations: Orientation.All + _defaultLabelFormat: Text.PlainText + cover: undefined + + Timer { + id: delayedQuit + interval: 10000 + onTriggered: { + console.log("lipstick-appsupport-ui: exiting...") + Qt.quit() + } + } + + DBusInterface { + id: dbusClient + bus: DBus.SessionBus + + service: 'com.jolla.appsupport.permissions' + path: '/com/jolla/appsupport/permissions' + iface: 'com.jolla.appsupport.permissions' + + function reply(key, result) { + call('consentReply', [ key, result ]) + } + } + + DBusAdaptor { + id: dbusService + + service: 'com.jolla.appsupport.consent' + iface: 'com.jolla.appsupport.consent' + path: '/com/jolla/appsupport/consent' + + xml: ' \n' + + ' \n' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' \n' + + ' \n' + + ' \n' + + ' \n' + + ' ' + + ' \n' + + ' \n' + + function checkConsent(uuid, + count, + index, + buttons, + groupName, + packageName, + appName, + message, + detailedMessage, + buttonTexts) { + delayedQuit.restart() + root.uuid = uuid + root.queryCount = count + root.queryIndex = index + root._showConsentWindow(uuid, buttons, groupName, message, detailedMessage, buttonTexts) + return 0 + } + + function cancel() { + root._closeWindow() + delayedQuit.restart() + } + + function watchdog() { + delayedQuit.restart() + return root.uuid + } + } +} diff --git a/usr/share/lipstick-bluetooth-ui/BluetoothAuthorizationWindow.qml b/usr/share/lipstick-bluetooth-ui/BluetoothAuthorizationWindow.qml index 55887bcf..ff253b79 100644 --- a/usr/share/lipstick-bluetooth-ui/BluetoothAuthorizationWindow.qml +++ b/usr/share/lipstick-bluetooth-ui/BluetoothAuthorizationWindow.qml @@ -5,7 +5,7 @@ ** ****************************************************************************/ -import QtQuick 2.0 +import QtQuick 2.6 import QtQuick.Window 2.0 import Sailfish.Silica 1.0 import Sailfish.Bluetooth 1.0 @@ -63,8 +63,6 @@ SystemDialog { width: parent.width SystemDialogHeader { - id: header - //: Another Bluetooth device has requested a connection to this device //% "Connection request" title: qsTrId("lipstick-jolla-home-he-connection_request") @@ -99,8 +97,8 @@ SystemDialog { SystemDialogTextButton { id: cancelButton - width: root.width / 2 + width: root.width / 2 //: Disallow the other Bluetooth device from connecting to this one //% "No" text: qsTrId("lipstick-jolla-home-la-service_connect_deny") @@ -113,9 +111,9 @@ SystemDialog { SystemDialogTextButton { id: confirmButton + anchors.right: parent.right width: root.width / 2 - //: Allow the other Bluetooth device to connect to this one //% "Yes" text: qsTrId("lipstick-jolla-home-la-service_connect_allow") diff --git a/usr/share/lipstick-bluetooth-ui/BluetoothPairing.qml b/usr/share/lipstick-bluetooth-ui/BluetoothPairing.qml index 00c847bc..e9e589b2 100644 --- a/usr/share/lipstick-bluetooth-ui/BluetoothPairing.qml +++ b/usr/share/lipstick-bluetooth-ui/BluetoothPairing.qml @@ -5,11 +5,11 @@ ** ****************************************************************************/ -import QtQuick 2.0 +import QtQuick 2.6 import QtQuick.Window 2.0 import Sailfish.Silica 1.0 import Sailfish.Bluetooth 1.0 -import org.nemomobile.notifications 1.0 as Nemo +import Nemo.Notifications 1.0 as Nemo import org.kde.bluezqt 1.0 as BluezQt ApplicationWindow { diff --git a/usr/share/lipstick-bluetooth-ui/BluetoothPairingWindow.qml b/usr/share/lipstick-bluetooth-ui/BluetoothPairingWindow.qml index 873fdb8b..b400ea27 100644 --- a/usr/share/lipstick-bluetooth-ui/BluetoothPairingWindow.qml +++ b/usr/share/lipstick-bluetooth-ui/BluetoothPairingWindow.qml @@ -5,7 +5,7 @@ ** ****************************************************************************/ -import QtQuick 2.0 +import QtQuick 2.6 import QtQuick.Window 2.0 import Nemo.DBus 2.0 import Sailfish.Silica 1.0 @@ -299,6 +299,7 @@ SystemDialog { BusyIndicator { id: busyIndicator + size: BusyIndicatorSize.Large anchors.horizontalCenter: parent.horizontalCenter visible: running @@ -306,6 +307,7 @@ SystemDialog { Label { id: passkeyLabel + width: parent.width color: Theme.highlightColor font.pixelSize: Theme.fontSizeExtraLarge @@ -317,6 +319,7 @@ SystemDialog { TextField { id: passkeyInputField + anchors.horizontalCenter: parent.horizontalCenter width: parent.width - (Theme.paddingLarge * 4) @@ -335,7 +338,7 @@ SystemDialog { EnterKey.enabled: text || inputMethodComposing EnterKey.iconSource: "image://theme/icon-m-enter-close" - EnterKey.onClicked: root.focus = true + EnterKey.onClicked: content.focus = true } TrustBluetoothDeviceSwitch { @@ -359,6 +362,7 @@ SystemDialog { SystemDialogTextButton { id: cancelButton + x: confirmButton.visible ? 0 : (parent.width/2) - (width/2) y: parent.height - height width: confirmButton.visible ? root.width / 2 : root.width @@ -376,6 +380,7 @@ SystemDialog { SystemDialogTextButton { id: confirmButton + x: cancelButton.visible ? parent.width - width : (parent.width/2) - (width/2) y: parent.height - height width: cancelButton.visible ? root.width / 2 : root.width diff --git a/usr/share/lipstick-bluetooth-ui/main.qml b/usr/share/lipstick-bluetooth-ui/main.qml index ea39f7e2..6918d8c1 100644 --- a/usr/share/lipstick-bluetooth-ui/main.qml +++ b/usr/share/lipstick-bluetooth-ui/main.qml @@ -5,13 +5,13 @@ ** ****************************************************************************/ -import QtQuick 2.0 +import QtQuick 2.6 import QtQuick.Window 2.0 import Sailfish.Silica 1.0 import Sailfish.Bluetooth 1.0 import Sailfish.Lipstick 1.0 import Nemo.DBus 2.0 -import org.nemomobile.notifications 1.0 as Nemo +import Nemo.Notifications 1.0 as Nemo import org.kde.bluezqt 1.0 as BluezQt import com.jolla.lipstick 0.1 @@ -20,7 +20,6 @@ ApplicationWindow { property BluetoothAuthorizationWindow _serviceAuthWindow property QtObject bluetoothManager: BluezQt.Manager - property bool monitorManagerObjectChanges: true property bool windowsVisible: pairing._windowVisible || (_serviceAuthWindow && _serviceAuthWindow.windowVisible) readonly property bool keepAlive: windowsVisible @@ -55,7 +54,6 @@ ApplicationWindow { if (_serviceAuthWindow.windowVisible) { _serviceAuthWindow.lower() } - root.monitorManagerObjectChanges = false return true } return false @@ -88,14 +86,15 @@ ApplicationWindow { SystemDialog { function execute() { //% "Turning Bluetooth off" - remorse.execute(qsTrId("lipstick-jolla-home-la-bluetoothoff")); + remorse.execute(qsTrId("lipstick-jolla-home-la-bluetoothoff")) } function cancel() { remorse.cancel() } - contentHeight: remorse.height + // the popup wouldn't per se need a background, but 0 sized dialog blurs the view + contentHeight: remorse.height + remorse.y + Theme.paddingSmall visible: true onDismissed: remorse.trigger() @@ -199,22 +198,18 @@ ApplicationWindow { signal finishAction(int error) onInitiatedPairingRequest: { - root.monitorManagerObjectChanges = true pairing.initiatedPairingRequest(deviceAddress, deviceName) } onAgentPairingAction: { - root.monitorManagerObjectChanges = true pairing.agentPairingAction(deviceAddress, deviceName, action, passkey, requestId) } onAgentServiceAuthorizationAction: { - root.monitorManagerObjectChanges = true root._agentServiceAuthorizationRequest(deviceAddress, deviceName, uuid, requestId) } onFinishAction: { - root.monitorManagerObjectChanges = false if (pairing.finishPairing(error)) { return } @@ -223,10 +218,4 @@ ApplicationWindow { } } } - - Binding { - target: bluetoothManager - property: "monitorObjectManagerInterfaces" - value: root.monitorManagerObjectChanges - } } diff --git a/usr/share/lipstick-jolla-home-qt5/backgrounds/AlarmBackground.qml b/usr/share/lipstick-jolla-home-qt5/backgrounds/AlarmBackground.qml index a49c7a39..8e37ddbd 100644 --- a/usr/share/lipstick-jolla-home-qt5/backgrounds/AlarmBackground.qml +++ b/usr/share/lipstick-jolla-home-qt5/backgrounds/AlarmBackground.qml @@ -5,7 +5,7 @@ * License: Proprietary */ -import QtQuick 2.0 +import QtQuick 2.6 import Sailfish.Silica 1.0 import org.nemomobile.lipstick 0.1 diff --git a/usr/share/lipstick-jolla-home-qt5/backgrounds/AmbienceBackgroundLoader.qml b/usr/share/lipstick-jolla-home-qt5/backgrounds/AmbienceBackgroundLoader.qml index b794a64c..229c6f52 100644 --- a/usr/share/lipstick-jolla-home-qt5/backgrounds/AmbienceBackgroundLoader.qml +++ b/usr/share/lipstick-jolla-home-qt5/backgrounds/AmbienceBackgroundLoader.qml @@ -15,8 +15,8 @@ SynchronizedWallpaperLoader { function properties(item, ambience) { var properties = { "sourceItem": item, - "colorScheme": ambience.colorScheme, - "highlightColor": ambience.highlightColor + "colorScheme": ambience ? ambience.colorScheme : Theme.LightOnDark, + "highlightColor": ambience ? ambience.highlightColor : Theme.highlightColor } for (var property in applicationProperties) { diff --git a/usr/share/lipstick-jolla-home-qt5/backgrounds/BlurFilterSingleton.qml b/usr/share/lipstick-jolla-home-qt5/backgrounds/BlurFilterSingleton.qml index 495ecf7e..18e2b161 100644 --- a/usr/share/lipstick-jolla-home-qt5/backgrounds/BlurFilterSingleton.qml +++ b/usr/share/lipstick-jolla-home-qt5/backgrounds/BlurFilterSingleton.qml @@ -6,7 +6,6 @@ import QtQuick 2.6 import Sailfish.Silica.Background 1.0 -import Sailfish.Ambience 1.0 import com.jolla.lipstick 0.1 GlassBlur { diff --git a/usr/share/lipstick-jolla-home-qt5/backgrounds/BlurredBackground.qml b/usr/share/lipstick-jolla-home-qt5/backgrounds/BlurredBackground.qml index 7fcb1b55..a4df025c 100644 --- a/usr/share/lipstick-jolla-home-qt5/backgrounds/BlurredBackground.qml +++ b/usr/share/lipstick-jolla-home-qt5/backgrounds/BlurredBackground.qml @@ -1,4 +1,4 @@ -import QtQuick 2.1 +import QtQuick 2.6 import Sailfish.Silica 1.0 import Sailfish.Silica.Background 1.0 import org.nemomobile.lipstick 0.1 diff --git a/usr/share/lipstick-jolla-home-qt5/backgrounds/DialogBackground.qml b/usr/share/lipstick-jolla-home-qt5/backgrounds/DialogBackground.qml index 9979e0af..61ba6d71 100644 --- a/usr/share/lipstick-jolla-home-qt5/backgrounds/DialogBackground.qml +++ b/usr/share/lipstick-jolla-home-qt5/backgrounds/DialogBackground.qml @@ -5,7 +5,7 @@ * License: Proprietary */ -import QtQuick 2.0 +import QtQuick 2.6 import Sailfish.Silica 1.0 import org.nemomobile.lipstick 0.1 diff --git a/usr/share/lipstick-jolla-home-qt5/backgrounds/DimmedBackground.qml b/usr/share/lipstick-jolla-home-qt5/backgrounds/DimmedBackground.qml index 17263195..b14e4661 100644 --- a/usr/share/lipstick-jolla-home-qt5/backgrounds/DimmedBackground.qml +++ b/usr/share/lipstick-jolla-home-qt5/backgrounds/DimmedBackground.qml @@ -5,7 +5,7 @@ * License: Proprietary */ -import QtQuick 2.0 +import QtQuick 2.6 import Sailfish.Silica 1.0 import Sailfish.Silica.Background 1.0 import org.nemomobile.lipstick 0.1 diff --git a/usr/share/lipstick-jolla-home-qt5/backgrounds/DimmedBackgroundLoader.qml b/usr/share/lipstick-jolla-home-qt5/backgrounds/DimmedBackgroundLoader.qml index a6289d6b..cb15707f 100644 --- a/usr/share/lipstick-jolla-home-qt5/backgrounds/DimmedBackgroundLoader.qml +++ b/usr/share/lipstick-jolla-home-qt5/backgrounds/DimmedBackgroundLoader.qml @@ -5,7 +5,7 @@ * License: Proprietary */ -import QtQuick 2.0 +import QtQuick 2.6 import Sailfish.Silica 1.0 import org.nemomobile.lipstick 0.1 diff --git a/usr/share/lipstick-jolla-home-qt5/backgrounds/SynchronizedWallpaperLoader.qml b/usr/share/lipstick-jolla-home-qt5/backgrounds/SynchronizedWallpaperLoader.qml index 51127465..54a8a68d 100644 --- a/usr/share/lipstick-jolla-home-qt5/backgrounds/SynchronizedWallpaperLoader.qml +++ b/usr/share/lipstick-jolla-home-qt5/backgrounds/SynchronizedWallpaperLoader.qml @@ -27,7 +27,8 @@ Private.AnimatedLoader { delayReload = true } else if (Lipstick.compositor) { delayReload = false - load(wallpaper, "", properties(sourceItem, Lipstick.compositor.wallpaper.ambience)) + var ambience = Lipstick.compositor.wallpaper ? Lipstick.compositor.wallpaper.ambience : null + load(wallpaper, "", properties(sourceItem, ambience)) } } diff --git a/usr/share/lipstick-jolla-home-qt5/camera/ButtonAnchor.qml b/usr/share/lipstick-jolla-home-qt5/camera/ButtonAnchor.qml index 7b0cd1fa..55b754f9 100644 --- a/usr/share/lipstick-jolla-home-qt5/camera/ButtonAnchor.qml +++ b/usr/share/lipstick-jolla-home-qt5/camera/ButtonAnchor.qml @@ -1,4 +1,4 @@ -import QtQuick 2.0 +import QtQuick 2.6 import Sailfish.Silica 1.0 OverlayAnchor {} diff --git a/usr/share/lipstick-jolla-home-qt5/camera/CameraSplash.qml b/usr/share/lipstick-jolla-home-qt5/camera/CameraSplash.qml index e6fb5c8b..03e5666f 100644 --- a/usr/share/lipstick-jolla-home-qt5/camera/CameraSplash.qml +++ b/usr/share/lipstick-jolla-home-qt5/camera/CameraSplash.qml @@ -1,6 +1,6 @@ import QtQuick 2.6 import Sailfish.Silica 1.0 -import org.nemomobile.configuration 1.0 +import Nemo.Configuration 1.0 Item { id: splash diff --git a/usr/share/lipstick-jolla-home-qt5/camera/OverlayAnchor.qml b/usr/share/lipstick-jolla-home-qt5/camera/OverlayAnchor.qml index 0dbf715f..a2e22392 100644 --- a/usr/share/lipstick-jolla-home-qt5/camera/OverlayAnchor.qml +++ b/usr/share/lipstick-jolla-home-qt5/camera/OverlayAnchor.qml @@ -1,4 +1,4 @@ -import QtQuick 2.0 +import QtQuick 2.6 import Sailfish.Silica 1.0 Item { diff --git a/usr/share/lipstick-jolla-home-qt5/compositor.qml b/usr/share/lipstick-jolla-home-qt5/compositor.qml index b66c6d4a..3932b1bb 100644 --- a/usr/share/lipstick-jolla-home-qt5/compositor.qml +++ b/usr/share/lipstick-jolla-home-qt5/compositor.qml @@ -1,13 +1,13 @@ /**************************************************************************** ** -** Copyright (c) 2013 - 2020 Jolla Ltd. +** Copyright (c) 2013 - 2023 Jolla Ltd. ** Copyright (c) 2020 - 2021 Open Mobile Platform LLC. ** ** License: Proprietary ** ****************************************************************************/ -import QtQuick 2.2 +import QtQuick 2.6 import QtQuick.Window 2.2 as QtQuick import org.nemomobile.lipstick 0.1 import org.nemomobile.systemsettings 1.0 @@ -17,7 +17,6 @@ import Nemo.FileManager 1.0 import Nemo.Configuration 1.0 import Sailfish.Silica 1.0 import Sailfish.Silica 1.0 as SS -import Sailfish.Ambience 1.0 import Sailfish.Silica.private 1.0 import Sailfish.Lipstick 1.0 import com.jolla.lipstick 0.1 @@ -92,7 +91,10 @@ Compositor { property alias notificationOverviewLayer: notificationOverviewLayerItem property alias shutdownLayer: shutdownLayerItem + property alias floatingScreenshotButtonActive: screenshotButton.active + property alias volumeGestureFilterItem: globalVolumeGestureItem + readonly property alias experimentalFeatures: experimentalFeatures // Needs more prototyping, disable by default. See JB#40618 readonly property bool quickAppToggleGestureExceeded: experimentalFeatures.quickAppToggleGesture && peekLayer.quickAppToggleGestureExceeded @@ -127,7 +129,7 @@ Compositor { // When true the device unlock screen will be shown immediately during displayAboutToBeOn. // Set when display is turned on with double power key press or when plugging in USB cable - property bool showDeviceLock + property bool pendingShowUnlockScreen // True if only the current notification window is allowed to be visible property bool onlyCurrentNotificationAllowed @@ -157,7 +159,7 @@ Compositor { readonly property real topmostWindowHeight: topmostWindowAngle % 180 == 0 ? height : width - property int homeOrientation: Qt.PortraitOrientation + property int homeOrientation: QtQuick.Screen.primaryOrientation screenOrientation: { if (orientationLock == "portrait") return Qt.PortraitOrientation @@ -245,7 +247,10 @@ Compositor { readonly property bool largeScreen: SS.Screen.sizeCategory >= SS.Screen.Large property string _peekDirection - property bool _displayOn + property bool _displayOn // display is powered on, which is not the same as + property bool _displayWokenUp // display is in on/dimmed state + + readonly property bool multitaskingHome: experimentalFeatures.multitasking_home // Emitted before transitioning to a home layer. The lockscreen connects to this and either // clears its locked status allowing home to gain focus, or shows the pin query if the device @@ -322,6 +327,10 @@ Compositor { path: "/desktop/sailfish/experimental" property bool quickAppToggleGesture + property bool multitasking_home: true + property bool topmenu_shutdown_reboot_visible + property int lockscreen_notification_count: 4 + property bool dismiss_lockscreen_on_bootup } MultiPointTouchDrag { @@ -518,10 +527,17 @@ Compositor { if (alarmLayer.window) windows.push(alarmLayer.window.window) else if (dialogLayer.window) windows.push(dialogLayer.window.window) else if (topmostWindow) windows.push(topmostWindow.window) + var notifications = notificationLayer.children + for (var i = 0; i < notifications.length; i++) { + if (notifications[i].window) { + windows.push(notifications[i].window) + } + } var overlays = overlayLayer.contentItem.children - for (var ii = 0; ii < overlays.length; ++ii) - windows.push(overlays[ii].window) + for (i = 0; i < overlays.length; ++i) { + windows.push(overlays[i].window) + } // Lipstick does not use real Qt windows. Instead, all "windows" // within the lipstick home application are just special QtQuick // items added to a single global scene. @@ -559,8 +575,12 @@ Compositor { launchManager.launch(item, arguments || []) } - function invokeRemoteAction(remoteAction) { - launchManager.invokeRemoteAction(remoteAction) + function invokeRemoteAction(remoteAction, trusted) { + launchManager.invokeRemoteAction(remoteAction, trusted) + } + + function invokeRemoteTextAction(remoteAction, text, trusted) { + launchManager.invokeRemoteTextAction(remoteAction, text, trusted) } function invokeDBusMethod(service, path, iface, method, arguments) { @@ -631,9 +651,9 @@ Compositor { visible: root.homeVisible dimmer { - offset: Math.abs(homeLayerItem.events.offset) + offset: root.multitaskingHome ? Math.abs(homeLayerItem.events.offset) : 0 distance: homeLayerItem._transposed ? homeLayerItem.height : homeLayerItem.width - relativeDim: homeLayerItem.events.visible + relativeDim: !root.multitaskingHome || homeLayerItem.events.visible } onTransitionComplete: ambienceChangeTimeout.running = false @@ -747,16 +767,65 @@ Compositor { * (indicatorHomeForeground.transposed ? -launcherLayer.x : launcherLayer.y) opacity: { - if (launcherLayer.peekFilter.bottomActive) { - return launcherLayer.contentOpacity - } else if (launcherLayerItem.closeFromEdge) { - return peekLayer.contentOpacity + if (launcherLayer.peekFilter.bottomActive || launcherLayerItem.closeFromEdge) { + return launcherLayer.contentOpacity * peekLayer.contentOpacity } else { return exposed ? 1.0 : 0.0 } } opacityBehavior.enabled: !launcherLayerItem.closeFromEdge && !launcherLayer.peekFilter.bottomActive } + + Item { + id: closeAreaIndicator + + width: parent.width + height: Theme.paddingMedium + opacity: 0 + + Rectangle { + anchors.left: parent.left + height: parent.height + width: Theme.itemSizeMedium // topMenuLayerItem.edgeFilter.topRejectMargin except always set + gradient: Gradient { + GradientStop { position: 0; color: Theme.highlightColor } + GradientStop { position: 1; color: "transparent" } + } + } + Rectangle { + anchors.right: parent.right + height: parent.height + width: Theme.itemSizeMedium + gradient: Gradient { + GradientStop { position: 0; color: Theme.highlightColor } + GradientStop { position: 1; color: "transparent" } + } + } + + states: [ + State { + name: "shown" + when: appLayerItem.closingWindowId != 0 + PropertyChanges { + target: closeAreaIndicator + // triggered closing reverts briefly to peekfilter progress 0 + opacity: Theme.opacityHigh * + (appLayerItem.peekFilter.progress > 0 ? appLayerItem.peekFilter.progress : 1) + } + } + ] + transitions: [ + Transition { + from: "shown" + to: "" + OpacityAnimator { + target: closeAreaIndicator + to: 0 + duration: 250 + } + } + ] + } } Layer { @@ -865,8 +934,8 @@ Compositor { BlurSource { id: peekBlurSource - anchors.fill: parent + anchors.fill: parent blur: (topMenuLayer.exposed || launcherLayerItem.exposed || unresponsiveApplicationDialog.windowVisible @@ -1281,7 +1350,13 @@ Compositor { displayOffRectangle.suppressDisplayOffBehavior = false } - onExposedChanged: if (exposed) topmenuEdgeHandle.earlyFadeout = false + onExposedChanged: { + if (exposed) { + topmenuEdgeHandle.earlyFadeout = false + launcherLayerItem.resetPinning() + } + } + onClosed: { displayOffRectangle.keepVisible = false topmenuEdgeHandle.earlyFadeout = false @@ -1296,6 +1371,16 @@ Compositor { OverlayLayer { id: overlayLayer + + // There's no notification for item flags, but the only known instances of + // ItemAcceptsInputMethod changing dynamically is the TextInput/Edit read only + // property. By including it in the binding we'll force a re-evaluation if + // the property both exists and changes. + activeFocusItem: root.activeFocusItem + && !root.activeFocusItem.readOnly + && JollaSystemInfo.itemAcceptsInputMethod(root.activeFocusItem) + ? root.activeFocusItem + : null } } @@ -1308,6 +1393,8 @@ Compositor { id: notificationLayer property alias contentItem: notificationLayer + property alias overlayItem: notificationLayer + property int __compositor_is_layer // Identifies this as a layer to OverlayLayer.qml anchors.fill: parent @@ -1328,43 +1415,6 @@ Compositor { } } - MouseTracker { - id: mouseTracker - - anchors.fill: parent - enabled: displayCursor.value && available && !lipstickSettings.lowPowerMode - rotation: QtQuick.Screen.angleBetween(Lipstick.compositor.topmostWindowOrientation, QtQuick.Screen.primaryOrientation) - - onAvailableChanged: if (available) mouseVisibilityTimer.restart() - onMouseXChanged: if (available) mouseVisibilityTimer.restart() - onMouseYChanged: if (available) mouseVisibilityTimer.restart() - - Timer { - id: mouseVisibilityTimer - - // Hide after 10 minutes of idle - interval: 10 * 60 * 1000 - } - - ConfigurationValue { - id: displayCursor - key: "/desktop/sailfish/compositor/display_cursor" - defaultValue: false - } - - Image { - // JB#56057: Support custom pointer graphics with different hotspot co-ordinates - // Now the hotspot co-ordinates below need to be updated if graphic-pointer-default icon is changed - property real hotspotX: 13/48 * width - property real hotspotY: 4/48 * height - x: mouseTracker.mouseX - hotspotX - y: mouseTracker.mouseY - hotspotY - opacity: mouseTracker.enabled && mouseVisibilityTimer.running ? 1.0 : 0.0 - Behavior on opacity { FadeAnimator {}} - source: "image://theme/graphic-pointer-default" - } - } - Component { id: windowWrapper WindowWrapper { } @@ -1429,10 +1479,7 @@ Compositor { var isApplicationWindow = window.category == "" || window.category == "silica" var isWallpaperWindow = window.category === "wallpaper" - var component = null; - if (window.isInProcess) component = inProcWindowWrapper - else component = windowWrapper - + var component = window.isInProcess ? inProcWindowWrapper : windowWrapper var properties = { 'window': window, 'parent': null @@ -1441,8 +1488,13 @@ Compositor { var parent = null if (isHomeWindow) { - parent = homeLayerItem.switcher - properties.parent = homeLayerItem.switcher.contentItem + if (root.multitaskingHome) { + parent = homeLayerItem.switcher + properties.parent = homeLayerItem.switcher.contentItem + } else { + parent = launcherLayer + properties.parent = launcherLayerItem.contentItem + } } else if (isEventsWindow) { parent = homeLayerItem.events properties.parent = homeLayerItem.events.contentItem @@ -1450,8 +1502,13 @@ Compositor { parent = lockScreenLayer properties.parent = lockScreenLayerItem.contentItem } else if (isLauncherWindow) { - parent = launcherLayer - properties.parent = launcherLayerItem.contentItem + if (root.multitaskingHome) { + parent = launcherLayer + properties.parent = launcherLayerItem.contentItem + } else { + parent = homeLayerItem.switcher + properties.parent = homeLayerItem.switcher.contentItem + } } else if (isShutdownWindow) { parent = shutdownLayer properties.parent = shutdownLayer.contentItem @@ -1516,14 +1573,23 @@ Compositor { } if (isHomeWindow) { - homeLayerItem.switcher.window = w + if (root.multitaskingHome) { + homeLayerItem.switcher.window = w + } else { + launcherLayer.window = w + } + launcherLayer.allowed = true desktop = Desktop.instance } else if (isLockScreenWindow) { lockScreenLayer.window = w setCurrentWindow(lockScreenLayer.window) } else if (isLauncherWindow) { - launcherLayer.window = w + if (root.multitaskingHome) { + launcherLayer.window = w + } else { + homeLayerItem.switcher.window = w + } } else if (isTopMenuWindow) { topMenuLayerItem.window = w } else if (isEventsWindow) { @@ -1554,7 +1620,7 @@ Compositor { onWindowRemoved: { if (debug) console.debug("\nCompositor: Window removed \"" + window.title + "\"") - var w = window.userData; + var w = window.userData if (!w) { return @@ -1582,7 +1648,7 @@ Compositor { } if (topmostWindow == w) { - setCurrentWindow(root.obscuredWindow); + setCurrentWindow(root.obscuredWindow) } var closingIndex = windowsBeingClosed.indexOf(window.userData) @@ -1644,12 +1710,26 @@ Compositor { onShowUnlockScreen: { if (!root.visible) { - showDeviceLock = true + pendingShowUnlockScreen = true // -> onDisplayAboutToBeOn in LockScreen.qml + } else if (!_displayWokenUp) { + pendingShowUnlockScreen = true // -> onDisplayOn below } else if (root.deviceIsLocked && !cameraLayerItem.active) { + pendingShowUnlockScreen = false root.unlock() } } - onDisplayOn: showDeviceLock = false + onDisplayOn: { + _displayWokenUp = true + if (pendingShowUnlockScreen) { + pendingShowUnlockScreen = false + pendingShowUnlockScreenTimer.start() + } + } + onPendingShowUnlockScreenChanged: { + if (!pendingShowUnlockScreen) { + pendingShowUnlockScreenTimer.stop() + } + } onDisplayAboutToBeOn: { _displayOn = true @@ -1659,6 +1739,7 @@ Compositor { } onDisplayAboutToBeOff: { + _displayWokenUp = false if (lipstickSettings.blankingPolicy == "call" || lipstickSettings.blankingPolicy == "alarm") { currentAlarm = true } @@ -1668,7 +1749,8 @@ Compositor { _displayOn = false root.PeekFilter.cancelGesture() displayOffAnimation.complete() - showDeviceLock = false + pendingShowUnlockScreen = false + pendingShowUnlockScreenTimer.stop() showApplicationOverLockscreen = Desktop.startupWizardRunning topMenuLayerItem.active = false @@ -1690,6 +1772,12 @@ Compositor { } } + Timer { + id: pendingShowUnlockScreenTimer + interval: 250 // As short as possible without the end result looking unintentional + onTriggered: root.showUnlockScreen() + } + Timer { id: incomingAlarmTimer interval: 2000 @@ -1707,6 +1795,43 @@ Compositor { : (appLayer.window ? appLayer.window.window : null) } + MouseTracker { + id: mouseTracker + + anchors.fill: parent + enabled: displayCursor.value && available && !lipstickSettings.lowPowerMode + rotation: QtQuick.Screen.angleBetween(Lipstick.compositor.topmostWindowOrientation, QtQuick.Screen.primaryOrientation) + + onAvailableChanged: if (available) mouseVisibilityTimer.restart() + onMouseXChanged: if (available) mouseVisibilityTimer.restart() + onMouseYChanged: if (available) mouseVisibilityTimer.restart() + + Timer { + id: mouseVisibilityTimer + + // Hide after 10 minutes of idle + interval: 10 * 60 * 1000 + } + + ConfigurationValue { + id: displayCursor + key: "/desktop/sailfish/compositor/display_cursor" + defaultValue: true + } + + Image { + // JB#56057: Support custom pointer graphics with different hotspot co-ordinates + // Now the hotspot co-ordinates below need to be updated if graphic-pointer-default icon is changed + property real hotspotX: 13/48 * width + property real hotspotY: 4/48 * height + x: mouseTracker.mouseX - hotspotX + y: mouseTracker.mouseY - hotspotY + opacity: mouseTracker.enabled && mouseVisibilityTimer.running ? 1.0 : 0.0 + Behavior on opacity { FadeAnimator {}} + source: "image://theme/graphic-pointer-default" + } + } + TouchBlocker { id: shutdownLayerItem @@ -1778,6 +1903,92 @@ Compositor { } } + MouseArea { + id: screenshotButton + x: defaultX + y: defaultY + visible: active && !snapping + enabled: visible + width: Theme.itemSizeExtraLarge + height: Theme.itemSizeExtraLarge + + drag.target: screenshotButton + drag.axis: Drag.XAndYAxis + drag.minimumX: 0 - width * dragOverscan + drag.maximumX: root.width - width * (1 - dragOverscan) + drag.minimumY: 0 - height * dragOverscan + drag.maximumY: root.height - height * (1 - dragOverscan) + + property bool active + property bool snapping + readonly property real defaultX: (root.width - width) * 0.50 + readonly property real defaultY: (root.height - height) * 0.75 + readonly property real dragOverscan: 0.4 + readonly property real hideOverscan: 0.3 + property var screenshotObject + + onActiveChanged: resetPosition() + onSnappingChanged: screenshotExposureTimer.running = snapping + onClicked: beginExposure() + onPressedChanged: { if (!pressed) spotHidden() } + + function resetPosition() { + x = defaultX + y = defaultY + } + function spotHidden() { + if ((x + width * hideOverscan < 0) + || (x + width * (1 - hideOverscan) > root.width) + || (y + height * hideOverscan < 0) + || (y + height * (1 - hideOverscan) > root.height)) { + active = false + } + } + function beginExposure() { + snapping = true + if (!screenshotObject) { + var component = Qt.createComponent(Qt.resolvedUrl("volumecontrol/Screenshot.qml")) + if (component.status == Component.Ready) { + screenshotObject = component.createObject(screenshotButton) + } else { + console.warn("Screenshot object instantiation failed:", component.errorString()) + } + } + if (screenshotObject) { + screenshotObject.capture() + } + } + function endExposure() { + snapping = false + } + + Rectangle { + radius: width / 2 + width: shutterIcon.width + height: width + anchors.centerIn: parent + color: Theme.secondaryHighlightColor + visible: shutterIcon.opacity < 1.0 + } + Image { + id: shutterIcon + anchors.centerIn: parent + source: "image://theme/icon-camera-shutter" + + opacity: { + if (screenshotButton.pressed) { + return Theme.opacityHigh + } + return 1.0 + } + } + Timer { + id: screenshotExposureTimer + interval: 1000 + onTriggered: parent.endExposure() + } + } + Loader { id: debugWindow active: root.debug diff --git a/usr/share/lipstick-jolla-home-qt5/compositor/ApplicationCloseGestureHint.qml b/usr/share/lipstick-jolla-home-qt5/compositor/ApplicationCloseGestureHint.qml index 7778b3bb..453cec7c 100644 --- a/usr/share/lipstick-jolla-home-qt5/compositor/ApplicationCloseGestureHint.qml +++ b/usr/share/lipstick-jolla-home-qt5/compositor/ApplicationCloseGestureHint.qml @@ -1,6 +1,6 @@ -import QtQuick 2.2 +import QtQuick 2.6 import Sailfish.Silica 1.0 -import org.nemomobile.configuration 1.0 +import Nemo.Configuration 1.0 Loader { readonly property bool hinting: item && item.hinting diff --git a/usr/share/lipstick-jolla-home-qt5/compositor/BlurSource.qml b/usr/share/lipstick-jolla-home-qt5/compositor/BlurSource.qml index 614b4922..8b432278 100644 --- a/usr/share/lipstick-jolla-home-qt5/compositor/BlurSource.qml +++ b/usr/share/lipstick-jolla-home-qt5/compositor/BlurSource.qml @@ -1,4 +1,4 @@ -import QtQuick 2.1 +import QtQuick 2.6 import Sailfish.Silica 1.0 import Sailfish.Silica.Background 1.0 import org.nemomobile.lipstick 0.1 diff --git a/usr/share/lipstick-jolla-home-qt5/compositor/DebugButton.qml b/usr/share/lipstick-jolla-home-qt5/compositor/DebugButton.qml index 48cef12b..71ff90fe 100644 --- a/usr/share/lipstick-jolla-home-qt5/compositor/DebugButton.qml +++ b/usr/share/lipstick-jolla-home-qt5/compositor/DebugButton.qml @@ -5,7 +5,7 @@ ** ****************************************************************************/ -import QtQuick 2.2 +import QtQuick 2.6 Rectangle { id: root diff --git a/usr/share/lipstick-jolla-home-qt5/compositor/DebugWindow.qml b/usr/share/lipstick-jolla-home-qt5/compositor/DebugWindow.qml index 7ab8b0e1..9ef9d216 100644 --- a/usr/share/lipstick-jolla-home-qt5/compositor/DebugWindow.qml +++ b/usr/share/lipstick-jolla-home-qt5/compositor/DebugWindow.qml @@ -5,7 +5,7 @@ ** ****************************************************************************/ -import QtQuick 2.2 +import QtQuick 2.6 import Sailfish.Silica 1.0 import org.nemomobile.lipstick 0.1 import Nemo.DBus 2.0 diff --git a/usr/share/lipstick-jolla-home-qt5/compositor/HintCoordinator.qml b/usr/share/lipstick-jolla-home-qt5/compositor/HintCoordinator.qml index 0568c629..52fc9689 100644 --- a/usr/share/lipstick-jolla-home-qt5/compositor/HintCoordinator.qml +++ b/usr/share/lipstick-jolla-home-qt5/compositor/HintCoordinator.qml @@ -1,6 +1,6 @@ -import QtQuick 2.0 +import QtQuick 2.6 import Sailfish.Silica.private 1.0 -import org.nemomobile.configuration 1.0 +import Nemo.Configuration 1.0 ConfigurationValue { // 0 - No hint showing @@ -49,7 +49,7 @@ ConfigurationValue { } function getEpoch() { - var date = new Date(); + var date = new Date() return Math.floor(date.getTime() / 1000) } diff --git a/usr/share/lipstick-jolla-home-qt5/compositor/RotatingItem.qml b/usr/share/lipstick-jolla-home-qt5/compositor/RotatingItem.qml index cebcbde1..c1c3cb3d 100644 --- a/usr/share/lipstick-jolla-home-qt5/compositor/RotatingItem.qml +++ b/usr/share/lipstick-jolla-home-qt5/compositor/RotatingItem.qml @@ -1,4 +1,4 @@ -import QtQuick 2.2 +import QtQuick 2.6 import Sailfish.Silica 1.0 import org.nemomobile.lipstick 0.1 diff --git a/usr/share/lipstick-jolla-home-qt5/compositor/UnresponsiveApplicationDialog.qml b/usr/share/lipstick-jolla-home-qt5/compositor/UnresponsiveApplicationDialog.qml index 0e80cf00..913494be 100644 --- a/usr/share/lipstick-jolla-home-qt5/compositor/UnresponsiveApplicationDialog.qml +++ b/usr/share/lipstick-jolla-home-qt5/compositor/UnresponsiveApplicationDialog.qml @@ -5,7 +5,7 @@ ** ****************************************************************************/ -import QtQuick 2.0 +import QtQuick 2.6 import org.nemomobile.lipstick 0.1 import com.jolla.lipstick 0.1 import Sailfish.Silica 1.0 @@ -50,7 +50,7 @@ SystemWindow { //% "You can either wait or close the application." description: qsTrId("lipstick-jolla-home-la-application_hanged_description") - topPadding: transpose ? Theme.paddingLarge : 2*Theme.paddingLarge + semiTight: true } Row { id: buttonRow diff --git a/usr/share/lipstick-jolla-home-qt5/connectivity/USBModeSelector.qml b/usr/share/lipstick-jolla-home-qt5/connectivity/USBModeSelector.qml index 916966de..8d3ca977 100644 --- a/usr/share/lipstick-jolla-home-qt5/connectivity/USBModeSelector.qml +++ b/usr/share/lipstick-jolla-home-qt5/connectivity/USBModeSelector.qml @@ -5,7 +5,7 @@ ** ****************************************************************************/ -import QtQuick 2.0 +import QtQuick 2.6 import org.nemomobile.lipstick 0.1 import Sailfish.Silica 1.0 import Sailfish.Lipstick 1.0 @@ -13,6 +13,7 @@ import "../systemwindow" SystemWindow { id: usbDialog + objectName: "usbDialog" property bool largeIcons: Screen.sizeCategory >= Screen.Large @@ -106,6 +107,7 @@ SystemWindow { Column { id: content + width: parent.width SystemDialogHeader { @@ -117,7 +119,7 @@ SystemWindow { //: Displayed above the available USB connection modes //% "Switch to one of the following modes(s)" description: qsTrId("lipstick-jolla-home-la-usb_connected_description", buttonModel.count) - topPadding: transpose ? Theme.paddingLarge : 2*Theme.paddingLarge + semiTight: true } Row { id: buttonRow diff --git a/usr/share/lipstick-jolla-home-qt5/connectivity/VpnAgent.qml b/usr/share/lipstick-jolla-home-qt5/connectivity/VpnAgent.qml index 765cc235..b82f5296 100644 --- a/usr/share/lipstick-jolla-home-qt5/connectivity/VpnAgent.qml +++ b/usr/share/lipstick-jolla-home-qt5/connectivity/VpnAgent.qml @@ -314,7 +314,7 @@ SystemWindow { /// VPN connect prompt; %1 is the VPN name //% "Connect to %1" title: qsTrId("lipstick-jolla-home-he-enter_vpn_credentials").arg(vpnName) - topPadding: Screen.sizeCategory >= Screen.Large ? 2*Theme.paddingLarge : Theme.paddingLarge + tight: true } Column { diff --git a/usr/share/lipstick-jolla-home-qt5/eventsview/EventFeedAccountManager.qml b/usr/share/lipstick-jolla-home-qt5/eventsview/EventFeedAccountManager.qml index 70161521..04096bfb 100644 --- a/usr/share/lipstick-jolla-home-qt5/eventsview/EventFeedAccountManager.qml +++ b/usr/share/lipstick-jolla-home-qt5/eventsview/EventFeedAccountManager.qml @@ -6,21 +6,27 @@ ** ****************************************************************************/ -import QtQuick 2.0 +import QtQuick 2.6 import Sailfish.Silica 1.0 import Sailfish.Accounts 1.0 -import org.nemomobile.configuration 1.0 +import Nemo.Configuration 1.0 +import org.nemomobile.socialcache 1.0 Item { id: container property var eventFeedAccounts + property alias downloader: downloader property var _autoSyncConfs: ({}) signal refreshed signal accountEnabledChanged + SocialImageCache { + id: downloader + } + Timer { id: accountRefreshTimer interval: 10 diff --git a/usr/share/lipstick-jolla-home-qt5/eventsview/EventFeedList.qml b/usr/share/lipstick-jolla-home-qt5/eventsview/EventFeedList.qml index f8d0c3a0..a2278672 100644 --- a/usr/share/lipstick-jolla-home-qt5/eventsview/EventFeedList.qml +++ b/usr/share/lipstick-jolla-home-qt5/eventsview/EventFeedList.qml @@ -5,13 +5,10 @@ ** ****************************************************************************/ -import QtQuick 2.0 +import QtQuick 2.6 import Sailfish.Silica 1.0 -import Sailfish.Ambience 1.0 -import Sailfish.Accounts 1.0 import com.jolla.lipstick 0.1 import org.nemomobile.lipstick 0.1 -import org.nemomobile.socialcache 1.0 Column { id: column @@ -68,22 +65,28 @@ Column { showingRemovableContent = false } - EventFeedSocialSubviewModel { + Loader { id: eventFeedListModel - manager: accountManager + + // EventFeedSocialSubviewModel requires EventFeedAccountManager + // which requires Sailfish.Accounts. + Component.onCompleted: { + setSource(Qt.resolvedUrl("EventFeedSocialSubviewModel.qml"), { + "manager": Qt.binding(function() { return accountManager.item }) + }) + } } - EventFeedAccountManager { + Loader { id: accountManager - } - SocialImageCache { - id: downloader + // Handle Sailfish.Accounts dependency during runtime. + Component.onCompleted: setSource(Qt.resolvedUrl("EventFeedAccountManager.qml")) } Repeater { id: eventFeedList - model: eventFeedListModel.model + model: eventFeedListModel.item ? eventFeedListModel.item.model : null Loader { id: loader @@ -92,9 +95,9 @@ Column { Component.onCompleted: { var props = { - "downloader": downloader, + "downloader": accountManager.item.downloader, "providerName": providerName, - "subviewModel": eventFeedListModel, + "subviewModel": eventFeedListModel.item, "viewVisible": Qt.binding(function() { return Desktop.eventsViewVisible }), "eventsColumnMaxWidth": Math.min(Screen.width, Screen.height) } diff --git a/usr/share/lipstick-jolla-home-qt5/eventsview/EventFeedSocialSubviewModel.qml b/usr/share/lipstick-jolla-home-qt5/eventsview/EventFeedSocialSubviewModel.qml index dc8fab4c..4c771ddf 100644 --- a/usr/share/lipstick-jolla-home-qt5/eventsview/EventFeedSocialSubviewModel.qml +++ b/usr/share/lipstick-jolla-home-qt5/eventsview/EventFeedSocialSubviewModel.qml @@ -6,9 +6,8 @@ ** ****************************************************************************/ -import QtQuick 2.0 +import QtQuick 2.6 import Sailfish.Silica 1.0 -import Sailfish.Accounts 1.0 Item { id: subviewModel diff --git a/usr/share/lipstick-jolla-home-qt5/eventsview/EventsView.qml b/usr/share/lipstick-jolla-home-qt5/eventsview/EventsView.qml index 0c2d7ac7..31b0f838 100644 --- a/usr/share/lipstick-jolla-home-qt5/eventsview/EventsView.qml +++ b/usr/share/lipstick-jolla-home-qt5/eventsview/EventsView.qml @@ -5,7 +5,7 @@ ** ****************************************************************************/ -import QtQuick 2.0 +import QtQuick 2.6 import org.nemomobile.lipstick 0.1 import Sailfish.Silica 1.0 import com.jolla.lipstick 0.1 diff --git a/usr/share/lipstick-jolla-home-qt5/eventsview/EventsViewList.qml b/usr/share/lipstick-jolla-home-qt5/eventsview/EventsViewList.qml index 624ced4c..8752a999 100644 --- a/usr/share/lipstick-jolla-home-qt5/eventsview/EventsViewList.qml +++ b/usr/share/lipstick-jolla-home-qt5/eventsview/EventsViewList.qml @@ -5,12 +5,12 @@ ** ****************************************************************************/ -import QtQuick 2.0 +import QtQuick 2.6 import Sailfish.Silica 1.0 import com.jolla.lipstick 0.1 import org.nemomobile.lipstick 0.1 -import org.nemomobile.time 1.0 -import org.nemomobile.configuration 1.0 +import Nemo.Time 1.0 +import Nemo.Configuration 1.0 import "../lockscreen" import "../notifications" as Notifications import "weather" @@ -45,6 +45,7 @@ SilicaFlickable { Connections { id: expandingItemConn + property real targetYOffset onHeightChanged: { @@ -60,6 +61,7 @@ SilicaFlickable { Behavior on contentY { id: scrollBehavior + enabled: false SequentialAnimation { diff --git a/usr/share/lipstick-jolla-home-qt5/eventsview/EventsWindow.qml b/usr/share/lipstick-jolla-home-qt5/eventsview/EventsWindow.qml index e7b5b742..8baa8b14 100644 --- a/usr/share/lipstick-jolla-home-qt5/eventsview/EventsWindow.qml +++ b/usr/share/lipstick-jolla-home-qt5/eventsview/EventsWindow.qml @@ -5,7 +5,7 @@ * License: Proprietary */ -import QtQuick 2.1 +import QtQuick 2.6 import Sailfish.Silica 1.0 import org.nemomobile.lipstick 0.1 import com.jolla.lipstick 0.1 diff --git a/usr/share/lipstick-jolla-home-qt5/eventsview/calendar/CalendarWidgetLoader.qml b/usr/share/lipstick-jolla-home-qt5/eventsview/calendar/CalendarWidgetLoader.qml index 34f6c700..76290e57 100644 --- a/usr/share/lipstick-jolla-home-qt5/eventsview/calendar/CalendarWidgetLoader.qml +++ b/usr/share/lipstick-jolla-home-qt5/eventsview/calendar/CalendarWidgetLoader.qml @@ -5,18 +5,17 @@ ** ****************************************************************************/ -import QtQuick 2.0 +import QtQuick 2.6 import Sailfish.Silica 1.0 import Sailfish.Lipstick 1.0 import org.nemomobile.lipstick 0.1 import com.jolla.lipstick 0.1 -import org.nemomobile.lipstick 0.1 Loader { id: loader property Item eventsView - property string widgetFilePath: StandardPaths.resolveImport("Sailfish.Calendar.CalendarWidget") + readonly property string widgetFilePath: StandardPaths.qmlImportPath + "Sailfish/Calendar/CalendarWidget.qml" property bool widgetExists: fileUtils.exists(widgetFilePath) property bool eventsVisible: eventsViewVisible diff --git a/usr/share/lipstick-jolla-home-qt5/eventsview/weather/WeatherAdvertisement.qml b/usr/share/lipstick-jolla-home-qt5/eventsview/weather/WeatherAdvertisement.qml index 7959f169..5fb4a8c4 100644 --- a/usr/share/lipstick-jolla-home-qt5/eventsview/weather/WeatherAdvertisement.qml +++ b/usr/share/lipstick-jolla-home-qt5/eventsview/weather/WeatherAdvertisement.qml @@ -5,7 +5,7 @@ ** ****************************************************************************/ -import QtQuick 2.0 +import QtQuick 2.6 import Sailfish.Silica 1.0 import Nemo.DBus 2.0 diff --git a/usr/share/lipstick-jolla-home-qt5/eventsview/weather/WeatherBanner.qml b/usr/share/lipstick-jolla-home-qt5/eventsview/weather/WeatherBanner.qml index 62bdd5b3..2094cde6 100644 --- a/usr/share/lipstick-jolla-home-qt5/eventsview/weather/WeatherBanner.qml +++ b/usr/share/lipstick-jolla-home-qt5/eventsview/weather/WeatherBanner.qml @@ -5,7 +5,7 @@ ** ****************************************************************************/ -import QtQuick 2.0 +import QtQuick 2.6 import Sailfish.Weather 1.0 as Weather import Nemo.Configuration 1.0 diff --git a/usr/share/lipstick-jolla-home-qt5/eventsview/weather/WeatherIcon.qml b/usr/share/lipstick-jolla-home-qt5/eventsview/weather/WeatherIcon.qml index 4243f8d1..f254ccd0 100644 --- a/usr/share/lipstick-jolla-home-qt5/eventsview/weather/WeatherIcon.qml +++ b/usr/share/lipstick-jolla-home-qt5/eventsview/weather/WeatherIcon.qml @@ -5,7 +5,7 @@ ** ****************************************************************************/ -import QtQuick 2.0 +import QtQuick 2.6 import Sailfish.Silica 1.0 Item { diff --git a/usr/share/lipstick-jolla-home-qt5/eventsview/weather/WeatherLoader.qml b/usr/share/lipstick-jolla-home-qt5/eventsview/weather/WeatherLoader.qml index a9769346..030a3268 100644 --- a/usr/share/lipstick-jolla-home-qt5/eventsview/weather/WeatherLoader.qml +++ b/usr/share/lipstick-jolla-home-qt5/eventsview/weather/WeatherLoader.qml @@ -5,9 +5,9 @@ ** ****************************************************************************/ -import QtQuick 2.0 +import QtQuick 2.6 import Sailfish.Silica 1.0 -import org.nemomobile.configuration 1.0 +import Nemo.Configuration 1.0 import Nemo.DBus 2.0 import org.nemomobile.lipstick 0.1 import com.jolla.lipstick 0.1 @@ -15,7 +15,8 @@ import com.jolla.lipstick 0.1 Loader { id: weatherLoader - property alias advertiseWeather: weatherAdvertisementConfiguration.value + // disable advertising as foreca isn't working now + property bool advertiseWeather: false && weatherAdvertisementConfiguration.value property bool eventsVisible: eventsViewVisible onItemChanged: { diff --git a/usr/share/lipstick-jolla-home-qt5/eventsview/weather/WeatherNotice.qml b/usr/share/lipstick-jolla-home-qt5/eventsview/weather/WeatherNotice.qml index 87ae40da..acf3b305 100644 --- a/usr/share/lipstick-jolla-home-qt5/eventsview/weather/WeatherNotice.qml +++ b/usr/share/lipstick-jolla-home-qt5/eventsview/weather/WeatherNotice.qml @@ -5,7 +5,7 @@ ** ****************************************************************************/ -import QtQuick 2.0 +import QtQuick 2.6 import Sailfish.Silica 1.0 import Nemo.DBus 2.0 diff --git a/usr/share/lipstick-jolla-home-qt5/launcher/Launcher.qml b/usr/share/lipstick-jolla-home-qt5/launcher/Launcher.qml index ac439a26..f4dc61db 100644 --- a/usr/share/lipstick-jolla-home-qt5/launcher/Launcher.qml +++ b/usr/share/lipstick-jolla-home-qt5/launcher/Launcher.qml @@ -5,13 +5,13 @@ * License: Proprietary */ -import QtQuick 2.1 +import QtQuick 2.6 import org.nemomobile.lipstick 0.1 import Sailfish.Silica 1.0 import Sailfish.Silica.private 1.0 as SilicaPrivate import Sailfish.Policy 1.0 import Sailfish.AccessControl 1.0 -import org.nemomobile.configuration 1.0 +import Nemo.Configuration 1.0 import Sailfish.Lipstick 1.0 import Nemo.DBus 2.0 import com.jolla.lipstick 0.1 @@ -19,10 +19,11 @@ import com.jolla.lipstick 0.1 SilicaListView { id: launcherPager - property bool launcherActive: Lipstick.compositor.launcherLayer.active - onLauncherActiveChanged: if (!launcherActive) { resetPosition(400) } + onVisibleChanged: if (!visible) { resetPosition(400) } property bool editMode: launcher.launcherEditMode + property alias openedChildFolder: launcher.openedChildFolder + onEditModeChanged: { if (editMode) { snapMode = ListView.NoSnap @@ -47,7 +48,14 @@ SilicaListView { highlightMoveDuration: 300 pressDelay: 0 quickScroll: false - interactive: !launcher.openedChildFolder && launcherActive && !Lipstick.compositor.launcherLayer.pinned + interactive: { + var interactive = !launcher.openedChildFolder && !Lipstick.compositor.launcherLayer.hinting + if (Lipstick.compositor.multitaskingHome) { + return interactive && !Lipstick.compositor.launcherLayer.pinned + } else { + return interactive + } + } function resetPosition(delay) { resetPositionTimer.interval = delay === undefined ? 1 : delay @@ -56,7 +64,7 @@ SilicaListView { Timer { id: resetPositionTimer - onTriggered: if (!launcherActive) { launcherPager.positionViewAtBeginning() } + onTriggered: if (!launcherPager.visible) { launcherPager.positionViewAtBeginning() } } function scroll(up) { @@ -123,11 +131,12 @@ SilicaListView { anchors.horizontalCenter: parent.horizontalCenter source: "image://theme/graphic-edge-swipe-handle-bottom" highlighted: Lipstick.compositor.launcherLayer.pinned + visible: Lipstick.compositor.multitaskingHome } MouseArea { objectName: "Launcher" - y: launcherPager.originY + y: launcherPager.originY + launcher.statusBarHeight parent: launcherPager.contentItem width: launcherPager.width height: launcherPager.height * launcherPager.model.count @@ -237,7 +246,7 @@ SilicaListView { rootFolder: true interactive: false - height: cellHeight * Math.ceil(count / columns) + height: cellHeight * Math.ceil(count / columns) + (headerItem ? headerItem.height : 0) Component.onCompleted: manageDummyPages() onContentHeightChanged: manageDummyPages() diff --git a/usr/share/lipstick-jolla-home-qt5/launcher/LauncherFolder.qml b/usr/share/lipstick-jolla-home-qt5/launcher/LauncherFolder.qml index 009e4d67..c18404fe 100644 --- a/usr/share/lipstick-jolla-home-qt5/launcher/LauncherFolder.qml +++ b/usr/share/lipstick-jolla-home-qt5/launcher/LauncherFolder.qml @@ -5,7 +5,7 @@ ** ****************************************************************************/ -import QtQuick 2.5 +import QtQuick 2.6 import org.nemomobile.lipstick 0.1 import Sailfish.Silica 1.0 import Sailfish.Silica.private 1.0 @@ -28,9 +28,12 @@ Rectangle { launcherGrid.setEditMode(false) opacity = 0.0 enabled = false + Lipstick.compositor.launcherLayer.pinned = false destroy(450) } + onVisibleChanged: if (!visible) launcherFolder.close() + z: 10 opacity: 0.0 Behavior on opacity { SmoothedAnimation { duration: 400; velocity: 1000 / duration } } @@ -49,14 +52,12 @@ Rectangle { target: Lipstick.compositor onDisplayOff: launcherFolder.close() } - Connections { - target: Lipstick.compositor.launcherLayer - onActiveChanged: if (!Lipstick.compositor.launcherLayer.active) launcherFolder.close() - } + Connections { target: model onItemRemoved: if (model.itemCount === 0) launcherFolder.close() } + Connections { target: launcherGrid.reorderItem onYChanged: { @@ -99,8 +100,12 @@ Rectangle { Rectangle { id: header + + property int topPadding: launcherGrid.orientation == Orientation.Portrait + ? Screen.topCutout.height : 0 + width: parent.width - height: launcherGrid.cellHeight - Theme.fontSizeExtraSmall/2 + height: topPadding + launcherGrid.cellHeight - Theme.fontSizeExtraSmall/2 gradient: Gradient { GradientStop { position: 0.0; color: Theme.rgba(Theme.primaryColor, 0.0) } GradientStop { position: 1.0; color: Theme.rgba(Theme.primaryColor, 0.15) } @@ -118,7 +123,7 @@ Rectangle { height: parent.height x: launcherGrid.x // launcherGrid is centered in it's parent LauncherIcon { - y: (launcherGrid.cellHeight - height - Theme.fontSizeExtraSmall)/2 + y: header.topPadding + (launcherGrid.cellHeight - height - Theme.fontSizeExtraSmall) / 2 icon: model.iconId anchors.horizontalCenter: parent.horizontalCenter pressed: icon.pressed && icon.containsMouse @@ -144,13 +149,10 @@ Rectangle { TextField { id: titleEditor - anchors { - left: icon.right - leftMargin: -Theme.paddingLarge - right: parent.right - verticalCenter: parent.verticalCenter - } - autoScrollEnabled: false + + x: icon.x + icon.width - Theme.paddingLarge + width: parent.width - x - icon.x + y: header.topPadding + (launcherGrid.cellHeight - height - Theme.fontSizeExtraSmall) / 2 font.pixelSize: Theme.fontSizeExtraLarge font.family: Theme.fontFamilyHeading text: model.title @@ -245,8 +247,12 @@ Rectangle { VerticalScrollDecorator {} + header: null y: Theme.fontSizeExtraSmall/2 - height: cellHeight * visibleRowCount + topMargin: 0 + bottomMargin: 0 + height: visibleRowCount >= 2 ? cellHeight * visibleRowCount + : Math.round(cellHeight * 1.5) cacheBuffer: height displayMarginBeginning: Theme.fontSizeExtraSmall/2 displayMarginEnd: Theme.fontSizeExtraSmall/2 @@ -269,9 +275,12 @@ Rectangle { } return false } - property bool shown: (launcherGrid.launcherEditMode && launcherGrid.reorderItem || - model.itemCount > launcherGrid.columns * visibleRowCount) && !selectIcon - height: launcherGrid.cellHeight - Theme.fontSizeExtraSmall/2 + property bool shown: (launcherGrid.launcherEditMode && launcherGrid.reorderItem + || model.itemCount > launcherGrid.columns * visibleRowCount) + && !selectIcon + + height: visibleRowCount >= 2 ? launcherGrid.cellHeight - Theme.fontSizeExtraSmall/2 + : Math.round(launcherGrid.cellHeight / 2) width: parent.width y: parent.height - (shown ? height : 0) Behavior on y { NumberAnimation { duration: 300; easing.type: Easing.InOutQuad } } diff --git a/usr/share/lipstick-jolla-home-qt5/launcher/LauncherGrid.qml b/usr/share/lipstick-jolla-home-qt5/launcher/LauncherGrid.qml index 105026d4..bfbc35dd 100644 --- a/usr/share/lipstick-jolla-home-qt5/launcher/LauncherGrid.qml +++ b/usr/share/lipstick-jolla-home-qt5/launcher/LauncherGrid.qml @@ -5,7 +5,7 @@ * License: Proprietary */ -import QtQuick 2.4 +import QtQuick 2.6 import org.nemomobile.lipstick 0.1 import com.jolla.lipstick 0.1 import Sailfish.Silica 1.0 @@ -13,6 +13,7 @@ import Sailfish.Silica.private 1.0 import Sailfish.Policy 1.0 import Sailfish.AccessControl 1.0 import Sailfish.Lipstick 1.0 +import Nemo.Configuration 1.0 import "../main" IconGridViewBase { @@ -26,6 +27,22 @@ IconGridViewBase { property alias reorderItem: gridManager.reorderItem property alias gridManager: gridManager property bool canUninstall: AccessControl.hasGroup(AccessControl.RealUid, "sailfish-system") + property int topMargin: Math.max(_page.orientation === Orientation.Portrait + ? Screen.topCutout.height : 0, + largeScreen ? Theme.paddingLarge : Theme.paddingSmall) + property int bottomMargin: Math.max(_page.orientation === Orientation.InvertedPortrait + ? Screen.topCutout.height : 0, + largeScreen ? Theme.paddingLarge : Theme.paddingSmall) + property int realCellHeight: Math.floor(pageHeight / rows) // cell height after page's vertical paddings + property int statusBarHeight: { + if (Lipstick.compositor.multitaskingHome) { + return 0 + } else { + var statusBar = Lipstick.compositor.homeLayer.statusBar + return statusBar.baseY + statusBar.height + } + } + signal itemLaunched function categoryQsTrIds() { @@ -78,8 +95,95 @@ IconGridViewBase { removeApplicationEnabled = enabled } - pageHeight: launcherPager.height + onVisibleChanged: if (!visible) setEditMode(false) + + // base class uses pageHeight to calculate how man rows fit with minimum size, + // we then make the delegates fit full height of the page and finally in delegate + // itself position them according to margins + pageHeight: launcherPager.height - topMargin - bottomMargin + cellHeight: Math.floor(launcherPager.height / rows) + header: launcherClockConfig.launcher_clock_visible ? clockHeader : null + + ConfigurationGroup { + id: launcherClockConfig + + path: "/desktop/sailfish/experimental" + + property bool launcher_clock_visible: false + property int launcher_clock_size: 1.5*Theme.fontSizeHuge + } + + Component { + id: clockHeader + + Item { + width: gridview.width + height: Math.ceil((clockColumn.height + gridview.statusBarHeight) / gridview.cellHeight) * gridview.cellHeight + - gridview.statusBarHeight + + Column { + id: clockColumn + + // left-align with the launcher icons on the first column + x: (gridview.cellWidth - Theme.iconSizeLauncher)/2 + y: parent.height - height + width: parent.width - 2*x + height: implicitHeight + bottomPadding: Theme.paddingMedium + + Row { + id: clockRow + + width: parent.width + spacing: Theme.paddingMedium + + ClockItem { + id: clockItem + + color: Theme.highlightColor + font.pixelSize: launcherClockConfig.launcher_clock_size + updatesEnabled: visible + } + + Label { + id: dateLabel + + property string dateText: Format.formatDate(clockItem.time, Formatter.DateLong) + property string weekdayText: Format.formatDate(clockItem.time, Format.WeekdayNameStandalone) + + text: { + //: Date and weekday shown together, e.g. "20 December 2021, Monday" + //% "%1, %2" + var text = qsTrId("lipstick-jolla-home-date_and_weekday").arg(dateText).arg(weekdayText) + if (fontMetrics.advanceWidth(text) > width) { + return dateText + "
" + weekdayText[0].toUpperCase() + weekdayText.substring(1) + } else { + return text + } + } + + anchors.baseline: clockItem.baseline + truncationMode: TruncationMode.Fade + width: parent.width - parent.spacing - clockItem.width + anchors.baselineOffset: lineCount > 1 ? -height/2 : 0 + color: Theme.highlightColor + FontMetrics { + id: fontMetrics + font: dateLabel.font + } + } + } + + Separator { + id: dateTimeSeparator + + anchors { left: parent.left; right: parent.right } + color: Theme.highlightColor + } + } + } + } EditableGridManager { id: gridManager supportsFolders: rootFolder @@ -99,7 +203,7 @@ IconGridViewBase { source: "image://theme/" + iconId opacity: show ? 1.0 : 0.0 Behavior on opacity { FadeAnimation {} } - y: target ? target.offsetY + target.iconOffset : -20000 + y: target ? target.targetY + target.iconOffset : -20000 anchors.horizontalCenter: target ? target.horizontalCenter : gridview.contentItem.horizontalCenter scale: 1.3 parent: gridview.contentItem @@ -115,10 +219,6 @@ IconGridViewBase { target: Lipstick.compositor onDisplayOff: setEditMode(false) } - Connections { - target: Lipstick.compositor.launcherLayer - onActiveChanged: if (!Lipstick.compositor.launcherLayer.active) setEditMode(false) - } PolicyValue { id: policy @@ -137,15 +237,20 @@ IconGridViewBase { } width: cellWidth - height: cellHeight + height: gridview.realCellHeight manager: gridManager isFolder: model.object.type == LauncherModel.Folder folderItemCount: isFolder && model.object ? model.object.itemCount : 0 editMode: gridview.launcherEditMode - // This compresses the icons toward the center of the screen, leaving extra margin at the top and bottom - offsetY: y - (((y-gridview.originY+height/2)%launcherPager.height)/launcherPager.height - 0.5) * - (largeScreen && rootFolder ? Theme.paddingLarge*4 : Theme._homePageMargin - Theme.paddingLarge) + // the grid view lays out item for full screen, this overrides the layouting so that + // the page has top and bottom margins + targetY: { + var rowInPage = Math.floor(index % (gridview.columns * gridview.rows) / gridview.columns) + var pageNumber = Math.floor(model.index / (gridview.columns * gridview.rows)) + var pageStart = pageNumber * launcherPager.height + return gridview.originY + pageStart + gridview.topMargin + rowInPage * gridview.realCellHeight + } onEditModeChanged: { if (editMode && !uninstallButton && canUninstall && policy.value) { @@ -153,12 +258,6 @@ IconGridViewBase { } } - onIsUpdatingChanged: { - if (isUpdating && !updatingItem) { - updatingItem = updatingComponent.createObject(contentItem) - } - } - Timer { id: scaleUpTimer interval: 200 @@ -200,8 +299,10 @@ IconGridViewBase { } else if (isFolder) { setEditMode(false) showFolder(model.object) - // Ensure the launcher is visible - could be peeking from bottom due to hint. - Lipstick.compositor.setCurrentWindow(Lipstick.compositor.launcherLayer.window) + if (Lipstick.compositor.multitaskingHome) { + // Ensure the launcher is visible - could be peeking from bottom due to hint. + Lipstick.compositor.setCurrentWindow(Lipstick.compositor.launcherLayer.window) + } } else if (launcherEditMode) { setEditMode(false) } else { @@ -212,14 +313,18 @@ IconGridViewBase { } else { Desktop.instance.switcher.activateWindowFor(object) } - Lipstick.compositor.launcherLayer.hide() + + if (Lipstick.compositor.multitaskingHome) { + Lipstick.compositor.launcherLayer.hide() + } + gridview.itemLaunched() } Lipstick.compositor.launcherLayer.pinned = false } onPressAndHold: { - if (Lipstick.compositor.launcherLayer.active && !launcherEditMode) { + if (!launcherEditMode) { setEditMode(true) wrapper.startReordering(true) } @@ -302,19 +407,20 @@ IconGridViewBase { visible: !launcherEditMode || isFolder } - Component { - id: updatingComponent - BusyIndicator { - anchors.centerIn: launcherIcon - running: model.object.isUpdating - opacity: running - Behavior on opacity { FadeAnimation {} } - Label { - anchors.centerIn: parent - text: model.object.updatingProgress + '%' - visible: model.object.isUpdating && model.object.updatingProgress >= 0 && model.object.updatingProgress <= 100 - font.pixelSize: Theme.fontSizeSmall - } + BusyIndicator { + anchors.centerIn: launcherIcon + running: model.object.isUpdating + || (!Lipstick.compositor.multitaskingHome && model.object.isLaunching) + opacity: running + Behavior on opacity { FadeAnimation {} } + + // If the launcher icons use custom size scale the busy indicator accordingly to align dimensions + scale: (Theme.iconSizeLauncher / Theme.pixelRatio) / 86 + Label { + anchors.centerIn: parent + text: model.object.updatingProgress + '%' + visible: model.object.isUpdating && model.object.updatingProgress >= 0 && model.object.updatingProgress <= 100 + font.pixelSize: Theme.fontSizeSmall } } diff --git a/usr/share/lipstick-jolla-home-qt5/launcher/LauncherView.qml b/usr/share/lipstick-jolla-home-qt5/launcher/LauncherView.qml index ebe00fc3..975b3d79 100644 --- a/usr/share/lipstick-jolla-home-qt5/launcher/LauncherView.qml +++ b/usr/share/lipstick-jolla-home-qt5/launcher/LauncherView.qml @@ -5,13 +5,12 @@ * License: Proprietary */ -import QtQuick 2.0 +import QtQuick 2.6 import org.nemomobile.lipstick 0.1 import Sailfish.Silica 1.0 import Sailfish.Silica.private 1.0 import com.jolla.lipstick 0.1 import "../main" -import "../backgrounds" ApplicationWindow { id: launcherWindow @@ -22,12 +21,6 @@ ApplicationWindow { allowedOrientations: Lipstick.compositor.topmostWindowOrientation - children: MenuBackground { - z: -1 - - anchors.fill: parent - } - initialPage: Component { Page { id: page @@ -35,11 +28,27 @@ ApplicationWindow { layer.enabled: orientationTransitionRunning Launcher { + id: launcher + // We don't want the pager to resize due to keyboard being shown. height: Math.ceil(page.height + pageStack.panelSize) width: parent.width } + Binding { + when: !Lipstick.compositor.multitaskingHome + target: Lipstick.compositor.switcherLayer + property: "contentY" + value: { + if (launcher.openedChildFolder) { + var statusBar = Lipstick.compositor.homeLayer.statusBar + return statusBar.baseY + statusBar.height + } else { + return launcher.contentY + } + } + } + orientationTransitions: OrientationTransition { page: page applicationWindow: launcherWindow diff --git a/usr/share/lipstick-jolla-home-qt5/launcher/OpenFileDialog.qml b/usr/share/lipstick-jolla-home-qt5/launcher/OpenFileDialog.qml index 3101edc5..3e77e871 100644 --- a/usr/share/lipstick-jolla-home-qt5/launcher/OpenFileDialog.qml +++ b/usr/share/lipstick-jolla-home-qt5/launcher/OpenFileDialog.qml @@ -8,7 +8,7 @@ SystemWindow { id: root property string content - property bool isUri: true + property bool isGeoUri: openFileModel.url.toString().substring(0, 4) == "geo:" property real reservedHeight: ((Screen.sizeCategory < Screen.Large) ? 0.2 * Screen.height : 0.4 * Screen.height) - 1 property bool verticalOrientation: Lipstick.compositor.topmostWindowOrientation === Qt.PrimaryOrientation @@ -48,18 +48,22 @@ SystemWindow { bottomPadding: 0 title: openFileModel.isFile - //% "Open file" - ? qsTrId("lipstick-jolla-home-la-open_file") - //% "Open link" - : qsTrId("lipstick-jolla-home-la-open_link") + ? //% "Open file" + qsTrId("lipstick-jolla-home-la-open_file") + : root.isGeoUri + ? //% "Open location" + qsTrId("lipstick-jolla-home-la-open_location") + : //% "Open link" + qsTrId("lipstick-jolla-home-la-open_link") //% "Attempting to open" - description: qsTrId("lipstick-jolla-home-la-attempting_to_open_file") + description: root.isGeoUri ? "" : qsTrId("lipstick-jolla-home-la-attempting_to_open_file") } Label { x: (Screen.sizeCategory < Screen.Large) ? Theme.horizontalPageMargin : 0 // Match the padding inside SystemDialogHeader width: parent.width - (2 * x) + visible: !root.isGeoUri // not pretty enough to show color: Theme.highlightColor wrapMode: Text.Wrap maximumLineCount: 2 diff --git a/usr/share/lipstick-jolla-home-qt5/launcher/UninstallButton.qml b/usr/share/lipstick-jolla-home-qt5/launcher/UninstallButton.qml index cdaa07b3..7cf581c8 100644 --- a/usr/share/lipstick-jolla-home-qt5/launcher/UninstallButton.qml +++ b/usr/share/lipstick-jolla-home-qt5/launcher/UninstallButton.qml @@ -5,7 +5,7 @@ ** ****************************************************************************/ -import QtQuick 2.1 +import QtQuick 2.6 import Sailfish.Silica 1.0 Image { diff --git a/usr/share/lipstick-jolla-home-qt5/layers/AlarmLayer.qml b/usr/share/lipstick-jolla-home-qt5/layers/AlarmLayer.qml index a6237dee..f19d6114 100644 --- a/usr/share/lipstick-jolla-home-qt5/layers/AlarmLayer.qml +++ b/usr/share/lipstick-jolla-home-qt5/layers/AlarmLayer.qml @@ -5,7 +5,7 @@ ** ****************************************************************************/ -import QtQuick 2.0 +import QtQuick 2.6 import Sailfish.Silica 1.0 import Sailfish.Lipstick 1.0 import org.nemomobile.lipstick 0.1 diff --git a/usr/share/lipstick-jolla-home-qt5/layers/AppLayer.qml b/usr/share/lipstick-jolla-home-qt5/layers/AppLayer.qml index 4d31d5a2..bbc98f7f 100644 --- a/usr/share/lipstick-jolla-home-qt5/layers/AppLayer.qml +++ b/usr/share/lipstick-jolla-home-qt5/layers/AppLayer.qml @@ -1,4 +1,4 @@ -import QtQuick 2.0 +import QtQuick 2.6 import Sailfish.Silica 1.0 import org.nemomobile.lipstick 0.1 import Sailfish.Lipstick 1.0 @@ -22,7 +22,7 @@ StackLayer { onQueueWindow: { if (Desktop.startupWizardRunning) { if (JollaSystemInfo.matchingPidForCommand(window.window.processId, '/usr/bin/jolla-startupwizard', true) !== -1 - || JollaSystemInfo.matchingPidForCommand(window.window.processId, '/usr/bin/sailfish-browser', true) !== -1) { + || JollaSystemInfo.matchingPidForCommand(window.window.processId, '/usr/bin/sailfish-captiveportal', true) !== -1) { contentItem.appendItem(window) } } else if (!Desktop.instance.switcher.checkMinimized(window.window.windowId)) { diff --git a/usr/share/lipstick-jolla-home-qt5/layers/DialogLayer.qml b/usr/share/lipstick-jolla-home-qt5/layers/DialogLayer.qml index cc21a527..be78b3a5 100644 --- a/usr/share/lipstick-jolla-home-qt5/layers/DialogLayer.qml +++ b/usr/share/lipstick-jolla-home-qt5/layers/DialogLayer.qml @@ -5,7 +5,7 @@ ** ****************************************************************************/ -import QtQuick 2.0 +import QtQuick 2.6 import Sailfish.Silica 1.0 import org.nemomobile.lipstick 0.1 import "../backgrounds" diff --git a/usr/share/lipstick-jolla-home-qt5/layers/EdgeLayer.qml b/usr/share/lipstick-jolla-home-qt5/layers/EdgeLayer.qml index bea6307e..36646713 100644 --- a/usr/share/lipstick-jolla-home-qt5/layers/EdgeLayer.qml +++ b/usr/share/lipstick-jolla-home-qt5/layers/EdgeLayer.qml @@ -1,4 +1,4 @@ -import QtQuick 2.0 +import QtQuick 2.6 import QtQuick.Window 2.1 as QtQuick import Sailfish.Silica 1.0 import org.nemomobile.lipstick 0.1 @@ -96,6 +96,7 @@ Layer { clip: edgeLayer._showActive || edgeLayer._hideActive || edgeLayer._smoothClip + || edgeLayer.pinned || (interactiveArea && interactiveArea.drag.active) || gestureTransition.running || visibleTransition.running diff --git a/usr/share/lipstick-jolla-home-qt5/layers/EventsLayer.qml b/usr/share/lipstick-jolla-home-qt5/layers/EventsLayer.qml index c9c2639b..dea928ea 100644 --- a/usr/share/lipstick-jolla-home-qt5/layers/EventsLayer.qml +++ b/usr/share/lipstick-jolla-home-qt5/layers/EventsLayer.qml @@ -1,4 +1,4 @@ -import QtQuick 2.0 +import QtQuick 2.6 import Sailfish.Silica 1.0 import org.nemomobile.lipstick 0.1 diff --git a/usr/share/lipstick-jolla-home-qt5/layers/HomeLayer.qml b/usr/share/lipstick-jolla-home-qt5/layers/HomeLayer.qml index c1a64f26..77722439 100644 --- a/usr/share/lipstick-jolla-home-qt5/layers/HomeLayer.qml +++ b/usr/share/lipstick-jolla-home-qt5/layers/HomeLayer.qml @@ -5,11 +5,11 @@ * License: Proprietary */ -import QtQuick 2.0 +import QtQuick 2.6 import Sailfish.Silica 1.0 import org.nemomobile.lipstick 0.1 import Nemo.DBus 2.0 -import org.nemomobile.configuration 1.0 +import Nemo.Configuration 1.0 import com.jolla.lipstick 0.1 import Sailfish.Lipstick 1.0 import "../compositor" @@ -194,7 +194,8 @@ Pannable { SwitcherLayer { id: switcherLayer - readonly property real inactiveScale: Screen.sizeCategory >= Screen.Large ? 0.90 : 0.83 + readonly property real inactiveScale: Lipstick.compositor.multitaskingHome ? (Screen.sizeCategory >= Screen.Large ? 0.90 : 0.83) + : 1.0 readonly property bool moving: homescreen.moving || (Desktop.instance && Desktop.instance.switcher.moving) property bool inhibitScale diff --git a/usr/share/lipstick-jolla-home-qt5/layers/LauncherLayer.qml b/usr/share/lipstick-jolla-home-qt5/layers/LauncherLayer.qml index ec3926af..e0ec364c 100644 --- a/usr/share/lipstick-jolla-home-qt5/layers/LauncherLayer.qml +++ b/usr/share/lipstick-jolla-home-qt5/layers/LauncherLayer.qml @@ -1,7 +1,8 @@ -import QtQuick 2.0 +import QtQuick 2.6 import Sailfish.Silica 1.0 import Sailfish.Lipstick 1.0 import org.nemomobile.lipstick 0.1 +import "../backgrounds" EdgeLayer { id: launcherLayer @@ -110,6 +111,12 @@ EdgeLayer { hintDuration: 600 _finishDragWithAnimation: !pinned && !_wasPinning + MenuBackground { + z: -1 + anchors.fill: parent + parent: launcherLayer.contentItem + } + Timer { // long-press timer to detect pinning interval: 400 @@ -117,8 +124,10 @@ EdgeLayer { && (absoluteExposure > cellHeight/2 && (maximumExposure - absoluteExposure) > cellHeight/2) onRunningChanged: launcherLayer._startPinPosition = launcherLayer._transposed ? launcherLayer.x : launcherLayer.y onTriggered: { - launcherLayer.pinPosition = launcherLayer._transposed ? launcherLayer.x : launcherLayer.y - launcherLayer.pinned = true + if (Lipstick.compositor.multitaskingHome) { + launcherLayer.pinPosition = launcherLayer._transposed ? launcherLayer.x : launcherLayer.y + launcherLayer.pinned = true + } } } diff --git a/usr/share/lipstick-jolla-home-qt5/layers/Layer.qml b/usr/share/lipstick-jolla-home-qt5/layers/Layer.qml index 6a1da4b7..6dea9ef9 100644 --- a/usr/share/lipstick-jolla-home-qt5/layers/Layer.qml +++ b/usr/share/lipstick-jolla-home-qt5/layers/Layer.qml @@ -5,7 +5,7 @@ * License: Proprietary */ -import QtQuick 2.0 +import QtQuick 2.6 import Sailfish.Silica 1.0 import Sailfish.Lipstick 1.0 import com.jolla.lipstick 0.1 diff --git a/usr/share/lipstick-jolla-home-qt5/layers/LayerEdgeIndicator.qml b/usr/share/lipstick-jolla-home-qt5/layers/LayerEdgeIndicator.qml index 55bc930b..5f7b2136 100644 --- a/usr/share/lipstick-jolla-home-qt5/layers/LayerEdgeIndicator.qml +++ b/usr/share/lipstick-jolla-home-qt5/layers/LayerEdgeIndicator.qml @@ -5,7 +5,7 @@ ** ****************************************************************************/ -import QtQuick 2.1 +import QtQuick 2.6 import Sailfish.Silica 1.0 Icon { diff --git a/usr/share/lipstick-jolla-home-qt5/layers/LayersParent.qml b/usr/share/lipstick-jolla-home-qt5/layers/LayersParent.qml index 396dc2e5..841458e8 100644 --- a/usr/share/lipstick-jolla-home-qt5/layers/LayersParent.qml +++ b/usr/share/lipstick-jolla-home-qt5/layers/LayersParent.qml @@ -1,4 +1,4 @@ -import QtQuick 2.2 +import QtQuick 2.6 import QtQuick.Window 2.2 as QtQuick Item { diff --git a/usr/share/lipstick-jolla-home-qt5/layers/LockScreenLayer.qml b/usr/share/lipstick-jolla-home-qt5/layers/LockScreenLayer.qml index ca4c6110..0211b34d 100644 --- a/usr/share/lipstick-jolla-home-qt5/layers/LockScreenLayer.qml +++ b/usr/share/lipstick-jolla-home-qt5/layers/LockScreenLayer.qml @@ -5,7 +5,7 @@ * License: Proprietary */ -import QtQuick 2.0 +import QtQuick 2.6 import Sailfish.Silica 1.0 import org.nemomobile.lipstick 0.1 import com.jolla.lipstick 0.1 diff --git a/usr/share/lipstick-jolla-home-qt5/layers/NotificationOverviewLayer.qml b/usr/share/lipstick-jolla-home-qt5/layers/NotificationOverviewLayer.qml index 028ea6ce..dabb9605 100644 --- a/usr/share/lipstick-jolla-home-qt5/layers/NotificationOverviewLayer.qml +++ b/usr/share/lipstick-jolla-home-qt5/layers/NotificationOverviewLayer.qml @@ -5,7 +5,7 @@ ** ****************************************************************************/ -import QtQuick 2.2 +import QtQuick 2.6 import Sailfish.Silica 1.0 import org.nemomobile.lipstick 0.1 import com.jolla.lipstick 0.1 @@ -27,7 +27,7 @@ Layer { } exposed: { - if (!hasNotifications) { + if (!hasNotifications || !Lipstick.compositor.multitaskingHome) { return false } return (Lipstick.compositor.lockScreenLayer.exposed || Lipstick.compositor.homeVisible || lipstickSettings.lowPowerMode) @@ -176,6 +176,7 @@ Layer { anchors.verticalCenter: parent.verticalCenter showCount: Screen.sizeCategory >= Screen.Large || lockScreenLocked + maximumCount: Lipstick.compositor.experimentalFeatures.lockscreen_notification_count layer.enabled: lipstickSettings.lowPowerMode layer.effect: ShaderEffect { diff --git a/usr/share/lipstick-jolla-home-qt5/layers/OverlayLayer.qml b/usr/share/lipstick-jolla-home-qt5/layers/OverlayLayer.qml index 465afd61..4dafa81d 100644 --- a/usr/share/lipstick-jolla-home-qt5/layers/OverlayLayer.qml +++ b/usr/share/lipstick-jolla-home-qt5/layers/OverlayLayer.qml @@ -1,4 +1,4 @@ -import QtQuick 2.0 +import QtQuick 2.6 import Sailfish.Silica 1.0 import org.nemomobile.lipstick 0.1 import com.jolla.lipstick 0.1 @@ -9,16 +9,9 @@ Item { property alias contentItem: overlayLayer onChildrenChanged: updateWindows() + z: 10000 // ensure on top of siblings after reparenting - readonly property Item activeFocusItem: root.activeFocusItem - // There's no notification for item flags, but the only known instances of - // ItemAcceptsInputMethod changing dynamically is the TextInput/Edit read only - // property. By including it in the binding we'll force a re-evaluation if - // the property both exists and changes. - && !root.activeFocusItem.readOnly - && JollaSystemInfo.itemAcceptsInputMethod(root.activeFocusItem) - ? root.activeFocusItem - : null + property Item activeFocusItem onActiveFocusItemChanged: { // Search for the layer of the focus item diff --git a/usr/share/lipstick-jolla-home-qt5/layers/PannableLayer.qml b/usr/share/lipstick-jolla-home-qt5/layers/PannableLayer.qml index 57cd306d..e3023bfb 100644 --- a/usr/share/lipstick-jolla-home-qt5/layers/PannableLayer.qml +++ b/usr/share/lipstick-jolla-home-qt5/layers/PannableLayer.qml @@ -1,4 +1,4 @@ -import QtQuick 2.0 +import QtQuick 2.6 import Sailfish.Silica 1.0 import Sailfish.Lipstick 1.0 import org.nemomobile.lipstick 0.1 diff --git a/usr/share/lipstick-jolla-home-qt5/layers/PartnerLayer.qml b/usr/share/lipstick-jolla-home-qt5/layers/PartnerLayer.qml index cf9d4142..54c6ccf5 100644 --- a/usr/share/lipstick-jolla-home-qt5/layers/PartnerLayer.qml +++ b/usr/share/lipstick-jolla-home-qt5/layers/PartnerLayer.qml @@ -5,7 +5,7 @@ * License: Proprietary */ -import QtQuick 2.2 +import QtQuick 2.6 import QtQuick.Window 2.1 as QtQuick import Sailfish.Silica 1.0 import org.nemomobile.lipstick 0.1 diff --git a/usr/share/lipstick-jolla-home-qt5/layers/StackLayer.qml b/usr/share/lipstick-jolla-home-qt5/layers/StackLayer.qml index 046d84db..c56c25e3 100644 --- a/usr/share/lipstick-jolla-home-qt5/layers/StackLayer.qml +++ b/usr/share/lipstick-jolla-home-qt5/layers/StackLayer.qml @@ -5,7 +5,7 @@ * License: Proprietary */ -import QtQuick 2.0 +import QtQuick 2.6 import Sailfish.Silica 1.0 import org.nemomobile.lipstick 0.1 import "../backgrounds" @@ -153,7 +153,8 @@ Layer { } function show(w, quick) { - if (quick === undefined) quick = false + if (quick === undefined) + quick = false if (Lipstick.compositor.debug) { console.log("StackLayer: Show window: \"", w, "\" current window: \"", window, "\"") console.log("StackLayer: Show active layer: \"", layer, "\" is active: ", active) diff --git a/usr/share/lipstick-jolla-home-qt5/layers/SwitcherLayer.qml b/usr/share/lipstick-jolla-home-qt5/layers/SwitcherLayer.qml index b6f1eadb..90058eaf 100644 --- a/usr/share/lipstick-jolla-home-qt5/layers/SwitcherLayer.qml +++ b/usr/share/lipstick-jolla-home-qt5/layers/SwitcherLayer.qml @@ -1,4 +1,4 @@ -import QtQuick 2.0 +import QtQuick 2.6 import Sailfish.Silica 1.0 import org.nemomobile.lipstick 0.1 import com.jolla.lipstick 0.1 diff --git a/usr/share/lipstick-jolla-home-qt5/layers/TopMenuLayer.qml b/usr/share/lipstick-jolla-home-qt5/layers/TopMenuLayer.qml index ebd46f09..56b08694 100644 --- a/usr/share/lipstick-jolla-home-qt5/layers/TopMenuLayer.qml +++ b/usr/share/lipstick-jolla-home-qt5/layers/TopMenuLayer.qml @@ -1,4 +1,4 @@ -import QtQuick 2.0 +import QtQuick 2.6 import Sailfish.Silica 1.0 import Sailfish.Lipstick 1.0 import org.nemomobile.lipstick 0.1 diff --git a/usr/share/lipstick-jolla-home-qt5/lockscreen/Clock.qml b/usr/share/lipstick-jolla-home-qt5/lockscreen/Clock.qml index 5cf87b32..5da008e6 100644 --- a/usr/share/lipstick-jolla-home-qt5/lockscreen/Clock.qml +++ b/usr/share/lipstick-jolla-home-qt5/lockscreen/Clock.qml @@ -5,10 +5,10 @@ ** ****************************************************************************/ -import QtQuick 2.0 +import QtQuick 2.6 import Sailfish.Silica 1.0 import Sailfish.Lipstick 1.0 -import org.nemomobile.time 1.0 +import Nemo.Time 1.0 import org.nemomobile.lipstick 0.1 import "../main" diff --git a/usr/share/lipstick-jolla-home-qt5/lockscreen/DeviceLockPrompt.qml b/usr/share/lipstick-jolla-home-qt5/lockscreen/DeviceLockPrompt.qml index fe63c910..7fdf74a7 100644 --- a/usr/share/lipstick-jolla-home-qt5/lockscreen/DeviceLockPrompt.qml +++ b/usr/share/lipstick-jolla-home-qt5/lockscreen/DeviceLockPrompt.qml @@ -5,7 +5,7 @@ ** ****************************************************************************/ -import QtQuick 2.0 +import QtQuick 2.6 import Sailfish.Silica 1.0 import com.jolla.settings.system 1.0 import com.jolla.lipstick 0.1 diff --git a/usr/share/lipstick-jolla-home-qt5/lockscreen/DeviceLockView.qml b/usr/share/lipstick-jolla-home-qt5/lockscreen/DeviceLockView.qml index 9c4003ba..0ba634b0 100644 --- a/usr/share/lipstick-jolla-home-qt5/lockscreen/DeviceLockView.qml +++ b/usr/share/lipstick-jolla-home-qt5/lockscreen/DeviceLockView.qml @@ -7,13 +7,13 @@ ** ****************************************************************************/ -import QtQuick 2.0 +import QtQuick 2.6 import Sailfish.Silica 1.0 import com.jolla.lipstick 0.1 import com.jolla.settings.system 1.0 -import org.nemomobile.configuration 1.0 +import Nemo.Configuration 1.0 import org.nemomobile.devicelock 1.0 -import org.nemomobile.ngf 1.0 +import Nemo.Ngf 1.0 import org.nemomobile.lipstick 0.1 DeviceLockInput { @@ -29,15 +29,15 @@ DeviceLockInput { // Don't show emergency call button if device has no voice capability, in case something happen and // the value is undefined show it since this is critical functionality - showEmergencyButton: Desktop.simManager.enabledModems.length > 0 || !Desktop.simManager.ready + showEmergencyButton: Desktop.deviceInfo.hasCellularVoiceCallFeature && (Desktop.simManager.enabledModems.length > 0 || !Desktop.simManager.ready) focus: !Desktop.startupWizardRunning + pasteDisabled: true Timer { id: resetTimer interval: 300 onTriggered: { pininput.titleText = pininput.enterSecurityCode - pininput.lastChance = false pininput.emergency = false pininput._resetView() } diff --git a/usr/share/lipstick-jolla-home-qt5/lockscreen/EdgeIndicator.qml b/usr/share/lipstick-jolla-home-qt5/lockscreen/EdgeIndicator.qml index f0a46b7c..80e30864 100644 --- a/usr/share/lipstick-jolla-home-qt5/lockscreen/EdgeIndicator.qml +++ b/usr/share/lipstick-jolla-home-qt5/lockscreen/EdgeIndicator.qml @@ -1,4 +1,4 @@ -import QtQuick 2.0 +import QtQuick 2.6 import Sailfish.Silica 1.0 import org.nemomobile.lipstick 0.1 diff --git a/usr/share/lipstick-jolla-home-qt5/lockscreen/LockItem.qml b/usr/share/lipstick-jolla-home-qt5/lockscreen/LockItem.qml index f5e92756..71a4f3b0 100644 --- a/usr/share/lipstick-jolla-home-qt5/lockscreen/LockItem.qml +++ b/usr/share/lipstick-jolla-home-qt5/lockscreen/LockItem.qml @@ -7,13 +7,12 @@ ** ****************************************************************************/ -import QtQuick 2.2 +import QtQuick 2.6 import Sailfish.Silica 1.0 import Sailfish.Telephony 1.0 import Sailfish.Media 1.0 import Nemo.DBus 2.0 import com.jolla.lipstick 0.1 -import com.jolla.settings.system 1.0 import org.nemomobile.lipstick 0.1 import "../backgrounds" import "../main" @@ -27,7 +26,6 @@ SilicaFlickable { property alias mpris: mpris property alias leftIndicator: leftIndicator property alias rightIndicator: rightIndicator - property string iconSuffix property real contentTopMargin property int statusBarHeight @@ -306,7 +304,13 @@ SilicaFlickable { width: bottomBackground.width - OngoingCall { + Loader { + active: Desktop.deviceInfo.hasCellularVoiceCallFeature + width: parent.width + + sourceComponent: Component { + OngoingCall {} + } } Row { diff --git a/usr/share/lipstick-jolla-home-qt5/lockscreen/LockScreen.qml b/usr/share/lipstick-jolla-home-qt5/lockscreen/LockScreen.qml index def8c6ae..3927113c 100644 --- a/usr/share/lipstick-jolla-home-qt5/lockscreen/LockScreen.qml +++ b/usr/share/lipstick-jolla-home-qt5/lockscreen/LockScreen.qml @@ -5,16 +5,15 @@ ** ****************************************************************************/ -import QtQuick 2.0 +import QtQuick 2.6 import org.nemomobile.lipstick 0.1 -import org.nemomobile.configuration 1.0 +import Nemo.Configuration 1.0 import org.nemomobile.devicelock 1.0 -import org.nemomobile.ngf 1.0 +import Nemo.Ngf 1.0 import Sailfish.Silica 1.0 -import Sailfish.Ambience 1.0 import com.jolla.lipstick 0.1 import Sailfish.Lipstick 1.0 -import org.nemomobile.notifications 1.0 +import Nemo.Notifications 1.0 import "../compositor" import "../main" import "../statusarea" @@ -40,6 +39,12 @@ ApplicationWindow { property bool vignetteActive + // This must be synchronized with tk_lock state. Hence, connect to + // device lock state reported by Desktop.qml that is from system bus. + readonly property bool systemStartedAndUnlocked: systemStarted.value && Desktop.deviceLockState === DeviceLock.Unlocked + readonly property bool deviceLockCodeInUse: DeviceLock.automaticLocking != -1 + readonly property bool dismissLockscreenOnBootup: Lipstick.compositor.experimentalFeatures.dismiss_lockscreen_on_bootup + palette.colorScheme: lockScreen.lowPowerMode ? Theme.LightOnDark : undefined onVignetteActiveChanged: { @@ -54,7 +59,7 @@ ApplicationWindow { id: systemStarted value: (!startupWizardExpiry.running || Lipstick.compositor.appLayer.opaque) - && (PinQueryAgent.simStatus != PinQueryAgent.SimUndefined || PinQueryAgent.simPinCompleted) + && (PinQueryAgent.simStatus != PinQueryAgent.SimUndefined || PinQueryAgent.simPinCompleted) } Timer { @@ -99,7 +104,8 @@ ApplicationWindow { lockScreen.nextPannableItem(false, false) } else { lockScreen.reset() - if (Lipstick.compositor.showDeviceLock && deviceLockItem.locked) { + if (Lipstick.compositor.pendingShowUnlockScreen && deviceLockItem.locked) { + Lipstick.compositor.pendingShowUnlockScreen = false lockScreen.nextPannableItem(false, true) } } @@ -152,7 +158,7 @@ ApplicationWindow { if (lipstickSettings.lockscreenVisible && !Lipstick.compositor.cameraLayer.active) { Lipstick.compositor.setCurrentWindow(Lipstick.compositor.lockScreenLayer.window) } else if (!lipstickSettings.lockscreenVisible && Desktop.deviceLockState == DeviceLock.Locked) { - Lipstick.compositor.showDeviceLock = true + Lipstick.compositor.pendingShowUnlockScreen = true } } } @@ -275,22 +281,24 @@ ApplicationWindow { } } - Connections { - target: Desktop - - // This must be synchronized with tk_lock state. Hence, connect to - // device lock state reported by Desktop.qml that is from system bus. - onDeviceLockStateChanged: { - if (systemStarted.value && Desktop.deviceLockState === DeviceLock.Unlocked) { - if (!DeviceLock.enabled) { + onSystemStartedAndUnlockedChanged: { + if (systemStartedAndUnlocked) { + if (!deviceLockCodeInUse) { + // We can end up here only due to the singular Undefined -> Unlocked device lock state + // transition that is seen during lipstick startup when device lock code is not in use + if (dismissLockscreenOnBootup) { + // Optional behvior selectable via dconf: land to home/launcher + lockScreen.unlock(false) + } else { + // Default behevior: land to lockscreen if (deviceLockContainer.isCurrentItem) { lockScreen.setCurrentItem(lockContainer, lockScreen.visible) } - } else if (!lockScreen.nextPannableItem(true, false)) { - lockScreen.unlock(true) - } else if (lockScreen.pinQueryPannable) { - lockScreen.pinQueryPannable.deviceWasLocked = true } + } else if (!lockScreen.nextPannableItem(true, false)) { + lockScreen.unlock(true) + } else if (lockScreen.pinQueryPannable) { + lockScreen.pinQueryPannable.deviceWasLocked = true } } } @@ -315,14 +323,13 @@ ApplicationWindow { lockScreen.nextPannableItem(false, false) lockScreen.open(false) - if (lockedOut ) { + if (lockedOut) { lipstickSettings.lockScreen(true) } } else { lockScreen.activate(lockScreenPage.vignetteActive) lipstickSettings.lockScreen(true) } - } onNotice: { @@ -397,9 +404,6 @@ ApplicationWindow { readonly property bool pendingPannableItem: deviceLockItem.locked || needPinQuery - // Suffix that should be added to all theme icons that are shown in low power mode - property string iconSuffix: lipstickSettings.lowPowerMode ? ('?' + Theme.highlightColor) : '' - property color textColor: Lipstick.compositor.lockScreenLayer.textColor readonly property bool locked: lipstickSettings.lockscreenVisible || Desktop.deviceLockState >= DeviceLock.Locked @@ -600,7 +604,6 @@ ApplicationWindow { visible: systemStarted.value allowAnimations: Lipstick.compositor.lockScreenLayer.vignette.opened - iconSuffix: lockScreen.iconSuffix clock.followPeekPosition: !parent.rightItem Binding { target: lockItem.mpris.item; property: "enabled"; value: !lockScreen.lowPowerMode } @@ -652,7 +655,7 @@ ApplicationWindow { && item.authenticationInput.status !== AuthenticationInput.Idle ? 1 : 0 - headingVerticalOffset: statusBar.y + statusBar.height + Theme.paddingLarge + headingVerticalOffset: statusBar.y + statusBar.height enabled: locked && !lockScreen.moving && deviceLockContainer.isCurrentItem focus: !Desktop.startupWizardRunning diff --git a/usr/share/lipstick-jolla-home-qt5/lockscreen/OngoingCall.qml b/usr/share/lipstick-jolla-home-qt5/lockscreen/OngoingCall.qml index f4155a26..090f1d81 100644 --- a/usr/share/lipstick-jolla-home-qt5/lockscreen/OngoingCall.qml +++ b/usr/share/lipstick-jolla-home-qt5/lockscreen/OngoingCall.qml @@ -12,10 +12,10 @@ import QtQuick 2.6 import Sailfish.Silica 1.0 import Sailfish.Lipstick 1.0 import Sailfish.Media 1.0 -import MeeGo.QOfono 0.2 +import QOfono 0.2 import Nemo.DBus 2.0 import org.nemomobile.lipstick 0.1 -import org.nemomobile.policy 1.0 +import Nemo.Policy 1.0 import com.jolla.lipstick 0.1 BackgroundItem diff --git a/usr/share/lipstick-jolla-home-qt5/lockscreen/SneakPeekHint.qml b/usr/share/lipstick-jolla-home-qt5/lockscreen/SneakPeekHint.qml index ff165c86..3059c0a1 100644 --- a/usr/share/lipstick-jolla-home-qt5/lockscreen/SneakPeekHint.qml +++ b/usr/share/lipstick-jolla-home-qt5/lockscreen/SneakPeekHint.qml @@ -1,4 +1,4 @@ -import QtQuick 2.0 +import QtQuick 2.6 import Sailfish.Silica 1.0 import org.nemomobile.lipstick 0.1 diff --git a/usr/share/lipstick-jolla-home-qt5/lockscreen/Vignette.qml b/usr/share/lipstick-jolla-home-qt5/lockscreen/Vignette.qml index 159fabd4..b5760612 100644 --- a/usr/share/lipstick-jolla-home-qt5/lockscreen/Vignette.qml +++ b/usr/share/lipstick-jolla-home-qt5/lockscreen/Vignette.qml @@ -5,7 +5,7 @@ * License: Proprietary */ -import QtQuick 2.2 +import QtQuick 2.6 import Sailfish.Silica 1.0 import org.nemomobile.lipstick 0.1 diff --git a/usr/share/lipstick-jolla-home-qt5/lockscreen/WeatherIndicator.qml b/usr/share/lipstick-jolla-home-qt5/lockscreen/WeatherIndicator.qml index c74913de..2ac48efc 100644 --- a/usr/share/lipstick-jolla-home-qt5/lockscreen/WeatherIndicator.qml +++ b/usr/share/lipstick-jolla-home-qt5/lockscreen/WeatherIndicator.qml @@ -5,7 +5,7 @@ ** ****************************************************************************/ -import QtQuick 2.0 +import QtQuick 2.6 import Sailfish.Silica 1.0 import Sailfish.Weather 1.0 as Weather import org.nemomobile.lipstick 0.1 diff --git a/usr/share/lipstick-jolla-home-qt5/lockscreen/WeatherIndicatorLoader.qml b/usr/share/lipstick-jolla-home-qt5/lockscreen/WeatherIndicatorLoader.qml index bbaf95e2..9fff500b 100644 --- a/usr/share/lipstick-jolla-home-qt5/lockscreen/WeatherIndicatorLoader.qml +++ b/usr/share/lipstick-jolla-home-qt5/lockscreen/WeatherIndicatorLoader.qml @@ -5,7 +5,7 @@ ** ****************************************************************************/ -import QtQuick 2.0 +import QtQuick 2.6 import Sailfish.Silica 1.0 import org.nemomobile.lipstick 0.1 import com.jolla.lipstick 0.1 diff --git a/usr/share/lipstick-jolla-home-qt5/main.qml b/usr/share/lipstick-jolla-home-qt5/main.qml index 72dd30b1..c7ae9777 100644 --- a/usr/share/lipstick-jolla-home-qt5/main.qml +++ b/usr/share/lipstick-jolla-home-qt5/main.qml @@ -5,14 +5,14 @@ ** ****************************************************************************/ -import QtQuick 2.1 +import QtQuick 2.6 import QtFeedback 5.0 import Sailfish.Silica 1.0 import Sailfish.Lipstick 1.0 import org.nemomobile.lipstick 0.1 import Nemo.DBus 2.0 -import org.nemomobile.notifications 1.0 as Nemo -import org.nemomobile.configuration 1.0 +import Nemo.Notifications 1.0 as Nemo +import Nemo.Configuration 1.0 import com.jolla.lipstick 0.1 import "main" @@ -61,9 +61,8 @@ ApplicationWindow { allowedOrientations: Orientation.All property alias switcher: switcher - property bool coversVisible: Lipstick.compositor.switcherLayer.visible - readonly property bool active: Lipstick.compositor.switcherLayer.active && Lipstick.compositor.systemInitComplete + onActiveChanged: { if (!active) { hintTimer.stop() @@ -79,8 +78,8 @@ ApplicationWindow { } } - onCoversVisibleChanged: { - if (coversVisible) { + onVisibleChanged: { + if (visible) { CoverControl.status = Cover.Activating CoverControl.status = Cover.Active } else { @@ -105,7 +104,11 @@ ApplicationWindow { Timer { id: hintTimer interval: 1000 - onTriggered: if (!Lipstick.compositor.topMenuHinting) Lipstick.compositor.launcherLayer.showHint() + onTriggered: { + if (!Lipstick.compositor.topMenuHinting && Lipstick.compositor.multitaskingHome) { + Lipstick.compositor.launcherLayer.showHint() + } + } } Switcher { @@ -114,6 +117,7 @@ ApplicationWindow { } Binding { + when: Lipstick.compositor.multitaskingHome target: Lipstick.compositor.switcherLayer property: "contentY" value: switcher.contentY @@ -153,19 +157,23 @@ ApplicationWindow { onCancelTransfer: bluetoothObexSystemAgent.cancelTransfer(transferPath) } - VoicecallAgent { - onDialNumber: voicecall.dial(number) - } + Loader { + active: Desktop.deviceInfo.hasCellularVoiceCallFeature + sourceComponent: Component { + VoicecallAgent { + property QtObject voicecall: DBusInterface { + service: "com.jolla.voicecall.ui" + path: "/" + iface: "com.jolla.voicecall.ui" - DBusInterface { - id: voicecall - service: "com.jolla.voicecall.ui" - path: "/" - iface: "com.jolla.voicecall.ui" - - function dial(number) { - call('dial', number) - } + function dial(number) { + call('dial', number) + } + } + + onDialNumber: voicecall.dial(number) + } + } } ShutterKeyHandler { diff --git a/usr/share/lipstick-jolla-home-qt5/main/ConnectionManager.qml b/usr/share/lipstick-jolla-home-qt5/main/ConnectionManager.qml index 737c91b0..24b47a02 100644 --- a/usr/share/lipstick-jolla-home-qt5/main/ConnectionManager.qml +++ b/usr/share/lipstick-jolla-home-qt5/main/ConnectionManager.qml @@ -1,6 +1,6 @@ pragma Singleton -import QtQuick 2.2 -import MeeGo.Connman 0.2 +import QtQuick 2.6 +import Connman 0.2 QtObject { property alias cellularPath: networkManager.CellularTechnology diff --git a/usr/share/lipstick-jolla-home-qt5/main/Desktop.qml b/usr/share/lipstick-jolla-home-qt5/main/Desktop.qml index 7c130501..0051c025 100644 --- a/usr/share/lipstick-jolla-home-qt5/main/Desktop.qml +++ b/usr/share/lipstick-jolla-home-qt5/main/Desktop.qml @@ -14,8 +14,9 @@ import Sailfish.Silica.Background 1.0 import Sailfish.Telephony 1.0 import Nemo.DBus 2.0 import Nemo.FileManager 1.0 -import org.nemomobile.configuration 1.0 +import Nemo.Configuration 1.0 import org.nemomobile.devicelock 1.0 +import org.nemomobile.systemsettings 1.0 import com.jolla.lipstick 0.1 QtObject { @@ -67,7 +68,7 @@ QtObject { property var startupWizardDoneWatcher: FileWatcher { Component.onCompleted: { - var markerFile = StandardPaths.home + "/.jolla-startupwizard-usersession-done" + var markerFile = StandardPaths.home + "/.config/jolla-startupwizard-usersession-done" if (!testFileExists(markerFile)) { fileName = markerFile } @@ -105,6 +106,10 @@ QtObject { Component.onCompleted: getDeviceLockState() } + property QtObject deviceInfo: DeviceInfo { + readonly property bool hasCellularVoiceCallFeature: hasFeature(DeviceInfo.FeatureCellularVoice) + } + property QtObject pendingWindowPrompt: ConfigurationValue { key: "/desktop/lipstick-jolla-home/windowprompt/pending" defaultValue: [] @@ -112,9 +117,19 @@ QtObject { property bool windowPromptPending: pendingWindowPrompt.value.length > 0 + // allow forcing the loading while we have it otherwise disabled + property QtObject forceWeather: ConfigurationValue { + key: "/desktop/lipstick-jolla-home/force_weather_loading" + defaultValue: false + } + property bool weatherAvailable + // some file of the app that ensures it's installed + readonly property string weatherAppFile: StandardPaths.qmlImportPath + "org/sailfishos/weather/settings/qmldir" function refreshWeatherAvailable() { - weatherAvailable = fileUtils.exists(StandardPaths.resolveImport("Sailfish.Weather.WeatherIndicator")) + // hide weather due to foreca not working + //weatherAvailable = fileUtils.exists(weatherAppFile) + weatherAvailable = forceWeather.value } property FileUtils fileUtils: FileUtils { } diff --git a/usr/share/lipstick-jolla-home-qt5/main/EditableGridDelegate.qml b/usr/share/lipstick-jolla-home-qt5/main/EditableGridDelegate.qml index fa2b223d..b9dceded 100644 --- a/usr/share/lipstick-jolla-home-qt5/main/EditableGridDelegate.qml +++ b/usr/share/lipstick-jolla-home-qt5/main/EditableGridDelegate.qml @@ -5,7 +5,7 @@ * License: Proprietary */ -import QtQuick 2.1 +import QtQuick 2.6 import org.nemomobile.lipstick 0.1 import Sailfish.Silica 1.0 import Sailfish.Silica.private 1.0 @@ -29,7 +29,7 @@ Item { property bool dragged property bool animateMovement: true - property real offsetY: y + property real targetY: y property alias contentItem: delegateContentItem default property alias _content: delegateContentItem.data @@ -95,7 +95,7 @@ Item { if (_viewWidth !== manager.view.width) { // Don't animate when created or view is resized delegateContentItem.x = wrapper.x - delegateContentItem.y = wrapper.offsetY + delegateContentItem.y = wrapper.targetY _viewWidth = manager.view.width _oldY = y } else if (!reordering) { @@ -169,7 +169,7 @@ Item { } if (item && item !== wrapper && item.y !== wrapper.y) { - var yDist = item.y - offsetY + var yDist = item.y - targetY if (Math.abs(yDist) <= item.height - height/2) { // Our items have differing heights and we don't have enough overlap yet return @@ -188,12 +188,12 @@ Item { var folderThreshold = manager.supportsFolders && !isFolder ? item.width / 4 : item.width / 2 if (offset < folderThreshold) { - if (Math.abs(index - item.modelIndex) > 1 || index > item.modelIndex || item.y !== wrapper.offsetY) { + if (Math.abs(index - item.modelIndex) > 1 || index > item.modelIndex || item.y !== wrapper.targetY) { idx = index < item.modelIndex ? item.modelIndex - 1 : item.modelIndex manager.folderItem = null } } else if (offset >= item.width - folderThreshold) { - if (Math.abs(index - item.modelIndex) > 1 || index < item.modelIndex || item.y !== wrapper.offsetY) { + if (Math.abs(index - item.modelIndex) > 1 || index < item.modelIndex || item.y !== wrapper.targetY) { idx = index > item.modelIndex ? item.modelIndex + 1 : item.modelIndex manager.folderItem = null } @@ -242,7 +242,7 @@ Item { objectName: "EditableGridDelegate_contentItem" x: wrapper.x - y: wrapper.offsetY + y: wrapper.targetY width: wrapper.width height: wrapper.height parent: manager.contentContainer @@ -306,7 +306,7 @@ Item { moveTimer.running = true manager.reorderTimer.stop() } - onOffsetYChanged: { + onTargetYChanged: { moveTimer.running = true } } @@ -319,25 +319,25 @@ Item { ParallelAnimation { id: slideMoveAnim NumberAnimation { target: delegateContentItem; property: "x"; to: wrapper.x; duration: 150; easing.type: Easing.InOutQuad } - NumberAnimation { target: delegateContentItem; property: "y"; to: wrapper.offsetY; duration: 150; easing.type: Easing.InOutQuad } + NumberAnimation { target: delegateContentItem; property: "y"; to: wrapper.targetY; duration: 150; easing.type: Easing.InOutQuad } onStopped: { // This is a safeguard. If the animation is canceled make sure the icon is left in // the correct state. delegateContentItem.x = wrapper.x - delegateContentItem.y = wrapper.offsetY + delegateContentItem.y = wrapper.targetY } } SequentialAnimation { id: fadeMoveAnim NumberAnimation { target: delegateContentItem; property: "opacity"; to: 0; duration: 75 } - ScriptAction { script: { delegateContentItem.x = wrapper.x; delegateContentItem.y = wrapper.offsetY } } + ScriptAction { script: { delegateContentItem.x = wrapper.x; delegateContentItem.y = wrapper.targetY } } NumberAnimation { target: delegateContentItem; property: "opacity"; to: 1.0; duration: 75 } onStopped: { // This is a safeguard. If the animation is canceled make sure the icon is left in // the correct state. delegateContentItem.x = wrapper.x - delegateContentItem.y = wrapper.offsetY + delegateContentItem.y = wrapper.targetY delegateContentItem.opacity = 1.0 } } diff --git a/usr/share/lipstick-jolla-home-qt5/main/EditableGridManager.qml b/usr/share/lipstick-jolla-home-qt5/main/EditableGridManager.qml index b929ad83..f2e35cba 100644 --- a/usr/share/lipstick-jolla-home-qt5/main/EditableGridManager.qml +++ b/usr/share/lipstick-jolla-home-qt5/main/EditableGridManager.qml @@ -5,7 +5,7 @@ ** ****************************************************************************/ -import QtQuick 2.1 +import QtQuick 2.6 QtObject { property Item view diff --git a/usr/share/lipstick-jolla-home-qt5/main/OrientationTransition.qml b/usr/share/lipstick-jolla-home-qt5/main/OrientationTransition.qml index 4c2620bc..4038e7b3 100644 --- a/usr/share/lipstick-jolla-home-qt5/main/OrientationTransition.qml +++ b/usr/share/lipstick-jolla-home-qt5/main/OrientationTransition.qml @@ -1,4 +1,4 @@ -import QtQuick 2.2 +import QtQuick 2.6 import Sailfish.Silica 1.0 Transition { diff --git a/usr/share/lipstick-jolla-home-qt5/main/PeekArea.qml b/usr/share/lipstick-jolla-home-qt5/main/PeekArea.qml index 3372b9c0..b7de792f 100644 --- a/usr/share/lipstick-jolla-home-qt5/main/PeekArea.qml +++ b/usr/share/lipstick-jolla-home-qt5/main/PeekArea.qml @@ -5,7 +5,7 @@ * License: Proprietary */ -import QtQuick 2.0 +import QtQuick 2.6 import Sailfish.Silica 1.0 import Sailfish.Lipstick 1.0 import org.nemomobile.lipstick 0.1 @@ -201,6 +201,7 @@ Item { ParallelAnimation { id: resetAnimation + running: false NumberAnimation { @@ -219,6 +220,7 @@ Item { FadeAnimation { id: fadeOut + target: peekArea duration: clipEndAnimation.duration to: peekArea.closing ? 0 : 1 diff --git a/usr/share/lipstick-jolla-home-qt5/main/PeekAreaFilter.qml b/usr/share/lipstick-jolla-home-qt5/main/PeekAreaFilter.qml index e9908bcc..dcd25983 100644 --- a/usr/share/lipstick-jolla-home-qt5/main/PeekAreaFilter.qml +++ b/usr/share/lipstick-jolla-home-qt5/main/PeekAreaFilter.qml @@ -1,4 +1,4 @@ -import QtQuick 2.0 +import QtQuick 2.6 import Sailfish.Lipstick 1.0 PeekFilter { diff --git a/usr/share/lipstick-jolla-home-qt5/main/WallpaperPath.qml b/usr/share/lipstick-jolla-home-qt5/main/WallpaperPath.qml index 25802d82..541eca74 100644 --- a/usr/share/lipstick-jolla-home-qt5/main/WallpaperPath.qml +++ b/usr/share/lipstick-jolla-home-qt5/main/WallpaperPath.qml @@ -1,4 +1,4 @@ -import QtQuick 2.0 +import QtQuick 2.6 QtObject { id: wallpaperPath diff --git a/usr/share/lipstick-jolla-home-qt5/nfc/NfcDialog.qml b/usr/share/lipstick-jolla-home-qt5/nfc/NfcDialog.qml index 3f5ec93d..edb60e77 100644 --- a/usr/share/lipstick-jolla-home-qt5/nfc/NfcDialog.qml +++ b/usr/share/lipstick-jolla-home-qt5/nfc/NfcDialog.qml @@ -65,7 +65,7 @@ SystemWindow { //% "NFC tag detected" title: qsTrId("lipstick-jolla-home-nfc_tag_detected") - topPadding: transpose ? Theme.paddingLarge : 2*Theme.paddingLarge + semiTight: true } Label { diff --git a/usr/share/lipstick-jolla-home-qt5/notifications/AmbiencePreview.qml b/usr/share/lipstick-jolla-home-qt5/notifications/AmbiencePreview.qml deleted file mode 100644 index fdbace83..00000000 --- a/usr/share/lipstick-jolla-home-qt5/notifications/AmbiencePreview.qml +++ /dev/null @@ -1,118 +0,0 @@ -import QtQuick 2.0 -import Sailfish.Silica 1.0 -import Sailfish.Ambience 1.0 -import org.nemomobile.lipstick 0.1 - -Item { - id: ambiencePreview - property alias displayName: displayNameLabel.text - property alias coverImage: image.source - - signal finished - - width: 4 * Theme.itemSizeExtraLarge - height: Screen.sizeCategory >= Screen.Large - ? Theme.itemSizeExtraLarge + (2 * Theme.paddingLarge) - : Screen.height / 5 - visible: peekAnimation.running - y: -height - - property bool _pending - property bool _topMenuExposed: Lipstick.compositor.topMenuLayer.exposed - - function show() { - _pending = false - if (image.status === Image.Ready) { - peekAnimation.restart() - } else if (image.status === Image.Loading) { - _pending = true - } else { - finished() - } - } - - on_TopMenuExposedChanged: { - if (peekAnimation.running && _topMenuExposed) { - peekAnimation.stop() - } - } - - SequentialAnimation { - id: peekAnimation - alwaysRunToEnd: true - - NumberAnimation { - target: ambiencePreview - property: "y" - from: -height - to: 0 - duration: 300 - easing.type: Easing.OutQuad - } - PauseAnimation { - duration: 2000 - } - NumberAnimation { - target: ambiencePreview - property: "y" - from: 0 - to: -height - duration: 300 - easing.type: Easing.InQuad - } - ScriptAction { - script: finished() - } - } - - Image { - id: image - anchors.fill: parent - clip: true - fillMode: Image.PreserveAspectCrop - sourceSize { width: width; height: height } - smooth: true - asynchronous: true - - onStatusChanged: { - if (_pending) { - if (status === Image.Ready) { - _pending = false - peekAnimation.restart() - } else if (status === Image.Error || status === Image.Null) { - _pending = false - finished() - } - } - } - - Rectangle { - anchors.fill: parent - color: Theme.rgba(Theme.highlightDimmerColor, Theme.opacityHigh) - } - - BusyIndicator { - anchors.centerIn: parent - size: BusyIndicatorSize.Medium - running: true - } - } - - Label { - id: displayNameLabel - anchors { - left: parent.left - leftMargin: Theme.paddingLarge - right: parent.right - rightMargin: Theme.paddingLarge - bottom: parent.bottom - bottomMargin: Theme.paddingMedium - } - font.pixelSize: Theme.fontSizeLarge - horizontalAlignment: Text.AlignLeft - wrapMode: Text.Wrap - maximumLineCount: 2 - truncationMode: TruncationMode.Elide - color: Theme.highlightColor - } -} diff --git a/usr/share/lipstick-jolla-home-qt5/notifications/NotificationActionRow.qml b/usr/share/lipstick-jolla-home-qt5/notifications/NotificationActionRow.qml index cd7d96fe..c30c2bb0 100644 --- a/usr/share/lipstick-jolla-home-qt5/notifications/NotificationActionRow.qml +++ b/usr/share/lipstick-jolla-home-qt5/notifications/NotificationActionRow.qml @@ -6,13 +6,16 @@ import QtQuick 2.6 import Sailfish.Silica 1.0 +import org.nemomobile.lipstick 0.1 -Grid { +Item { id: root + property QtObject notification property bool active: true property alias count: repeater.count property alias animating: heightAnimation.running + property int textAreaMaxHeight: Screen.height property int visibleCount: { var count = 0 if (notification) { @@ -27,33 +30,52 @@ Grid { return count } - signal actionInvoked(string actionName) + signal actionInvoked(string actionName, string actionText) - property bool _active: active && visibleCount > 0 - property int _totalWidth - property int _currentMaxButtonWidth: Theme.buttonWidthExtraSmall + property bool replyActivationPending + property bool replyTextActive + onReplyTextActiveChanged: { + if (replyTextActive) { + replyTextLoader.active = true + } + } - on_ActiveChanged: if (_active) repeater.model = notification.remoteActions - Component.onCompleted: if (_active) repeater.model = notification.remoteActions + property string currentTextActionName + property var currentTextAction: { + if (notification && currentTextActionName != "") { + var actions = notification.remoteActions + for (var i = 0; i < actions.length; i++) { + var action = actions[i] + if (action.name === currentTextActionName) { + return action + } + } + } + return null + } + + onActiveChanged: { + if (!active) { + replyTextActive = false + } + } + + onNotificationChanged: { + if (buttonGrid._active && notification) { + repeater.model = notification.remoteActions + } + } - function calculateTotalWidth() { - var maxButtonWidth = 0 - var width = 0 - for (var i = 0; i < count; i++) { - var button = repeater.itemAt(i) - if (button && button.text.length > 0) { - width = width + button.implicitWidth + spacing - maxButtonWidth = Math.max(maxButtonWidth, button.implicitWidth) + Connections { + target: notification + onRemoteActionsChanged: { + if (buttonGrid._active) { + repeater.model = notification.remoteActions } } - _currentMaxButtonWidth = maxButtonWidth - _totalWidth = width } - spacing: Theme.paddingMedium - horizontalItemAlignment: Grid.AlignRight - columns: _totalWidth > parent.width ? 1 : visibleCount - height: _active ? implicitHeight : 0 + height: replyTextActive ? replyTextLoader.height : buttonGrid.height Behavior on height { NumberAnimation { id: heightAnimation @@ -62,29 +84,149 @@ Grid { } } - opacity: _active ? 1.0 : 0.0 - Behavior on opacity { FadeAnimator {}} - enabled: _active + function reset() { + currentTextActionName = "" + replyTextActive = false + } - Repeater { - id: repeater + Grid { + id: buttonGrid - delegate: SecondaryButton { - preferredWidth: Theme.buttonWidthExtraSmall - text: modelData.name !== "default" && modelData.name !== "app" - ? (modelData.displayName || "") - : "" - visible: text.length > 0 - onImplicitWidthChanged: calculateTotalWidth() + property bool _active: notification && active && visibleCount > 0 && !root.replyTextActive + property int _totalWidth + property int _currentMaxButtonWidth: Theme.buttonWidthExtraSmall - width: columns === 1 ? _currentMaxButtonWidth : implicitWidth + on_ActiveChanged: if (_active) repeater.model = notification.remoteActions + Component.onCompleted: if (_active) repeater.model = notification.remoteActions - onClicked: root.actionInvoked(modelData.name) + function calculateTotalWidth() { + var maxButtonWidth = 0 + var width = 0 + for (var i = 0; i < count; i++) { + var button = repeater.itemAt(i) + if (button && button.text.length > 0) { + width = width + button.implicitWidth + spacing + maxButtonWidth = Math.max(maxButtonWidth, button.implicitWidth) + } + } + _currentMaxButtonWidth = maxButtonWidth + _totalWidth = width + } + + spacing: Theme.paddingMedium + anchors.right: parent.right + columns: _totalWidth > parent.width ? 1 : visibleCount + height: _active ? implicitHeight : 0 + + opacity: _active ? 1.0 : 0.0 + Behavior on opacity { FadeAnimator {}} + enabled: _active + Repeater { + id: repeater + + delegate: SecondaryButton { + // on phone sized displays, try to squeeze in multiple buttons to fit on one line + preferredWidth: (root.visibleCount > 2 && Screen.sizeCategory < Screen.Large) + ? Theme.buttonWidthTiny : Theme.buttonWidthExtraSmall + text: modelData.name !== "default" && modelData.name !== "app" + ? (modelData.displayName || "") + : "" + visible: text.length > 0 + onImplicitWidthChanged: buttonGrid.calculateTotalWidth() + + width: buttonGrid.columns === 1 ? buttonGrid._currentMaxButtonWidth : implicitWidth + + onClicked: { + if (modelData.type === "input") { + root.currentTextActionName = modelData.name + if (Lipstick.compositor.lockScreenLayer.lockScreenEventsEnabled) { + root.replyActivationPending = true + Lipstick.compositor.unlock() + } else { + root.replyTextActive = true + } + } else { + root.actionInvoked(modelData.name, "") + } + } + } } } Connections { - target: notification - onRemoteActionsChanged: if (_active) repeater.model = notification.remoteActions + target: Lipstick.compositor.lockScreenLayer + onDeviceIsLockedChanged: { + if (root.replyActivationPending && !Lipstick.compositor.lockScreenLayer.deviceIsLocked) { + root.replyTextActive = true + root.replyActivationPending = false + } + } + onShowingLockCodeEntryChanged: { + if (!Lipstick.compositor.lockScreenLayer.showingLockCodeEntry) { + root.replyActivationPending = false + } + } + } + + Loader { + id: replyTextLoader + + active: false + sourceComponent: replyTextComponent + } + + Component { + id: replyTextComponent + + Item { + opacity: root.replyTextActive ? 1.0 : 0.0 + Behavior on opacity { FadeAnimation {} } + visible: opacity > 0 + width: root.width + height: Math.max(actionTextArea.height, actionTextEnter.height + 2*Theme.paddingSmall) + + TextArea { + id: actionTextArea + + textLeftMargin: 0 + textRightMargin: Theme.paddingMedium + height: Math.min(implicitHeight, Theme.itemSizeLarge*2, root.textAreaMaxHeight) + anchors.left: parent.left + anchors.right: actionTextEnter.left + placeholderText: root.currentTextAction ? root.currentTextAction.displayName : "" + labelVisible: false // the placeholder is enough + // avoid implicit inverted color on the keyboard side just because a popup might have such + _keyboardPalette: "" + _appWindow: undefined // suppress warnings + + Component.onCompleted: forceActiveFocus() + } + + Button { + id: actionTextEnter + + height: implicitHeight + 2*Theme.paddingSmall + enabled: actionTextArea.text != "" + icon.source: "image://theme/icon-m-send" + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.bottomMargin: Theme.paddingSmall + + onClicked: { + root.actionInvoked(root.currentTextActionName, actionTextArea.text) + actionTextArea.text = "" + } + } + + Connections { + target: root + onCurrentTextActionNameChanged: actionTextArea.text = "" + onReplyTextActiveChanged: { + if (root.replyTextActive) { + actionTextArea.forceActiveFocus() + } + } + } + } } } diff --git a/usr/share/lipstick-jolla-home-qt5/notifications/NotificationColumn.qml b/usr/share/lipstick-jolla-home-qt5/notifications/NotificationColumn.qml index 331cf3df..eddc82c5 100644 --- a/usr/share/lipstick-jolla-home-qt5/notifications/NotificationColumn.qml +++ b/usr/share/lipstick-jolla-home-qt5/notifications/NotificationColumn.qml @@ -5,7 +5,7 @@ ** ****************************************************************************/ -import QtQuick 2.2 +import QtQuick 2.6 import Sailfish.Silica 1.0 import Sailfish.Lipstick 1.0 import com.jolla.lipstick 0.1 @@ -16,6 +16,7 @@ import "../main" Column { id: root property bool showCount + property alias maximumCount: boundedModel.maximumCount readonly property bool hasNotifications: repeater.count > 0 diff --git a/usr/share/lipstick-jolla-home-qt5/notifications/NotificationListView.qml b/usr/share/lipstick-jolla-home-qt5/notifications/NotificationListView.qml index e62b352e..20c05a48 100644 --- a/usr/share/lipstick-jolla-home-qt5/notifications/NotificationListView.qml +++ b/usr/share/lipstick-jolla-home-qt5/notifications/NotificationListView.qml @@ -5,7 +5,7 @@ ** ****************************************************************************/ -import QtQuick 2.5 +import QtQuick 2.6 import org.nemomobile.lipstick 0.1 import Sailfish.Silica 1.0 import Sailfish.Lipstick 1.0 diff --git a/usr/share/lipstick-jolla-home-qt5/notifications/NotificationPreview.qml b/usr/share/lipstick-jolla-home-qt5/notifications/NotificationPreview.qml index 3193cd2d..8b122752 100644 --- a/usr/share/lipstick-jolla-home-qt5/notifications/NotificationPreview.qml +++ b/usr/share/lipstick-jolla-home-qt5/notifications/NotificationPreview.qml @@ -18,10 +18,11 @@ import "../systemwindow" SystemWindow { id: notificationWindow + property bool active property QtObject notification: notificationPreviewPresenter.notification property bool showNotification: notification != null && (notification.previewBody || notification.previewSummary) - property string summaryText: showNotification ? notification.previewSummary : "" - property string bodyText: showNotification ? notification.previewBody : "" + property string summaryText: (showNotification && notification) ? notification.previewSummary : "" + property string bodyText: (showNotification && notification) ? notification.previewBody : "" // we didn't earlier use app name on the popup so there can be transient notification that have only inferred // name set. As that's not always correct, showing transient notification name only if it's explicitly set. property string appNameText: notification != null ? (notification.isTransient ? notification.explicitAppName @@ -35,7 +36,17 @@ SystemWindow { property bool _invoked property string pendingAction + property string pendingActionText property QtObject pendingNotification + property int _availableHeight: notificationWindow.height + - (notificationWindow.transpose + ? Qt.inputMethod.keyboardRectangle.width + : Qt.inputMethod.keyboardRectangle.height) + property int _biggestCorner: Math.max(Screen.topLeftCorner.radius, + Screen.topRightCorner.radius, + Screen.bottomLeftCorner.radius, + Screen.bottomRightCorner.radius) + Binding { // Invocation typically closes the notification, so bind the current values @@ -89,13 +100,14 @@ SystemWindow { return -1 } - function _triggerAction(actionName) { + function _triggerAction(actionName, actionText) { if (Desktop.deviceLockState !== DeviceLock.Unlocked) { pendingAction = actionName + pendingActionText = actionText ? actionText : "" pendingNotification = notification } else { notificationWindow._invoked = true - notification.actionInvoked(actionName) + notification.actionInvoked(actionName, actionText) } // Always hide the notification preview after it is tapped @@ -105,10 +117,10 @@ SystemWindow { Lipstick.compositor.unlock() } - onVisibleChanged: if (!visible) popupArea.expanded = false + onActiveChanged: if (!active) popupArea.expanded = false opacity: 0 - visible: false + visible: active && !actionRow.replyActivationPending InverseMouseArea { id: outsideArea @@ -116,7 +128,12 @@ SystemWindow { anchors.fill: popupArea enabled: false - onPressedOutside: if (popupArea.expanded) notificationWindow.notificationExpired() + onPressedOutside: { + if (popupArea.expanded) { + notificationFeedbackPlayer.removeNotification(notification.id) + notificationWindow.notificationExpired() + } + } } Binding { @@ -130,13 +147,15 @@ SystemWindow { onDeviceIsLockedChanged: { if (pendingAction.length > 0 && !Lipstick.compositor.lockScreenLayer.deviceIsLocked) { notificationWindow._invoked = true - pendingNotification.actionInvoked(pendingAction) + pendingNotification.actionInvoked(pendingAction, pendingActionText) pendingAction = "" + pendingActionText = "" } } onShowingLockCodeEntryChanged: { if (!Lipstick.compositor.lockScreenLayer.showingLockCodeEntry) { pendingAction = "" + pendingActionText = "" } } } @@ -149,7 +168,18 @@ SystemWindow { Private.SwipeItem { id: popupArea - readonly property int baseX: Theme.paddingSmall + readonly property int baseX: { + // handle top notch separately + if (Lipstick.compositor.topmostWindowOrientation === Qt.PortraitOrientation + && Screen.topCutout.height > Theme.paddingLarge) { + // assuming pushed down enough to avoid corners + return Theme.paddingSmall + } + + return Math.max(_biggestCorner, Theme.paddingSmall, + Lipstick.compositor.topmostWindowOrientation === Qt.LandscapeOrientation + ? Screen.topCutout.height : 0) + } property bool expanded property real textOpacity: 0 @@ -159,13 +189,23 @@ SystemWindow { onSwipedAway: { notificationWindow.state = "" + notificationFeedbackPlayer.removeNotification(notification.id) notificationWindow.notificationExpired() } objectName: "NotificationPreview_popupArea" _showPress: false - y: Theme.paddingMedium + y: { + if (Qt.inputMethod.visible && popupArea.height > notificationWindow._availableHeight) { + // simple way to make sure the text area visible above the keyboard + return -actionRow.y + (notificationWindow._availableHeight - actionRow.height) + } + + return Theme.paddingMedium + + (Screen.hasCutouts && Lipstick.compositor.topmostWindowOrientation === Qt.PortraitOrientation + ? Screen.topCutout.height : 0) + } width: displayWidth swipeDistance: notificationWindow.width height: expanded @@ -231,10 +271,12 @@ SystemWindow { right: parent.right verticalCenter: popupPreviewScrollContainer.verticalCenter } - visible: !popupArea.expanded + visible: !popupArea.expanded || actionRow.replyTextActive source: "image://theme/icon-m-change-type" highlighted: dropDownMouseArea.containsMouse color: palette.primaryColor + transformOrigin: Item.Center + rotation: actionRow.replyTextActive ? 180 : 0 } MouseArea { @@ -245,9 +287,13 @@ SystemWindow { enabled: dropDownArrow.visible onClicked: { - scrollAnimation.reset() - popupArea.expanded = true - outsideArea.enabled = true + if (actionRow.replyTextActive) { + actionRow.replyTextActive = false + } else { + scrollAnimation.reset() + popupArea.expanded = true + outsideArea.enabled = true + } } } @@ -386,15 +432,19 @@ SystemWindow { NotificationActionRow { id: actionRow - onActionInvoked: _triggerAction(actionName) + onActionInvoked: _triggerAction(actionName, actionText) + notification: notificationWindow.notification active: !notificationWindow._invoked + textAreaMaxHeight: notificationWindow._availableHeight / 2 anchors { top: notificationIcon.loaded && notificationIcon.height > popupExpandedText.height ? notificationIcon.bottom : popupExpandedText.bottom topMargin: Theme.paddingMedium right: parent.right rightMargin: Theme.paddingMedium + left: parent.left + leftMargin: Theme.paddingMedium } } } @@ -409,7 +459,10 @@ SystemWindow { height: Lipstick.compositor.homeLayer.statusBar.height y: -height - onClicked: notificationWindow.notificationExpired() + onClicked: { + notificationFeedbackPlayer.removeNotification(notification.id) + notificationWindow.notificationExpired() + } Rectangle { anchors.fill: parent @@ -422,17 +475,30 @@ SystemWindow { anchors { verticalCenter: bannerArea.verticalCenter + verticalCenterOffset: Screen.hasCutouts + && Lipstick.compositor.topmostWindowOrientation === Qt.PortraitOrientation + ? Screen.topCutout.height / 2 : 0 left: bannerArea.left - leftMargin: Theme.horizontalPageMargin + leftMargin: { + if (Lipstick.compositor.topmostWindowOrientation === Qt.PortraitOrientation + && Screen.topCutout.height > Theme.paddingLarge) { + return Theme.horizontalPageMargin + } + + return Math.max(_biggestCorner, Theme.horizontalPageMargin) + } } // only one image shown so using fallbacks. image-path should be better than no image at all. source: { // don't use guessed appIcon on transient notifications, similar to appNameText - if (notificationWindow.appIconUrl != "" && (!notification.isTransient - || notification.appIconOrigin != Notification.InferredValue)) { - return Notifications.iconSource(notificationWindow.appIconUrl) - } else if (notification && notification.hints["image-path"] != "") { - return Notifications.iconSource(notification.hints["image-path"]) + if (notification) { + if (notificationWindow.appIconUrl != "" + && (!notification.isTransient + || notification.appIconOrigin != Notification.InferredValue)) { + return Notifications.iconSource(notificationWindow.appIconUrl) + } else if (notification.hints["image-path"] != "") { + return Notifications.iconSource(notification.hints["image-path"]) + } } return "" } @@ -462,34 +528,24 @@ SystemWindow { } } - Loader { - id: ambiencePreviewLoader - } - - Component { - id: ambiencePreviewComponent - AmbiencePreview { - onFinished: notificationWindow.notificationComplete() - } - } + Timer { + id: displayTimer - Binding { - target: notificationFeedbackPlayer - property: "minimumPriority" - value: lipstickSettings.lockscreenVisible ? 100 : 0 + interval: 0 + onTriggered: displayNotification() } Timer { - id: displayTimer + id: delayedShowNext + interval: 0 - repeat: false - onTriggered: displayNotification() + onTriggered: notificationPreviewPresenter.showNextNotification() } Timer { id: forceHideTimer + interval: 7000 - repeat: false onTriggered: { notificationTimer.duration = 3000 notificationTimer.start() @@ -539,38 +595,11 @@ SystemWindow { onAppIconUrlChanged: refreshPeriod() function displayNotification() { - // We use two different presentation styles: one that can be clicked and one that cannot. - // Check for configurations that can't be correctly activated - if (notification.remoteActions.length == 0) { - if (notification.previewSummary && notification.previewBody) { - // Notifications with preview summary + preview body should have actions, as tapping on the preview pop-up should trigger some action - console.log('Warning: Notification has both preview summary and preview body but no actions. Remove the preview body or add an action:', notification.appName, notification.category, notification.previewSummary, notification.previewBody) - } - } else { - if (notification.previewSummary && !notification.previewBody) { - // Notifications with preview summary but no body should not have any actions, as the small preview banner is too small to receive presses - console.log('Warning: Notification has an action but only shows a preview summary. Add a preview body or remove the actions:', notification.appName, notification.category, notification.previewSummary, notification.previewBody) - } else if ((!notification.previewSummary && !notification.previewBody) && notification.hints['transient'] == true) { - console.log('Warning: Notification has actions but is transient and without a preview, its actions will not be triggerable from the UI:', notification.appName, notification.category, notification.previewSummary, notification.previewBody) - } - } - if (showNotification) { - if (notification.category === "x-jolla.ambience.preview") { - ambiencePreviewLoader.sourceComponent = ambiencePreviewComponent - var preview = ambiencePreviewLoader.item - if (preview) { - preview.displayName = notification.previewSummary - preview.coverImage = notification.previewBody - preview.show() - state = "showAmbience" - } - } else { - var actions = notification.remoteActions - // Show preview banner or pop-up - var hasMultipleLines = (notification.previewSummary.length > 0 && notification.previewBody.length > 0) - state = actions.length > 0 || hasMultipleLines ? "showPopup" : "showBanner" - } + var actions = notification.remoteActions + // Show preview banner or pop-up + var hasMultipleLines = (notification.previewSummary.length > 0 && notification.previewBody.length > 0) + state = actions.length > 0 || hasMultipleLines ? "showPopup" : "showBanner" } } @@ -614,7 +643,9 @@ SystemWindow { function notificationComplete() { state = "" _invoked = false - notificationPreviewPresenter.showNextNotification() + actionRow.reset() + // avoid binding loop in case notification is completed from notification change handler + delayedShowNext.start() } states: [ @@ -623,7 +654,7 @@ SystemWindow { PropertyChanges { target: notificationWindow opacity: 1 - visible: true + active: true } PropertyChanges { target: popupArea @@ -644,7 +675,7 @@ SystemWindow { PropertyChanges { target: notificationWindow opacity: 1 - visible: true + active: true } PropertyChanges { target: popupPreviewScroll @@ -657,7 +688,7 @@ SystemWindow { PropertyChanges { target: notificationWindow opacity: 1 - visible: true + active: true } PropertyChanges { target: bannerArea @@ -671,7 +702,7 @@ SystemWindow { PropertyChanges { target: notificationWindow opacity: 1 - visible: true + active: true } PropertyChanges { target: bannerArea @@ -679,14 +710,6 @@ SystemWindow { x: bannerArea.x contentOpacity: 1 } - }, - State { - name: "showAmbience" - PropertyChanges { - target: notificationWindow - opacity: 1 - visible: true - } } ] diff --git a/usr/share/lipstick-jolla-home-qt5/notifications/NotificationStandardGroupItem.qml b/usr/share/lipstick-jolla-home-qt5/notifications/NotificationStandardGroupItem.qml index a79e3b4a..61026452 100644 --- a/usr/share/lipstick-jolla-home-qt5/notifications/NotificationStandardGroupItem.qml +++ b/usr/share/lipstick-jolla-home-qt5/notifications/NotificationStandardGroupItem.qml @@ -134,7 +134,7 @@ NotificationGroupItem { contentLeftMargin: groupHeader.textLeftMargin summaryText: notification ? notification.summary : "" bodyText: notification ? notification.body : "" - timestampText: notification ? (root._timestampCounter, Format.formatDate(notification.timestamp, Formatter.DurationElapsedShort)) : "" + timestampText: notification ? (root._timestampCounter, Format.formatDate(notification.timestamp, Formatter.TimeElapsedShort)) : "" animateContentResizing: boundedNotificationModel.updating animateAddition: defaultAnimateAddition diff --git a/usr/share/lipstick-jolla-home-qt5/notifications/NotificationStandardGroupMember.qml b/usr/share/lipstick-jolla-home-qt5/notifications/NotificationStandardGroupMember.qml index 5d4dee3d..174c49e5 100644 --- a/usr/share/lipstick-jolla-home-qt5/notifications/NotificationStandardGroupMember.qml +++ b/usr/share/lipstick-jolla-home-qt5/notifications/NotificationStandardGroupMember.qml @@ -143,6 +143,7 @@ NotificationGroupMember { onClicked: { if (expanded) { expanded = false + actionRow.replyTextActive = false } else { expand() } @@ -222,9 +223,11 @@ NotificationGroupMember { NotificationActionRow { id: actionRow + + notification: root.notification active: expanded - onActionInvoked: notification.actionInvoked(actionName) - anchors.right: parent.right + onActionInvoked: notification.actionInvoked(actionName, actionText) + width: parent.width } } diff --git a/usr/share/lipstick-jolla-home-qt5/sim/PinQueryWindow.qml b/usr/share/lipstick-jolla-home-qt5/sim/PinQueryWindow.qml index 759d6492..fbbde00d 100644 --- a/usr/share/lipstick-jolla-home-qt5/sim/PinQueryWindow.qml +++ b/usr/share/lipstick-jolla-home-qt5/sim/PinQueryWindow.qml @@ -7,11 +7,11 @@ ** ****************************************************************************/ -import QtQuick 2.1 +import QtQuick 2.6 import Sailfish.Silica 1.0 import com.jolla.lipstick 0.1 import com.jolla.settings.system 1.0 -import MeeGo.QOfono 0.2 +import QOfono 0.2 import "../sim" import "../main" diff --git a/usr/share/lipstick-jolla-home-qt5/sim/SimPinWrapper.qml b/usr/share/lipstick-jolla-home-qt5/sim/SimPinWrapper.qml index 93310916..d04b712a 100644 --- a/usr/share/lipstick-jolla-home-qt5/sim/SimPinWrapper.qml +++ b/usr/share/lipstick-jolla-home-qt5/sim/SimPinWrapper.qml @@ -5,7 +5,7 @@ ** ****************************************************************************/ -import QtQuick 2.1 +import QtQuick 2.6 import Sailfish.Silica 1.0 FocusScope { diff --git a/usr/share/lipstick-jolla-home-qt5/sim/SimReboot.qml b/usr/share/lipstick-jolla-home-qt5/sim/SimReboot.qml index b8fcace9..f02fd9de 100644 --- a/usr/share/lipstick-jolla-home-qt5/sim/SimReboot.qml +++ b/usr/share/lipstick-jolla-home-qt5/sim/SimReboot.qml @@ -5,7 +5,7 @@ ** ****************************************************************************/ -import QtQuick 2.0 +import QtQuick 2.6 import org.nemomobile.lipstick 0.1 import Nemo.DBus 2.0 import Sailfish.Silica 1.0 @@ -53,14 +53,15 @@ SystemWindow { ? qsTrId("lipstick-jolla-home-la-sim_removal_restart") //% "Without restarting the device, SIM card might not work reliably." : qsTrId("lipstick-jolla-home-la-sim_reboot") - topPadding: transpose ? Theme.paddingLarge : 2*Theme.paddingLarge + semiTight: true } SystemDialogIconButton { anchors.horizontalCenter: parent.horizontalCenter width: Theme.itemSizeHuge*1.5 - iconSource: (Screen.sizeCategory >= Screen.Large) ? (isRemovalWarning ? "image://theme/icon-l-acknowledge" : "image://theme/icon-l-reboot") - : (isRemovalWarning ? "image://theme/icon-m-acknowledge" : "image://theme/icon-m-reboot") + iconSource: (Screen.sizeCategory >= Screen.Large) + ? (isRemovalWarning ? "image://theme/icon-l-acknowledge" : "image://theme/icon-l-reboot") + : (isRemovalWarning ? "image://theme/icon-m-acknowledge" : "image://theme/icon-m-reboot") text: isRemovalWarning //% "Got it" ? qsTrId("lipstick-jolla-home-bt-ok") diff --git a/usr/share/lipstick-jolla-home-qt5/statusarea/AlarmStatusIndicator.qml b/usr/share/lipstick-jolla-home-qt5/statusarea/AlarmStatusIndicator.qml index 3ceacfd9..36766b39 100644 --- a/usr/share/lipstick-jolla-home-qt5/statusarea/AlarmStatusIndicator.qml +++ b/usr/share/lipstick-jolla-home-qt5/statusarea/AlarmStatusIndicator.qml @@ -12,7 +12,7 @@ import com.jolla.lipstick 0.1 import Sailfish.Silica 1.0 Icon { - source: "image://theme/icon-status-alarm" + iconSuffix + source: "image://theme/icon-status-alarm" visible: Desktop.timedStatus.alarmPresent height: visible ? implicitHeight : 0 } diff --git a/usr/share/lipstick-jolla-home-qt5/statusarea/BluetoothStatusIndicator.qml b/usr/share/lipstick-jolla-home-qt5/statusarea/BluetoothStatusIndicator.qml index a61d1f80..4cb80ae1 100644 --- a/usr/share/lipstick-jolla-home-qt5/statusarea/BluetoothStatusIndicator.qml +++ b/usr/share/lipstick-jolla-home-qt5/statusarea/BluetoothStatusIndicator.qml @@ -5,12 +5,12 @@ ** ****************************************************************************/ -import QtQuick 2.0 +import QtQuick 2.6 import Sailfish.Silica 1.0 import Sailfish.Lipstick 1.0 Icon { - source: 'image://theme/icon-status-bluetooth' + (bluetooth.connected ? '-connected' : '') + iconSuffix + source: 'image://theme/icon-status-bluetooth' + (bluetooth.connected ? '-connected' : '') opacity: bluetooth.connected || bluetooth.enabled ? 1.0 : 0.0 Behavior on opacity { FadeAnimation {} } diff --git a/usr/share/lipstick-jolla-home-qt5/statusarea/CellularNetworkNameStatusIndicator.qml b/usr/share/lipstick-jolla-home-qt5/statusarea/CellularNetworkNameStatusIndicator.qml index 4dce46ca..5be384ba 100644 --- a/usr/share/lipstick-jolla-home-qt5/statusarea/CellularNetworkNameStatusIndicator.qml +++ b/usr/share/lipstick-jolla-home-qt5/statusarea/CellularNetworkNameStatusIndicator.qml @@ -7,9 +7,9 @@ ** ****************************************************************************/ -import QtQuick 2.0 +import QtQuick 2.6 import Sailfish.Silica 1.0 -import MeeGo.QOfono 0.2 +import QOfono 0.2 import com.jolla.lipstick 0.1 import org.nemomobile.ofono 1.0 diff --git a/usr/share/lipstick-jolla-home-qt5/statusarea/CellularNetworkTypeStatusIndicator.qml b/usr/share/lipstick-jolla-home-qt5/statusarea/CellularNetworkTypeStatusIndicator.qml index 8efce3e4..d1db98e3 100644 --- a/usr/share/lipstick-jolla-home-qt5/statusarea/CellularNetworkTypeStatusIndicator.qml +++ b/usr/share/lipstick-jolla-home-qt5/statusarea/CellularNetworkTypeStatusIndicator.qml @@ -7,13 +7,14 @@ ** ****************************************************************************/ -import QtQuick 2.0 -import MeeGo.QOfono 0.2 +import QtQuick 2.6 +import QOfono 0.2 import Sailfish.Silica 1.0 import com.jolla.lipstick 0.1 Row { id: cellularNetworkTypeStatusIndicator + property alias text: cellularNetworkTypeStatusIndicatorText.text property alias color: cellularNetworkTypeStatusIndicatorText.color @@ -25,10 +26,11 @@ Row { property string _cellularDataTechnology: ofonoNetworkRegistration.technologyValue visible: dataSimIndex >= 0 && width > 0 - spacing: Math.round(Theme.paddingSmall/6) + spacing: Math.round(Theme.paddingSmall / 6) OfonoNetworkRegistration { id: ofonoNetworkRegistration + modemPath: Desktop.simManager.defaultDataModem readonly property string statusValue: valid ? status : "invalid" readonly property string technologyValue: valid ? technology : "invalid" @@ -49,19 +51,18 @@ Row { horizontalAlignment: Text.AlignRight color: Theme.primaryColor text: { - var techToG = {gsm: "2", edge: "2.5", umts: "3", hspa: "3.5", lte: "4"} + var techToG = {gsm: "2", edge: "2.5", umts: "3", hspa: "3.5", lte: "4", nr: "5"} var onlineIds = {registered: true, roaming: true} return (fakeOperator === "" ? ((_cellularGPRSAttached && onlineIds[_cellularRegistrationStatus]) ? (techToG[_cellularDataTechnology] || "") : "") - : "3.5"); + : "3.5") } } Text { - id: cellularNetworkTypeStatusIndicatorGeneration anchors { baseline: cellularNetworkTypeStatusIndicatorText.baseline } diff --git a/usr/share/lipstick-jolla-home-qt5/statusarea/ConnectionStatusIndicator.qml b/usr/share/lipstick-jolla-home-qt5/statusarea/ConnectionStatusIndicator.qml index 1f3dd122..053da526 100644 --- a/usr/share/lipstick-jolla-home-qt5/statusarea/ConnectionStatusIndicator.qml +++ b/usr/share/lipstick-jolla-home-qt5/statusarea/ConnectionStatusIndicator.qml @@ -7,9 +7,9 @@ ** ****************************************************************************/ -import QtQuick 2.0 +import QtQuick 2.6 import Sailfish.Silica 1.0 -import MeeGo.Connman 0.2 +import Connman 0.2 import org.nemomobile.lipstick 0.1 import com.jolla.lipstick 0.1 @@ -104,11 +104,11 @@ Item { opacity: blinkIconTimer.primaryIconVisible ? 1 : 0 source: { if (wlanStatus.connected && _wlanIconId !== "") - return "image://theme/" + _wlanIconId + iconSuffix + return "image://theme/" + _wlanIconId else if (_cellularIconId !== "") - return "image://theme/" + _cellularIconId + mobileDataIconSuffix + return "image://theme/" + _cellularIconId else if (_wlanIconId !== "") - return "image://theme/" + _wlanIconId + iconSuffix + return "image://theme/" + _wlanIconId else return "" } @@ -121,9 +121,9 @@ Item { id: secondaryIcon source: { if (wlanStatus.connected && _cellularIconId !== "") - return "image://theme/" + _cellularIconId + mobileDataIconSuffix + return "image://theme/" + _cellularIconId else if (mobileDataStatus.connected && _wlanIconId !== "") - return "image://theme/" + _wlanIconId + iconSuffix + return "image://theme/" + _wlanIconId else return "" } @@ -134,7 +134,7 @@ Item { Icon { id: tetheringOverlay - source: "image://theme/icon-status-data-share" + mobileDataIconSuffix + source: "image://theme/icon-status-data-share" visible: tethering.enabled anchors.bottom: parent.bottom } diff --git a/usr/share/lipstick-jolla-home-qt5/statusarea/DoNotDisturbIndicator.qml b/usr/share/lipstick-jolla-home-qt5/statusarea/DoNotDisturbIndicator.qml index 7b576e0e..b997e71e 100644 --- a/usr/share/lipstick-jolla-home-qt5/statusarea/DoNotDisturbIndicator.qml +++ b/usr/share/lipstick-jolla-home-qt5/statusarea/DoNotDisturbIndicator.qml @@ -8,7 +8,7 @@ import QtQuick 2.6 import Sailfish.Silica 1.0 -import org.nemomobile.configuration 1.0 +import Nemo.Configuration 1.0 Icon { source: "image://theme/icon-status-do-not-disturb" diff --git a/usr/share/lipstick-jolla-home-qt5/statusarea/FlightModeStatus.qml b/usr/share/lipstick-jolla-home-qt5/statusarea/FlightModeStatus.qml index b828efd7..df286052 100644 --- a/usr/share/lipstick-jolla-home-qt5/statusarea/FlightModeStatus.qml +++ b/usr/share/lipstick-jolla-home-qt5/statusarea/FlightModeStatus.qml @@ -8,7 +8,7 @@ ****************************************************************************/ import QtQml 2.2 -import MeeGo.Connman 0.2 +import Connman 0.2 NetworkManager { id: flightMode diff --git a/usr/share/lipstick-jolla-home-qt5/statusarea/FlightModeStatusIndicator.qml b/usr/share/lipstick-jolla-home-qt5/statusarea/FlightModeStatusIndicator.qml index c13ffed3..59850460 100644 --- a/usr/share/lipstick-jolla-home-qt5/statusarea/FlightModeStatusIndicator.qml +++ b/usr/share/lipstick-jolla-home-qt5/statusarea/FlightModeStatusIndicator.qml @@ -7,12 +7,12 @@ ** ****************************************************************************/ -import QtQuick 2.0 +import QtQuick 2.6 import Sailfish.Silica 1.0 Icon { property alias offline: flightModeStatus.enabled - source: "image://theme/icon-status-airplane-mode" + iconSuffix + source: "image://theme/icon-status-airplane-mode" FlightModeStatus { id: flightModeStatus diff --git a/usr/share/lipstick-jolla-home-qt5/statusarea/LocationStatusIndicator.qml b/usr/share/lipstick-jolla-home-qt5/statusarea/LocationStatusIndicator.qml index 2c181691..f3ad097a 100644 --- a/usr/share/lipstick-jolla-home-qt5/statusarea/LocationStatusIndicator.qml +++ b/usr/share/lipstick-jolla-home-qt5/statusarea/LocationStatusIndicator.qml @@ -5,14 +5,14 @@ ** ****************************************************************************/ -import QtQuick 2.0 +import QtQuick 2.6 import Sailfish.Silica 1.0 import com.jolla.lipstick 0.1 Icon { property bool recentlyOnDisplay - source: "image://theme/icon-status-gps" + iconSuffix + source: "image://theme/icon-status-gps" LocationStatus { id: locationStatus diff --git a/usr/share/lipstick-jolla-home-qt5/statusarea/MobileDataStatus.qml b/usr/share/lipstick-jolla-home-qt5/statusarea/MobileDataStatus.qml index 58b4eab3..6a83b6b0 100644 --- a/usr/share/lipstick-jolla-home-qt5/statusarea/MobileDataStatus.qml +++ b/usr/share/lipstick-jolla-home-qt5/statusarea/MobileDataStatus.qml @@ -1,6 +1,6 @@ import QtQml 2.2 import com.jolla.lipstick 0.1 -import MeeGo.Connman 0.2 +import Connman 0.2 QtObject { id: mobileData diff --git a/usr/share/lipstick-jolla-home-qt5/statusarea/ProfileStatusIndicator.qml b/usr/share/lipstick-jolla-home-qt5/statusarea/ProfileStatusIndicator.qml index db8a628b..72f11498 100644 --- a/usr/share/lipstick-jolla-home-qt5/statusarea/ProfileStatusIndicator.qml +++ b/usr/share/lipstick-jolla-home-qt5/statusarea/ProfileStatusIndicator.qml @@ -5,14 +5,14 @@ ** ****************************************************************************/ -import QtQuick 2.0 +import QtQuick 2.6 import Sailfish.Silica 1.0 import org.nemomobile.systemsettings 1.0 Icon { id: profileStatusIndicator - source: profileControl.isSilent ? ("image://theme/icon-status-silent" + iconSuffix) + source: profileControl.isSilent ? "image://theme/icon-status-silent" : "" width: source != "" ? implicitWidth : 0 height: source != "" ? implicitHeight : 0 diff --git a/usr/share/lipstick-jolla-home-qt5/statusarea/StatusArea.qml b/usr/share/lipstick-jolla-home-qt5/statusarea/StatusArea.qml index 6580d714..f780bbb5 100644 --- a/usr/share/lipstick-jolla-home-qt5/statusarea/StatusArea.qml +++ b/usr/share/lipstick-jolla-home-qt5/statusarea/StatusArea.qml @@ -7,7 +7,7 @@ ** ****************************************************************************/ -import QtQuick 2.0 +import QtQuick 2.6 import Sailfish.Silica 1.0 import Sailfish.Lipstick 1.0 import Sailfish.Telephony 1.0 @@ -15,23 +15,32 @@ import Sailfish.Settings.Networking 1.0 import com.jolla.lipstick 0.1 import org.nemomobile.devicelock 1.0 import org.nemomobile.lipstick 0.1 -import org.nemomobile.time 1.0 +import Nemo.Time 1.0 +import Nemo.Configuration 1.0 import "../lockscreen" import "../main" import "../backgrounds" ContrastBackground { id: statusArea + property bool updatesEnabled: true property bool recentlyOnDisplay: true property bool lockscreenMode - property string iconSuffix: lipstickSettings.lowPowerMode ? ('?' + Theme.highlightColor) : '' - property string mobileDataIconSuffix: '?' + (lipstickSettings.lowPowerMode ? Theme.highlightColor : mobileDataIconColor) - property alias mobileDataIconColor: cellularStatusLoader.mobileDataColor - property color color: lipstickSettings.lowPowerMode ? Theme.highlightColor : Theme.primaryColor + property color color: Theme.primaryColor + property int cornerPadding: { + // assuming the roundings are simple with x and y detached the radius amount from edges. + // for simplicity using just one padding (they are likely same anyway) + var biggestCorner = Math.max(Screen.topLeftCorner.radius, + Screen.topRightCorner.radius, + Screen.bottomLeftCorner.radius, + Screen.bottomRightCorner.radius) + // 0.7 assumed being enough of the rounding to avoid + return Math.max(biggestCorner * 0.7, Theme.paddingMedium) + } onUpdatesEnabledChanged: if (updatesEnabled) recentlyOnDisplay = updatesEnabled - height: batteryStatusIndicator.totalHeight + height: iconBar.height width: parent.width Timer { @@ -42,19 +51,25 @@ ContrastBackground { Item { id: iconBar + width: parent.width + // assuming the cutout case doesn't need padding due to clock item text not drawing full height height: batteryStatusIndicator.height + + (Screen.hasCutouts && Lipstick.compositor.topmostWindowOrientation === Qt.PortraitOrientation + ? Screen.topCutout.height : 0) - // Left side status indicators Row { id: leftIndicators + + x: statusArea.cornerPadding height: batteryStatusIndicator.height spacing: Theme.paddingSmall + BatteryStatusIndicator { id: batteryStatusIndicator + color: statusArea.color usbPreparingMode: usbModeSelector.preparingMode != "" - iconSuffix: statusArea.iconSuffix } ProfileStatusIndicator { @@ -71,35 +86,15 @@ ContrastBackground { //XXX Headset indicator //XXX Call forwarding indicator - - Loader { - active: Desktop.showDualSim - visible: active - sourceComponent: floatingIndicators - } - } - - // These indicators could be on either side, depending upon dual sim - Component { - id: floatingIndicators - Row { - spacing: Theme.paddingSmall - BluetoothStatusIndicator { - anchors.verticalCenter: parent.verticalCenter - visible: opacity > 0.0 - } - LocationStatusIndicator { - anchors.verticalCenter: parent.verticalCenter - visible: opacity > 0.0 - recentlyOnDisplay: statusArea.recentlyOnDisplay - } - } } Item { id: centralArea + anchors { top: iconBar.top + topMargin: Screen.hasCutouts && Lipstick.compositor.topmostWindowOrientation === Qt.PortraitOrientation + ? (Screen.topCutout.height) : 0 bottom: iconBar.bottom left: leftIndicators.right leftMargin: Theme.paddingMedium @@ -109,8 +104,18 @@ ContrastBackground { Loader { // If possible position this item centrally within the iconBar x: Math.max((iconBar.width - width)/2 - parent.x, 0) - y: (parent.height - height)/2 - sourceComponent: lockscreenMode ? lockIcon : timeText + y: (parent.height - height) / 2 + sourceComponent: lockscreenMode ? lockIcon + : displayClockOnLauncher.value ? undefined // clock already shown on the launcher header + : timeText + + + ConfigurationValue { + id: displayClockOnLauncher + + key: "/desktop/sailfish/experimental/display_clock_on_launcher" + defaultValue: false + } } } @@ -141,23 +146,30 @@ ContrastBackground { } } - // Right side status indicators Row { id: rightIndicators - height: parent.height + + height: leftIndicators.height spacing: Theme.paddingSmall anchors { right: parent.right - rightMargin: Theme.paddingMedium + rightMargin: statusArea.cornerPadding + } + + // Location status indicator positioned to leftmost on right side + // due to JB#58226 to avoid abrupt movement of the other indicators. + LocationStatusIndicator { + anchors.verticalCenter: parent.verticalCenter + visible: opacity > 0.0 + recentlyOnDisplay: statusArea.recentlyOnDisplay } VpnStatusIndicator { id: vpnStatusIndicator anchors.verticalCenter: parent.verticalCenter } - Loader { - active: !Desktop.showDualSim - visible: active - sourceComponent: floatingIndicators + BluetoothStatusIndicator { + anchors.verticalCenter: parent.verticalCenter + visible: opacity > 0.0 } ConnectionStatusIndicator { id: connStatusIndicator @@ -166,7 +178,7 @@ ContrastBackground { } Item { width: flightModeStatusIndicator.offline ? flightModeStatusIndicator.width : cellularStatusLoader.width - height: iconBar.height + height: parent.height visible: Desktop.simManager.enabledModems.length > 0 || flightModeStatusIndicator.offline FlightModeStatusIndicator { @@ -176,22 +188,21 @@ ContrastBackground { Loader { id: cellularStatusLoader + height: parent.height active: Desktop.simManager.availableModemCount > 0 - readonly property color mobileDataColor: item ? item.mobileDataColor : statusArea.color sourceComponent: Row { - property alias mobileDataColor: cellularNetworkTypeStatusIndicator.color height: parent.height opacity: 1.0 - flightModeStatusIndicator.opacity CellularNetworkTypeStatusIndicator { - id: cellularNetworkTypeStatusIndicator anchors.verticalCenter: parent.verticalCenter color: { - var repeaterItem = Desktop.simManager.indexOfModem(Desktop.simManager.defaultDataModem) === 1 && networkStatusRepeater.count > 1 + var repeaterItem = (Desktop.simManager.indexOfModem(Desktop.simManager.defaultDataModem) === 1 + && networkStatusRepeater.count > 1) ? networkStatusRepeater.itemAt(1) : networkStatusRepeater.itemAt(0) - return !!repeaterItem ? repeaterItem.iconColor : statusArea.color + return !!repeaterItem && repeaterItem.highlighted ? Theme.highlightColor : statusArea.color } } @@ -201,9 +212,8 @@ ContrastBackground { model: Desktop.simManager.enabledModems MobileNetworkStatusIndicator { - readonly property color iconColor: _highlight ? Theme.highlightColor : statusArea.color - readonly property bool _highlight: Telephony.promptForVoiceSim - || (Desktop.showDualSim && Desktop.simManager.activeModem !== modemPath) + highlighted: Telephony.promptForVoiceSim + || (Desktop.showDualSim && Desktop.simManager.activeModem !== modemPath) visible: Desktop.showDualSim || Desktop.simManager.activeModem === modemPath modemPath: modelData @@ -211,7 +221,6 @@ ContrastBackground { showMaximumStrength: fakeOperator !== "" showRoamingStatus: !Desktop.showDualSim - iconSuffix: _highlight ? ('?' + Theme.highlightColor) : statusArea.iconSuffix } } } diff --git a/usr/share/lipstick-jolla-home-qt5/statusarea/StatusBar.qml b/usr/share/lipstick-jolla-home-qt5/statusarea/StatusBar.qml index 0d7e94d3..d0a10598 100644 --- a/usr/share/lipstick-jolla-home-qt5/statusarea/StatusBar.qml +++ b/usr/share/lipstick-jolla-home-qt5/statusarea/StatusBar.qml @@ -1,4 +1,4 @@ -import QtQuick 2.0 +import QtQuick 2.6 import Sailfish.Silica 1.0 import Sailfish.Silica.private 1.0 as SilicaPrivate import Sailfish.Lipstick 1.0 @@ -13,7 +13,6 @@ SilicaControl { property alias updatesEnabled: statusArea.updatesEnabled property alias recentlyOnDisplay: statusArea.recentlyOnDisplay property alias lockscreenMode: statusArea.lockscreenMode - property alias iconSuffix: statusArea.iconSuffix property alias color: statusArea.color property alias backgroundVisible: background.visible property alias shadowVisible: statusArea.shadowVisible @@ -25,12 +24,13 @@ SilicaControl { SilicaPrivate.OverlayGradient { id: background + visible: false anchors { fill: parent bottomMargin: -2 * Theme.paddingLarge } - opacity: 1.0 - Math.abs(statusBar.y/Theme.paddingMedium) + opacity: 1.0 - Math.abs(statusBar.y / Theme.paddingMedium) } MouseArea { @@ -47,11 +47,14 @@ SilicaControl { Item { id: statusAreaContainer + height: parent.height width: parent.width clip: Lipstick.compositor.statusBarPushDownY > 0 + StatusArea { id: statusArea + y: baseY + Lipstick.compositor.statusBarPushDownY } } diff --git a/usr/share/lipstick-jolla-home-qt5/statusarea/TetheringStatus.qml b/usr/share/lipstick-jolla-home-qt5/statusarea/TetheringStatus.qml index e9008509..4b769f36 100644 --- a/usr/share/lipstick-jolla-home-qt5/statusarea/TetheringStatus.qml +++ b/usr/share/lipstick-jolla-home-qt5/statusarea/TetheringStatus.qml @@ -8,7 +8,7 @@ ****************************************************************************/ import QtQml 2.2 -import MeeGo.Connman 0.2 +import Connman 0.2 QtObject { id: tethering diff --git a/usr/share/lipstick-jolla-home-qt5/statusarea/VpnStatus.qml b/usr/share/lipstick-jolla-home-qt5/statusarea/VpnStatus.qml index 78ba7d71..75e42a23 100644 --- a/usr/share/lipstick-jolla-home-qt5/statusarea/VpnStatus.qml +++ b/usr/share/lipstick-jolla-home-qt5/statusarea/VpnStatus.qml @@ -7,9 +7,9 @@ ** ****************************************************************************/ -import QtQuick 2.2 -import MeeGo.Connman 0.2 -import org.nemomobile.systemsettings 1.0 +import QtQuick 2.6 +import Connman 0.2 +import Nemo.Connectivity 1.0 QtObject { id: vpn diff --git a/usr/share/lipstick-jolla-home-qt5/statusarea/VpnStatusIndicator.qml b/usr/share/lipstick-jolla-home-qt5/statusarea/VpnStatusIndicator.qml index f31c1080..3c191107 100644 --- a/usr/share/lipstick-jolla-home-qt5/statusarea/VpnStatusIndicator.qml +++ b/usr/share/lipstick-jolla-home-qt5/statusarea/VpnStatusIndicator.qml @@ -7,10 +7,10 @@ ** ****************************************************************************/ -import QtQuick 2.0 +import QtQuick 2.6 import Sailfish.Silica 1.0 -import MeeGo.Connman 0.2 -import org.nemomobile.systemsettings 1.0 +import Connman 0.2 +import Nemo.Connectivity 1.0 Item { id: vpnStatusIndicator diff --git a/usr/share/lipstick-jolla-home-qt5/statusarea/WlanStatus.qml b/usr/share/lipstick-jolla-home-qt5/statusarea/WlanStatus.qml index 6a252875..d3bd22a4 100644 --- a/usr/share/lipstick-jolla-home-qt5/statusarea/WlanStatus.qml +++ b/usr/share/lipstick-jolla-home-qt5/statusarea/WlanStatus.qml @@ -1,6 +1,6 @@ import QtQml 2.2 import com.jolla.lipstick 0.1 -import MeeGo.Connman 0.2 +import Connman 0.2 QtObject { id: wlan diff --git a/usr/share/lipstick-jolla-home-qt5/switcher/CloseAllAppsHint.qml b/usr/share/lipstick-jolla-home-qt5/switcher/CloseAllAppsHint.qml index 7d40fc8c..9c41834d 100644 --- a/usr/share/lipstick-jolla-home-qt5/switcher/CloseAllAppsHint.qml +++ b/usr/share/lipstick-jolla-home-qt5/switcher/CloseAllAppsHint.qml @@ -1,4 +1,4 @@ -import QtQuick 2.0 +import QtQuick 2.6 import Sailfish.Silica 1.0 Loader { diff --git a/usr/share/lipstick-jolla-home-qt5/switcher/StartupWatcher.qml b/usr/share/lipstick-jolla-home-qt5/switcher/StartupWatcher.qml index bc3115c2..d692d08f 100644 --- a/usr/share/lipstick-jolla-home-qt5/switcher/StartupWatcher.qml +++ b/usr/share/lipstick-jolla-home-qt5/switcher/StartupWatcher.qml @@ -1,4 +1,4 @@ -import QtQuick 2.2 +import QtQuick 2.6 import com.jolla.lipstick 0.1 Timer { diff --git a/usr/share/lipstick-jolla-home-qt5/switcher/Switcher.qml b/usr/share/lipstick-jolla-home-qt5/switcher/Switcher.qml index d95dea4d..fc7b7e2f 100644 --- a/usr/share/lipstick-jolla-home-qt5/switcher/Switcher.qml +++ b/usr/share/lipstick-jolla-home-qt5/switcher/Switcher.qml @@ -5,9 +5,9 @@ ** ****************************************************************************/ -import QtQuick 2.0 +import QtQuick 2.6 import org.nemomobile.lipstick 0.1 -import org.nemomobile.ngf 1.0 +import Nemo.Ngf 1.0 import com.jolla.lipstick 0.1 import Sailfish.Silica 1.0 import Sailfish.Silica.private 1.0 @@ -33,9 +33,8 @@ SilicaFlickable { property int secondLastAppIndex - readonly property bool switcherVisible: Lipstick.compositor && Lipstick.compositor.switcherLayer.visible - onSwitcherVisibleChanged: { - if (!switcherVisible) { + onVisibleChanged: { + if (!visible) { housekeeping = false // The view is completely hidden. The delay is a grace period, so // that if you quickly exit and reenter the view has not moved. @@ -382,11 +381,19 @@ SilicaFlickable { onWidthChanged: switcherGrid.updateColumns() + ViewPlaceholder { + //% "No apps running" + text: qsTrId("lipstick-jolla-home-me-no_apps_running") + y: parent.height/3 - height/2 + opacity: !Lipstick.compositor.multitaskingHome && switcherRoot.count === 0 ? 1.0 : 0.0 + Behavior on opacity { FadeAnimator {}} + } + SwitcherGrid { id: switcherGrid columns: largeColumns - statusBarHeight: switcherRoot.statusBarHeight + statusBarHeight: Lipstick.compositor.multitaskingHome ? switcherRoot.statusBarHeight : 0 readonly property bool allowSmallCovers: !largeScreen readonly property int largeItemCount: largeColumns * largeRows @@ -396,7 +403,7 @@ SilicaFlickable { function updateColumns() { // use a timer since switcherModel and pendingWindows models aren't in sync. if (!switcherRoot.housekeeping) { - if (switcherRoot.switcherVisible) { + if (switcherRoot.visible) { columnUpdateTimer.restart() } else { doUpdateColumns() @@ -410,7 +417,7 @@ SilicaFlickable { cols = switcherGrid.smallColumns if (cols !== switcherGrid.columns) { scrollAnimation.stop() - if (desktop.orientationTransitionRunning || !switcherRoot.switcherVisible) { + if (desktop.orientationTransitionRunning || !switcherRoot.visible) { switcherGrid.columns = cols switcherGrid.coverSize = switcherGrid.columns == switcherGrid.largeColumns ? Theme.coverSizeLarge : Theme.coverSizeSmall } else { @@ -509,7 +516,7 @@ SilicaFlickable { showingWid: switcherRoot.showingWid columns: switcherGrid.columns - animateMovement: switcherRoot.switcherVisible + animateMovement: switcherRoot.visible && !columnChangeAnimation.running && !desktop.orientationTransitionRunning @@ -518,19 +525,24 @@ SilicaFlickable { onClicked: { if (switcherRoot.housekeeping) { switcherRoot.housekeeping = false - } else if (running) { - switcherRoot.minimizeLaunchingWindows() - minimized = false - Lipstick.compositor.windowToFront(windowId) - } else if (launcherItem) { - var wasLaunching = launching - switcherRoot.minimizeLaunchingWindows() - // App is not running. Launch it now. - launching = true - minimized = false - switcherRoot.launchingItem = switcherDelegate - if (!wasLaunching) { - launcherItem.launchApplication() + } else { + if (!Lipstick.compositor.multitaskingHome) { + Lipstick.compositor.launcherLayer.hide() + } + if (running) { + switcherRoot.minimizeLaunchingWindows() + minimized = false + Lipstick.compositor.windowToFront(windowId) + } else if (launcherItem) { + var wasLaunching = launching + switcherRoot.minimizeLaunchingWindows() + // App is not running. Launch it now. + launching = true + minimized = false + switcherRoot.launchingItem = switcherDelegate + if (!wasLaunching) { + launcherItem.launchApplication() + } } } } @@ -598,7 +610,7 @@ SilicaFlickable { Component.onCompleted: { // avoid hard dependency to ngf module - ngfEffect = Qt.createQmlObject("import org.nemomobile.ngf 1.0; NonGraphicalFeedback { event: 'push_gesture' }", + ngfEffect = Qt.createQmlObject("import Nemo.Ngf 1.0; NonGraphicalFeedback { event: 'push_gesture' }", switcherGrid, 'NonGraphicalFeedback') } } @@ -631,7 +643,7 @@ SilicaFlickable { var wasHousekeeping = switcherRoot.housekeeping if (switcherRoot.housekeeping && !switcherRoot.housekeepingMenuActive) switcherRoot.housekeeping = false - else if (!wasHousekeeping) + else if (!wasHousekeeping && Lipstick.compositor.multitaskingHome) Lipstick.compositor.launcherLayer.showHint() } diff --git a/usr/share/lipstick-jolla-home-qt5/switcher/SwitcherItem.qml b/usr/share/lipstick-jolla-home-qt5/switcher/SwitcherItem.qml index 0c91505d..73c9325b 100644 --- a/usr/share/lipstick-jolla-home-qt5/switcher/SwitcherItem.qml +++ b/usr/share/lipstick-jolla-home-qt5/switcher/SwitcherItem.qml @@ -5,10 +5,10 @@ ** ****************************************************************************/ -import QtQuick 2.0 +import QtQuick 2.6 import QtQuick.Window 2.1 as QtQuick import org.nemomobile.lipstick 0.1 -import org.nemomobile.ngf 1.0 +import Nemo.Ngf 1.0 import com.jolla.coveractions 0.1 import Sailfish.Silica 1.0 import Sailfish.Silica.Background 1.0 @@ -85,7 +85,7 @@ EditableGridDelegate { cancelAnimation() contentItem.opacity = 1.0 contentItem.x = x - contentItem.y = offsetY + contentItem.y = targetY _oldY = y _viewWidth = manager.view.width } @@ -167,7 +167,7 @@ EditableGridDelegate { opacity: wrapper.coverOpacity width: rotation % 180 == 0 ? wrapper.width : wrapper.height height: rotation % 180 == 0 ? wrapper.height : wrapper.width - windowId: wrapper.coverId?wrapper.coverId:wrapper.windowId + windowId: wrapper.coverId ? wrapper.coverId : wrapper.windowId radius: Theme.paddingMedium smooth: true anchors.centerIn: parent @@ -415,6 +415,11 @@ EditableGridDelegate { } } + Connections { + target: wrapper.window + onClosed: wrapper.close() + } + SequentialAnimation { id: closeAnimation ParallelAnimation { diff --git a/usr/share/lipstick-jolla-home-qt5/system/ShutdownScreen.qml b/usr/share/lipstick-jolla-home-qt5/system/ShutdownScreen.qml index f20ffe44..e3b89597 100644 --- a/usr/share/lipstick-jolla-home-qt5/system/ShutdownScreen.qml +++ b/usr/share/lipstick-jolla-home-qt5/system/ShutdownScreen.qml @@ -5,7 +5,7 @@ * License: Proprietary */ -import QtQuick 2.0 +import QtQuick 2.6 import org.nemomobile.lipstick 0.1 import Sailfish.Silica 1.0 import Sailfish.Lipstick 1.0 diff --git a/usr/share/lipstick-jolla-home-qt5/system/StartupScreenBlanker.qml b/usr/share/lipstick-jolla-home-qt5/system/StartupScreenBlanker.qml index 5d10f270..f80fdb76 100644 --- a/usr/share/lipstick-jolla-home-qt5/system/StartupScreenBlanker.qml +++ b/usr/share/lipstick-jolla-home-qt5/system/StartupScreenBlanker.qml @@ -5,7 +5,7 @@ ** ****************************************************************************/ -import QtQuick 2.1 +import QtQuick 2.6 import Sailfish.Silica 1.0 import org.nemomobile.lipstick 0.1 diff --git a/usr/share/lipstick-jolla-home-qt5/systemwindow/SystemWindow.qml b/usr/share/lipstick-jolla-home-qt5/systemwindow/SystemWindow.qml index ed91a77c..3bb9371e 100644 --- a/usr/share/lipstick-jolla-home-qt5/systemwindow/SystemWindow.qml +++ b/usr/share/lipstick-jolla-home-qt5/systemwindow/SystemWindow.qml @@ -5,12 +5,14 @@ ** ****************************************************************************/ -import QtQuick 2.0 +import QtQuick 2.6 import Sailfish.Silica 1.0 import org.nemomobile.lipstick 0.1 FocusScope { id: systemWindow + + property int topmostWindowOrientation: Lipstick.compositor.topmostWindowOrientation property bool transpose: Lipstick.compositor.topmostWindowAngle % 180 != 0 property real contentHeight: height diff --git a/usr/share/lipstick-jolla-home-qt5/topmenu/AmbienceInstallPlaceholder.qml b/usr/share/lipstick-jolla-home-qt5/topmenu/AmbienceInstallPlaceholder.qml index 21d7c24f..3f37f851 100644 --- a/usr/share/lipstick-jolla-home-qt5/topmenu/AmbienceInstallPlaceholder.qml +++ b/usr/share/lipstick-jolla-home-qt5/topmenu/AmbienceInstallPlaceholder.qml @@ -1,4 +1,4 @@ -import QtQuick 2.0 +import QtQuick 2.6 import Sailfish.Silica 1.0 Item { diff --git a/usr/share/lipstick-jolla-home-qt5/topmenu/AmbienceSelector.qml b/usr/share/lipstick-jolla-home-qt5/topmenu/AmbienceSelector.qml index fbf904da..fac4161b 100644 --- a/usr/share/lipstick-jolla-home-qt5/topmenu/AmbienceSelector.qml +++ b/usr/share/lipstick-jolla-home-qt5/topmenu/AmbienceSelector.qml @@ -5,15 +5,14 @@ ** ****************************************************************************/ -import QtQuick 2.5 +import QtQuick 2.6 import Sailfish.Silica 1.0 import Sailfish.Silica.private 1.0 import Sailfish.Ambience 1.0 import Sailfish.Gallery 1.0 import Nemo.DBus 2.0 import Nemo.Thumbnailer 1.0 -import org.nemomobile.notifications 1.0 as SystemNotifications -import org.nemomobile.configuration 1.0 +import Nemo.Configuration 1.0 import org.nemomobile.lipstick 0.1 import org.nemomobile.devicelock 1.0 import com.jolla.lipstick 0.1 @@ -35,17 +34,6 @@ Item { visible: ambiencesEnabled.value clip: ambienceList.y < 0 - Timer { - id: ambiencePreviewTimer - interval: 200 - onTriggered: ambiencePreviewNotification.publish() - } - - SystemNotifications.Notification { - id: ambiencePreviewNotification - category: "x-jolla.ambience.preview" - } - ConfigurationValue { id: ambiencesEnabled key: "/desktop/lipstick-jolla-home/topmenu_ambiences_enabled" @@ -85,19 +73,8 @@ Item { NumberAnimation { property: "x"; duration: 500; easing.type: Easing.InOutQuad } } - model: AmbienceInstallModel { - source: AmbienceModel { - id: ambienceModel - } - - onAmbienceInstalling: { - ambiencePreviewNotification.summary = displayName - ambiencePreviewNotification.body = coverImage - // Give some time for the TOH dialog to fade out - ambiencePreviewTimer.restart() - } - - onAmbienceInstalled: ambienceModel.makeCurrent(index) + model: AmbienceModel { + id: ambienceModel } delegate: ListItem { @@ -109,7 +86,6 @@ Item { width: root.itemSize contentHeight: width - highlightedColor: Theme.rgba(highlightBackgroundColor || Theme.highlightBackgroundColor, Theme.highlightBackgroundOpacity) openMenuOnPressAndHold: Desktop.deviceLockState === DeviceLock.Unlocked onClicked: { diff --git a/usr/share/lipstick-jolla-home-qt5/topmenu/DynamicSwitchModel.qml b/usr/share/lipstick-jolla-home-qt5/topmenu/DynamicSwitchModel.qml index 9097788f..7835a565 100644 --- a/usr/share/lipstick-jolla-home-qt5/topmenu/DynamicSwitchModel.qml +++ b/usr/share/lipstick-jolla-home-qt5/topmenu/DynamicSwitchModel.qml @@ -1,4 +1,4 @@ -import QtQuick 2.2 +import QtQuick 2.6 import com.jolla.settings 1.0 ListModel { diff --git a/usr/share/lipstick-jolla-home-qt5/topmenu/DynamicSwitches.qml b/usr/share/lipstick-jolla-home-qt5/topmenu/DynamicSwitches.qml index 7c2ce87c..e4cb98db 100644 --- a/usr/share/lipstick-jolla-home-qt5/topmenu/DynamicSwitches.qml +++ b/usr/share/lipstick-jolla-home-qt5/topmenu/DynamicSwitches.qml @@ -1,4 +1,4 @@ -import QtQuick 2.2 +import QtQuick 2.6 import Sailfish.Silica 1.0 import Sailfish.Lipstick 1.0 // BluetoothStatus import com.jolla.lipstick 0.1 // LocationStatus diff --git a/usr/share/lipstick-jolla-home-qt5/topmenu/FavoriteSettingsDelegate.qml b/usr/share/lipstick-jolla-home-qt5/topmenu/FavoriteSettingsDelegate.qml index a989debe..f50a02a4 100644 --- a/usr/share/lipstick-jolla-home-qt5/topmenu/FavoriteSettingsDelegate.qml +++ b/usr/share/lipstick-jolla-home-qt5/topmenu/FavoriteSettingsDelegate.qml @@ -5,7 +5,7 @@ ** ****************************************************************************/ -import QtQuick 2.0 +import QtQuick 2.6 import Sailfish.Silica 1.0 import com.jolla.lipstick 0.1 import com.jolla.settings 1.0 diff --git a/usr/share/lipstick-jolla-home-qt5/topmenu/FavoriteSettingsItem.qml b/usr/share/lipstick-jolla-home-qt5/topmenu/FavoriteSettingsItem.qml index 16eff949..de2cb961 100644 --- a/usr/share/lipstick-jolla-home-qt5/topmenu/FavoriteSettingsItem.qml +++ b/usr/share/lipstick-jolla-home-qt5/topmenu/FavoriteSettingsItem.qml @@ -1,4 +1,4 @@ -import QtQuick 2.0 +import QtQuick 2.6 import Sailfish.Silica 1.0 import com.jolla.settings 1.0 import org.nemomobile.lipstick 0.1 diff --git a/usr/share/lipstick-jolla-home-qt5/topmenu/FavoriteSettingsLoader.qml b/usr/share/lipstick-jolla-home-qt5/topmenu/FavoriteSettingsLoader.qml index 401a496f..a47670f2 100644 --- a/usr/share/lipstick-jolla-home-qt5/topmenu/FavoriteSettingsLoader.qml +++ b/usr/share/lipstick-jolla-home-qt5/topmenu/FavoriteSettingsLoader.qml @@ -7,7 +7,7 @@ ** ****************************************************************************/ -import QtQuick 2.0 +import QtQuick 2.6 import Sailfish.Silica 1.0 import com.jolla.settings 1.0 import Nemo.DBus 2.0 diff --git a/usr/share/lipstick-jolla-home-qt5/topmenu/PowerButton.qml b/usr/share/lipstick-jolla-home-qt5/topmenu/PowerButton.qml index 013e1acf..bfbfac15 100644 --- a/usr/share/lipstick-jolla-home-qt5/topmenu/PowerButton.qml +++ b/usr/share/lipstick-jolla-home-qt5/topmenu/PowerButton.qml @@ -1,4 +1,4 @@ -import QtQuick 2.2 +import QtQuick 2.6 import Sailfish.Silica 1.0 MouseArea { diff --git a/usr/share/lipstick-jolla-home-qt5/topmenu/SimSelector.qml b/usr/share/lipstick-jolla-home-qt5/topmenu/SimSelector.qml index 4279e2ff..cdcf5a5a 100644 --- a/usr/share/lipstick-jolla-home-qt5/topmenu/SimSelector.qml +++ b/usr/share/lipstick-jolla-home-qt5/topmenu/SimSelector.qml @@ -5,7 +5,7 @@ * License: Proprietary */ -import QtQuick 2.0 +import QtQuick 2.6 import Sailfish.Silica 1.0 import Sailfish.Telephony 1.0 as Telephony import Sailfish.AccessControl 1.0 diff --git a/usr/share/lipstick-jolla-home-qt5/topmenu/TopMenu.qml b/usr/share/lipstick-jolla-home-qt5/topmenu/TopMenu.qml index d12d25d1..40693591 100644 --- a/usr/share/lipstick-jolla-home-qt5/topmenu/TopMenu.qml +++ b/usr/share/lipstick-jolla-home-qt5/topmenu/TopMenu.qml @@ -7,13 +7,13 @@ ** ****************************************************************************/ -import QtQuick 2.5 +import QtQuick 2.6 import Sailfish.Silica 1.0 import Sailfish.Silica.private 1.0 import Nemo.Notifications 1.0 as SystemNotifications import org.nemomobile.lipstick 0.1 import com.jolla.lipstick 0.1 -import org.nemomobile.configuration 1.0 +import Nemo.Configuration 1.0 import org.nemomobile.devicelock 1.0 import org.nemomobile.systemsettings 1.0 import "../backgrounds" @@ -132,8 +132,15 @@ SilicaFlickable { id: column width: parent.width Item { - id: headerItem + id: topPadding + width: 1 + height: Lipstick.compositor.topmostWindowOrientation === Qt.PortraitOrientation + && Screen.topCutout.height > 0 + ? (Screen.topCutout.height + Theme.paddingSmall) : 0 + } + + Item { width: topMenu.width height: topMenu.itemSize @@ -172,11 +179,12 @@ SilicaFlickable { Row { id: shutdownOptions - y: Math.min(0, -height + topMenu.offset) + y: Math.min(0, -height - topPadding.height + topMenu.offset) width: topMenu.width height: topMenu.itemSize visible: Lipstick.compositor.powerKeyPressed + || Lipstick.compositor.experimentalFeatures.topmenu_shutdown_reboot_visible PowerButton { id: shutdownButton @@ -213,8 +221,7 @@ SilicaFlickable { PowerButton { id: lockButton - y: Math.min(0, -height + topMenu.offset) - + y: Math.min(0, -height - topPadding.height + topMenu.offset) width: topMenu.width height: topMenu.itemSize @@ -250,7 +257,7 @@ SilicaFlickable { Loader { id: shortcutsLoader width: parent.width - active: shortcutsEnabled.value || actionsEnabled.value || Desktop.showMultiSimSelector + active: shortcutsEnabled.value || actionsEnabled.value || Desktop.showMultiSimSelector ConfigurationValue { id: shortcutsEnabled diff --git a/usr/share/lipstick-jolla-home-qt5/topmenu/TopMenuWindow.qml b/usr/share/lipstick-jolla-home-qt5/topmenu/TopMenuWindow.qml index c1964368..79b30722 100644 --- a/usr/share/lipstick-jolla-home-qt5/topmenu/TopMenuWindow.qml +++ b/usr/share/lipstick-jolla-home-qt5/topmenu/TopMenuWindow.qml @@ -1,4 +1,4 @@ -import QtQuick 2.0 +import QtQuick 2.6 import Sailfish.Silica 1.0 import Nemo.DBus 2.0 import org.nemomobile.lipstick 0.1 @@ -55,8 +55,9 @@ ApplicationWindow { } Image { - y: menu.expanded && menu.contentHeight >= page.height + height && menu.atYEnd ? page.height - height : - menu.exposedArea.height - height + y: menu.expanded && menu.contentHeight >= page.height + height && menu.atYEnd + ? page.height - height + : menu.exposedArea.height - height anchors.horizontalCenter: parent.horizontalCenter source: "image://theme/graphic-edge-swipe-handle-bottom" rotation: 180 diff --git a/usr/share/lipstick-jolla-home-qt5/topmenu/UserItem.qml b/usr/share/lipstick-jolla-home-qt5/topmenu/UserItem.qml index 584b0c00..bee73d33 100644 --- a/usr/share/lipstick-jolla-home-qt5/topmenu/UserItem.qml +++ b/usr/share/lipstick-jolla-home-qt5/topmenu/UserItem.qml @@ -4,7 +4,7 @@ * License: Proprietary */ -import QtQuick 2.5 +import QtQuick 2.6 import Sailfish.Silica 1.0 import com.jolla.settings.system 1.0 diff --git a/usr/share/lipstick-jolla-home-qt5/topmenu/UserSelector.qml b/usr/share/lipstick-jolla-home-qt5/topmenu/UserSelector.qml index bf14f762..1e0ae92b 100644 --- a/usr/share/lipstick-jolla-home-qt5/topmenu/UserSelector.qml +++ b/usr/share/lipstick-jolla-home-qt5/topmenu/UserSelector.qml @@ -4,7 +4,7 @@ * License: Proprietary */ -import QtQuick 2.5 +import QtQuick 2.6 import Sailfish.Silica 1.0 import org.nemomobile.lipstick 0.1 import org.nemomobile.systemsettings 1.0 diff --git a/usr/share/lipstick-jolla-home-qt5/volumecontrol/Screenshot.qml b/usr/share/lipstick-jolla-home-qt5/volumecontrol/Screenshot.qml index f05a582e..db90c046 100644 --- a/usr/share/lipstick-jolla-home-qt5/volumecontrol/Screenshot.qml +++ b/usr/share/lipstick-jolla-home-qt5/volumecontrol/Screenshot.qml @@ -7,11 +7,11 @@ ** ****************************************************************************/ -import QtQuick 2.0 -import org.nemomobile.ngf 1.0 +import QtQuick 2.6 +import Nemo.Ngf 1.0 import com.jolla.lipstick 0.1 import org.nemomobile.lipstick 0.1 -import org.nemomobile.notifications 1.0 +import Nemo.Notifications 1.0 import org.nemomobile.systemsettings 1.0 import Sailfish.Silica 1.0 import Sailfish.Share 1.0 @@ -38,9 +38,7 @@ Item { fileUtils.mkdir(folderPath) } - //: Filename of a captured screenshot, e.g. "Screenshot_1" - //% "Screenshot_%1" - var filename = fileUtils.uniqueFileName(folderPath, qsTrId("lipstick-jolla-home-la-screenshot") + ".png") + var filename = fileUtils.uniqueFileName(folderPath, "Screenshot_%1" + ".png") var filePath = folderPath + filename shareAction.resources = [ filePath ] diff --git a/usr/share/lipstick-jolla-home-qt5/volumecontrol/ScreenshotToggle.qml b/usr/share/lipstick-jolla-home-qt5/volumecontrol/ScreenshotToggle.qml new file mode 100644 index 00000000..62a1ee38 --- /dev/null +++ b/usr/share/lipstick-jolla-home-qt5/volumecontrol/ScreenshotToggle.qml @@ -0,0 +1,16 @@ +import QtQuick 2.6 +import Sailfish.Silica 1.0 +import com.jolla.settings 1.0 +import com.jolla.settings.system 1.0 +import org.nemomobile.lipstick 0.1 + +SettingsToggle { + id: root + + //% "Screenshot" + name: qsTrId("settings_system-screenshot-button") + icon.source: "image://theme/icon-m-browser-camera" + checked: Lipstick.compositor.floatingScreenshotButtonActive + + onToggled: Lipstick.compositor.floatingScreenshotButtonActive = !Lipstick.compositor.floatingScreenshotButtonActive +} diff --git a/usr/share/lipstick-jolla-home-qt5/volumecontrol/VolumeControl.qml b/usr/share/lipstick-jolla-home-qt5/volumecontrol/VolumeControl.qml index 561f640e..6a467954 100644 --- a/usr/share/lipstick-jolla-home-qt5/volumecontrol/VolumeControl.qml +++ b/usr/share/lipstick-jolla-home-qt5/volumecontrol/VolumeControl.qml @@ -5,11 +5,11 @@ ** ****************************************************************************/ -import QtQuick 2.0 +import QtQuick 2.6 import org.nemomobile.lipstick 0.1 import Sailfish.Silica 1.0 import org.nemomobile.systemsettings 1.0 -import org.nemomobile.configuration 1.0 +import Nemo.Configuration 1.0 import com.jolla.lipstick 0.1 import QtFeedback 5.0 import "../systemwindow" @@ -27,6 +27,7 @@ SystemWindow { volumeControl.callActive || showContinuousVolume property real statusBarPushDownY: volumeArea.y + volumeArea.height property bool showContinuousVolume: false + property bool suppressVolumeBar property int maximumVolume: controllingMedia ? volumeControl.maximumVolume : 100 property real initialChange: 0 property bool disableSmoothChange: true @@ -106,6 +107,13 @@ SystemWindow { defaultValue: false } + // FIXME: This should be something cleaner for API point of view. JB#59279 + ConfigurationValue { + id: swVolumeSliderActive + key: "/jolla/sound/sw_volume_slider/active" + defaultValue: false + } + HapticsEffect { id: silenceVibra intensity: 0.2 @@ -116,7 +124,9 @@ SystemWindow { id: volumeArea width: parent.width - height: Theme.iconSizeSmall + Theme.paddingMedium + height: volumeAnnotation.height + + (Screen.hasCutouts && Lipstick.compositor.topmostWindowOrientation === Qt.PortraitOrientation + ? Screen.topCutout.height : 0) y: -height Rectangle { @@ -171,9 +181,12 @@ SystemWindow { } Item { - objectName: "volumeAnnotation" + id: volumeAnnotation - anchors.fill: parent + objectName: "volumeAnnotation" + width: parent.width + height: Theme.iconSizeSmall + Theme.paddingMedium + anchors.bottom: parent.bottom property bool mute: controllingMedia ? (!volumeControl.callActive && volumeControl.volume === 0) @@ -188,7 +201,18 @@ SystemWindow { id: muteIcon anchors.verticalCenter: parent.verticalCenter - x: Theme.horizontalPageMargin + x: { + if (Screen.topCutout.height > Theme.paddingLarge + && Lipstick.compositor.topmostWindowOrientation === Qt.PortraitOrientation) { + return Theme.horizontalPageMargin + } + + var biggestCorner = Math.max(Screen.topLeftCorner.radius, + Screen.topRightCorner.radius, + Screen.bottomLeftCorner.radius, + Screen.bottomRightCorner.radius) + return Math.max(biggestCorner, Theme.horizontalPageMargin) + } opacity: parent.muteOpacity property string baseSource: controllingMedia ? "image://theme/icon-system-volume-mute" : "image://theme/icon-system-ringtone-mute" @@ -199,7 +223,7 @@ SystemWindow { id: volumeIcon anchors.verticalCenter: parent.verticalCenter - x: Theme.horizontalPageMargin + x: muteIcon.x opacity: 1 - parent.muteOpacity property string baseSource: controllingMedia ? "image://theme/icon-system-volume" : "image://theme/icon-system-ringtone" @@ -401,6 +425,10 @@ SystemWindow { property bool warningActive function showWarning(initial) { + if (swVolumeSliderActive.value) { + volumeBar.suppressVolumeBar = !volumeControl.windowVisible + volumeControl.windowVisible = true + } warningActive = true loader.item.initial = initial loader.item.dismiss.connect(function () { @@ -459,7 +487,7 @@ SystemWindow { Connections { target: volumeControl onWindowVisibleChanged: { - if (volumeControl.windowVisible) { + if (volumeControl.windowVisible && !suppressVolumeBar) { if (volumeBar.state == "") { if (Lipstick.compositor.volumeGestureFilterItem.active) { volumeBar.state = "showBarGesture" @@ -469,6 +497,7 @@ SystemWindow { } } } + suppressVolumeBar = false } onVolumeChanged: restartHideTimerIfWindowVisibleAndWarningNotVisible() onVolumeKeyPressed: { diff --git a/usr/share/lipstick-jolla-home-qt5/volumecontrol/WarningNote.qml b/usr/share/lipstick-jolla-home-qt5/volumecontrol/WarningNote.qml index 84cb47e2..60bd8e6b 100644 --- a/usr/share/lipstick-jolla-home-qt5/volumecontrol/WarningNote.qml +++ b/usr/share/lipstick-jolla-home-qt5/volumecontrol/WarningNote.qml @@ -5,7 +5,7 @@ ** ****************************************************************************/ -import QtQuick 2.0 +import QtQuick 2.6 import Sailfish.Silica 1.0 import Sailfish.Lipstick 1.0 import org.nemomobile.lipstick 0.1 diff --git a/usr/share/lipstick-jolla-home-qt5/windowwrappers/InProcWindowWrapper.qml b/usr/share/lipstick-jolla-home-qt5/windowwrappers/InProcWindowWrapper.qml index 46aa4ed6..8353bb33 100644 --- a/usr/share/lipstick-jolla-home-qt5/windowwrappers/InProcWindowWrapper.qml +++ b/usr/share/lipstick-jolla-home-qt5/windowwrappers/InProcWindowWrapper.qml @@ -5,7 +5,7 @@ ** ****************************************************************************/ -import QtQuick 2.0 +import QtQuick 2.6 import QtQuick.Window 2.1 as QtQuick WindowWrapperBase { diff --git a/usr/share/lipstick-jolla-home-qt5/windowwrappers/WindowWrapper.qml b/usr/share/lipstick-jolla-home-qt5/windowwrappers/WindowWrapper.qml index 84082b88..52cf6f8f 100644 --- a/usr/share/lipstick-jolla-home-qt5/windowwrappers/WindowWrapper.qml +++ b/usr/share/lipstick-jolla-home-qt5/windowwrappers/WindowWrapper.qml @@ -5,7 +5,7 @@ ** ****************************************************************************/ -import QtQuick 2.1 +import QtQuick 2.6 import org.nemomobile.lipstick 0.1 import QtQuick.Window 2.1 as QtQuick import Sailfish.Silica 1.0 diff --git a/usr/share/lipstick-jolla-home-qt5/windowwrappers/WindowWrapperBase.qml b/usr/share/lipstick-jolla-home-qt5/windowwrappers/WindowWrapperBase.qml index f729d8df..74e83b0b 100644 --- a/usr/share/lipstick-jolla-home-qt5/windowwrappers/WindowWrapperBase.qml +++ b/usr/share/lipstick-jolla-home-qt5/windowwrappers/WindowWrapperBase.qml @@ -5,7 +5,7 @@ ** ****************************************************************************/ -import QtQuick 2.0 +import QtQuick 2.6 import QtQuick.Window 2.1 as QtQuick import org.nemomobile.lipstick 0.1 diff --git a/usr/share/lipstick-obex-ui/IncomingFileConfirmationWindow.qml b/usr/share/lipstick-obex-ui/IncomingFileConfirmationWindow.qml index 93ca2b03..b39e18b5 100644 --- a/usr/share/lipstick-obex-ui/IncomingFileConfirmationWindow.qml +++ b/usr/share/lipstick-obex-ui/IncomingFileConfirmationWindow.qml @@ -5,7 +5,7 @@ ** ****************************************************************************/ -import QtQuick 2.0 +import QtQuick 2.6 import QtQuick.Window 2.0 import Sailfish.Silica 1.0 import Sailfish.Bluetooth 1.0 diff --git a/usr/share/lipstick-obex-ui/main.qml b/usr/share/lipstick-obex-ui/main.qml index 0c9c8780..b1d64d83 100644 --- a/usr/share/lipstick-obex-ui/main.qml +++ b/usr/share/lipstick-obex-ui/main.qml @@ -5,7 +5,7 @@ ** ****************************************************************************/ -import QtQuick 2.0 +import QtQuick 2.6 import QtQuick.Window 2.0 import Sailfish.Silica 1.0 import Nemo.DBus 2.0 diff --git a/usr/share/lipstick-security-ui/PasswordInputDialog.qml b/usr/share/lipstick-security-ui/PasswordInputDialog.qml index 16007d30..1fb333cf 100644 --- a/usr/share/lipstick-security-ui/PasswordInputDialog.qml +++ b/usr/share/lipstick-security-ui/PasswordInputDialog.qml @@ -28,6 +28,9 @@ SystemDialog { property alias suggestionText: suggestionLabel.text property int inputMethodHints + // extra toggle control for alphanumeric or digit codes + property bool digitsOnly + property bool alphanumericToggleEnabled property bool inputEnabled: true property bool requirePassword: true @@ -88,16 +91,32 @@ SystemDialog { width: header.width anchors.horizontalCenter: parent.horizontalCenter - Label { - id: descriptionLabel - + Item { x: (Screen.sizeCategory < Screen.Large) ? Theme.horizontalPageMargin : 0 width: header.width - 2*x - color: Theme.highlightColor - font.pixelSize: Theme.fontSizeMedium - wrapMode: Text.Wrap - horizontalAlignment: Text.AlignHCenter - height: implicitHeight + (root.suggestionsEnabled ? Theme.paddingSmall : Theme.paddingLarge) + height: alphanumericToggle.visible ? Math.max(alphanumericToggle.height, descriptionLabel.height) + : descriptionLabel.height + + IconButton { + id: alphanumericToggle + + visible: root.alphanumericToggleEnabled && passwordInput.visible + icon.source: root.digitsOnly ? "image://theme/icon-m-keyboard" + : "image://theme/icon-m-dialpad" + onClicked: root.digitsOnly = !root.digitsOnly + } + + Label { + id: descriptionLabel + + width: parent.width - 2 * (alphanumericToggle.visible ? (alphanumericToggle.width + Theme.paddingMedium) : 0) + color: Theme.highlightColor + font.pixelSize: Theme.fontSizeMedium + wrapMode: Text.Wrap + horizontalAlignment: Text.AlignHCenter + height: implicitHeight + (root.suggestionsEnabled ? Theme.paddingSmall : Theme.paddingLarge) + anchors.horizontalCenter: parent.horizontalCenter + } } BackgroundItem { @@ -133,6 +152,7 @@ SystemDialog { | Qt.ImhNoAutoUppercase | Qt.ImhHiddenText | Qt.ImhMultiLine // This stops the text input hiding the keyboard when enter is pressed. + | (root.digitsOnly ? Qt.ImhDigitsOnly : 0) _appWindow: undefined // suppresses warnings, TODO: fix password field magnifier color: Theme.highlightColor @@ -140,10 +160,10 @@ SystemDialog { placeholderColor: Theme.secondaryHighlightColor textMargin: 2*Theme.paddingLarge textTopMargin: Theme.paddingLarge - showEchoModeToggle: passwordEchoMode == TextInput.Normal + showEchoModeToggle: passwordEchoMode !== TextInput.Normal echoMode: (!showEchoModeToggle || _usePasswordEchoMode) && !root._showSuggestion ? passwordEchoMode - : TextInput.Password + : TextInput.Normal enabled: root.requirePassword && root.inputEnabled && !(root.suggestionsEnforced && root._showSuggestion) visible: root.requirePassword placeholderText: "" diff --git a/usr/share/lipstick-security-ui/SecurityCodeDialog.qml b/usr/share/lipstick-security-ui/SecurityCodeDialog.qml index 78ad337a..d332f562 100644 --- a/usr/share/lipstick-security-ui/SecurityCodeDialog.qml +++ b/usr/share/lipstick-security-ui/SecurityCodeDialog.qml @@ -23,7 +23,8 @@ PasswordInputDialog { minimumLength: agent.minimumCodeLength maximumLength: agent.maximumCodeLength - inputMethodHints: agent.codeInputIsKeyboard ? Qt.ImhPreferNumbers : Qt.ImhDigitsOnly + alphanumericToggleEnabled: true + digitsOnly: true passwordMaskDelay: 0 onConfirmed: { diff --git a/usr/share/lipstick-windowprompt/PermissionPrompt.qml b/usr/share/lipstick-windowprompt/PermissionPrompt.qml index 3a9ae8d6..87474b85 100644 --- a/usr/share/lipstick-windowprompt/PermissionPrompt.qml +++ b/usr/share/lipstick-windowprompt/PermissionPrompt.qml @@ -1,6 +1,6 @@ /* * Copyright (c) 2020 - 2021 Open Mobile Platform LLC. - * Copyright (c) 2021 Jolla Ltd. + * Copyright (c) 2021 - 2022 Jolla Ltd. * * License: Proprietary */ @@ -22,6 +22,7 @@ SystemDialog { function init(promptConfig) { root.promptConfig = promptConfig + // Column refreshes are lazy, so content.Height won't yet be correct at this point raise() show() // Trigger here to reset if another dialog is displayed without destructing the component @@ -39,26 +40,47 @@ SystemDialog { SilicaFlickable { id: flickable - readonly property real availableHeight: screenHeight - reservedHeight - buttons.height - autoDismissText.height - Theme.paddingSmall - property real originalContentHeight: content.height - property bool menuHasBeenOpened + + readonly property real availableHeight: screenHeight - reservedHeight - buttons.height + - autoDismissText.height - Theme.paddingSmall + property real originalContentHeight + property bool menuOpen + contentHeight: content.height height: Math.min(originalContentHeight, availableHeight) width: parent.width clip: contentHeight > availableHeight || contentHeight > originalContentHeight - onMenuHasBeenOpenedChanged: if (menuHasBeenOpened) originalContentHeight = content.height // break binding + + Binding on originalContentHeight { + when: !flickable.menuOpen + value: content.height + // Default restore mode in Qt 5 is Binding.RestoreBinding + // After Qt 6.0 the below line will need to be added + //restoreMode: Binding.RestoreNone + } Column { id: content topPadding: Theme.paddingLarge + + (Screen.hasCutouts && root.orientation === Qt.PortraitOrientation + ? Screen.topCutout.height : 0) spacing: Theme.paddingLarge width: parent.width Image { property string icon: root.promptConfig.icon || "" - source: icon != "" ? ((icon.indexOf("/") == 0 ? "file://" : "image://theme/") + icon) - : "" + source: { + if (icon !== "") { + if (icon.indexOf("/") === 0) { + return "file://" + icon + } else { + return LauncherUtil.resolveIconPath(icon) || "image://theme/" + icon + } + } else { + return "" + } + } anchors.horizontalCenter: parent.horizontalCenter height: Theme.iconSizeLauncher width: Theme.iconSizeLauncher @@ -90,10 +112,9 @@ SystemDialog { contentItem.clip: expanded contentHeight: description.implicitHeight + 2*description.y width: parent.width - onClicked: { - flickable.menuHasBeenOpened = true - openMenu() - } + onClicked: openMenu() + // We rely on the fact only one menu can be open at a time + onMenuOpenChanged: flickable.menuOpen = menuOpen Behavior on contentHeight { NumberAnimation { duration: 100; easing.type: Easing.InOutQuad } } Label { diff --git a/usr/share/lipstick-windowprompt/StorageDeviceSystemDialog.qml b/usr/share/lipstick-windowprompt/StorageDeviceSystemDialog.qml index b49de066..d285426e 100644 --- a/usr/share/lipstick-windowprompt/StorageDeviceSystemDialog.qml +++ b/usr/share/lipstick-windowprompt/StorageDeviceSystemDialog.qml @@ -7,7 +7,7 @@ ** ****************************************************************************/ -import QtQuick 2.0 +import QtQuick 2.6 import QtQuick.Window 2.1 as QtQuick import Sailfish.Silica 1.0 import Sailfish.Lipstick 1.0 @@ -125,7 +125,7 @@ SystemDialog { //% "Unlock memory card" : qsTrId("lipstick-jolla-home-he-memory_card_encrypted_unlock") - topPadding: Screen.sizeCategory >= Screen.Large ? 2*Theme.paddingLarge : Theme.paddingLarge + tight: true bottomPadding: Theme.paddingLarge } diff --git a/usr/share/lipstick-windowprompt/TermsPromptWindow.qml b/usr/share/lipstick-windowprompt/TermsPromptWindow.qml index 4732bceb..a01b5cf4 100644 --- a/usr/share/lipstick-windowprompt/TermsPromptWindow.qml +++ b/usr/share/lipstick-windowprompt/TermsPromptWindow.qml @@ -5,7 +5,7 @@ ** ****************************************************************************/ -import QtQuick 2.0 +import QtQuick 2.6 import QtQuick.Window 2.0 import Sailfish.Silica 1.0 import Sailfish.Lipstick 1.0 diff --git a/usr/share/lipstick-windowprompt/connectivity/AddNetworkView.qml b/usr/share/lipstick-windowprompt/connectivity/AddNetworkView.qml index 0dae6947..3b7d6810 100644 --- a/usr/share/lipstick-windowprompt/connectivity/AddNetworkView.qml +++ b/usr/share/lipstick-windowprompt/connectivity/AddNetworkView.qml @@ -1,13 +1,14 @@ -import QtQuick 2.0 +import QtQuick 2.6 import Sailfish.Silica 1.0 import Sailfish.Lipstick 1.0 import Sailfish.Settings.Networking 1.0 -import MeeGo.Connman 0.2 +import Connman 0.2 import Nemo.DBus 2.0 Column { id: root + property int horizontalMargin: Theme.paddingLarge property string path property bool canAccept: { if (network.ssid.length > 0 && (!identityField.required || network.identity.length > 0)) { @@ -61,12 +62,13 @@ Column { SystemDialogHeader { //% "Add network" title: qsTrId("lipstick_jolla_home-he-add_network") - topPadding: Screen.sizeCategory >= Screen.Large ? 2*Theme.paddingLarge : Theme.paddingLarge // align with ConnectionSelector + tight: true // align with ConnectionSelector } SsidField { id: ssidField + textMargin: root.horizontalMargin network: root.network property bool moveFocus: identityField.required || passphraseField.required @@ -87,10 +89,14 @@ Column { HiddenSwitch { network: root.network + leftMargin: root.horizontalMargin + rightMargin: root.horizontalMargin } EncryptionComboBox { network: root.network + leftMargin: root.horizontalMargin + rightMargin: root.horizontalMargin } Item { @@ -101,13 +107,18 @@ Column { EapComboBox { network: root.network + leftMargin: root.horizontalMargin + rightMargin: root.horizontalMargin } InnerAuthComboBox { network: root.network + leftMargin: root.horizontalMargin + rightMargin: root.horizontalMargin } CACertChooser { + horizontalMargin: root.horizontalMargin network: root.network onFromFileSelected: { @@ -124,12 +135,13 @@ Column { "clientCertFile": root.network.clientCertFile, "hidden": root.network.hidden }) - settingsDBus.call("showAddNetworkDialog"); + settingsDBus.call("showAddNetworkDialog") root.closeDialog() } } ClientCertChooser { + horizontalMargin: root.horizontalMargin network: root.network onKeyFromFileSelected: { @@ -147,7 +159,7 @@ Column { "clientCertFile": root.network.clientCertFile, "hidden": root.network.hidden }) - settingsDBus.call("showAddNetworkDialog"); + settingsDBus.call("showAddNetworkDialog") root.closeDialog() } onCertFromFileSelected: { @@ -165,7 +177,7 @@ Column { "clientCertFile": "custom", "hidden": root.network.hidden }) - settingsDBus.call("showAddNetworkDialog"); + settingsDBus.call("showAddNetworkDialog") root.closeDialog() } } @@ -173,6 +185,7 @@ Column { IdentityField { id: identityField + textMargin: root.horizontalMargin network: root.network immediateUpdate: true @@ -183,6 +196,7 @@ Column { PassphraseField { id: passphraseField + textMargin: root.horizontalMargin network: root.network immediateUpdate: true EnterKey.enabled: root.canAccept diff --git a/usr/share/lipstick-windowprompt/connectivity/ConnectionSelector.qml b/usr/share/lipstick-windowprompt/connectivity/ConnectionSelector.qml index 088ec726..cf3ef151 100644 --- a/usr/share/lipstick-windowprompt/connectivity/ConnectionSelector.qml +++ b/usr/share/lipstick-windowprompt/connectivity/ConnectionSelector.qml @@ -1,23 +1,23 @@ /**************************************************************************** ** -** Copyright (C) 2013 - 2019 Jolla Ltd. +** Copyright (C) 2013 - 2022 Jolla Ltd. ** Copyright (C) 2020 Open Mobile Platform LLC. ** ****************************************************************************/ -import QtQuick 2.0 +import QtQuick 2.6 import Sailfish.Silica 1.0 import Sailfish.Lipstick 1.0 import Sailfish.Telephony 1.0 import Sailfish.Settings.Networking 1.0 import Sailfish.Homescreen.UserAgent 1.0 -import MeeGo.Connman 0.2 -import MeeGo.QOfono 0.2 +import Connman 0.2 +import QOfono 0.2 import Nemo.Connectivity 1.0 import Nemo.DBus 2.0 import Nemo.Notifications 1.0 as SystemNotifications import org.nemomobile.ofono 1.0 -import org.nemomobile.configuration 1.0 +import Nemo.Configuration 1.0 import org.nemomobile.systemsettings 1.0 import Sailfish.Policy 1.0 @@ -33,6 +33,9 @@ SystemDialog { property real keyboardHeight: transpose ? Qt.inputMethod.keyboardRectangle.width : Qt.inputMethod.keyboardRectangle.height readonly property real reservedHeight: Math.max( (Screen.sizeCategory < Screen.Large ? 0.2 : 0.4) * screenHeight, keyboardHeight) - 1 + property int horizontalMargin: Math.max(Theme.paddingLarge, + (transpose && Screen.topCutout.height > 0) + ? Screen.topCutout.height + Theme.paddingSmall : 0) property var delayedFields: ({}) property string delayedServicePath @@ -148,14 +151,16 @@ SystemDialog { property bool disablingTethering property string cellularErrorText - property bool busy: (connectingService || disablingTethering || wifiListModel.scanning || delayedScanRequest.running || - (wifiMode && (wifiListModel.count == 0 && (!_wifiTechnology || !_wifiTechnology.tethering))) ) && - !(invalidCredentials || errorCondition) && (!wifiMode || wifiListModel.powered) + property bool busy: (connectingService || disablingTethering || delayedScanRequest.running + || (wifiMode && (wifiListModel.count == 0 + && (!_wifiTechnology || !_wifiTechnology.tethering)))) + && !(invalidCredentials || errorCondition) + && (!wifiMode || wifiListModel.powered) - property bool stateInformation: (!wifiMode && networkManager.offlineMode) || - (wifiMode && (!wifiListModel.powered || - (_wifiTechnology && _wifiTechnology.tethering))) || - busy || invalidCredentials || errorCondition || cellularErrorText.length > 0 + property bool stateInformation: (!wifiMode && networkManager.offlineMode) + || (wifiMode && (!wifiListModel.powered + || (_wifiTechnology && _wifiTechnology.tethering))) + || busy || invalidCredentials || errorCondition || cellularErrorText.length > 0 property bool showStatus: expanded && stateInformation && !networkListWrapper.visible property bool showList: expanded && wifiMode && wifiListModel.powered && !stateInformation && !statusArea.visible property bool addingNetwork @@ -169,9 +174,10 @@ SystemDialog { } width: parent.width - height: (connectionDialog.visible && expanded) ? (showStatus ? listView.headerHeight + statusArea.height - : (addingNetwork ? Math.min(contentHeight, expandedHeight) : expandedHeight)) - : listView.headerHeight + height: (connectionDialog.visible && expanded) + ? (showStatus ? listView.headerHeight + statusArea.height + : (addingNetwork ? Math.min(contentHeight, expandedHeight) : expandedHeight)) + : listView.headerHeight contentHeight: addingNetwork ? addNetworkView.height : listView.height clip: true @@ -332,17 +338,25 @@ SystemDialog { property bool waitingPropertiesReady property bool waitingAutoConnect property bool provisioningEap + property bool wasAvailable property Timer outOfRangeTimer: Timer { interval: 5000 onTriggered: connections.connectingService = false } + // FIXME: This exists to workaround a shortcoming with libconnman-qt + // as that sends sometimes extra signals causing onAvailableChanged + // to fire even if the value was the same as on previous call. + // JB#57750 + Component.onCompleted: wasAvailable = available + onAvailableChanged: { - if (available && !waitingPropertiesReady && connections.connectingService) { + if (!wasAvailable && available && !waitingPropertiesReady && connections.connectingService) { outOfRangeTimer.stop() connections.connectService(selectedService) } + wasAvailable = available } onPropertiesReady: { @@ -395,6 +409,7 @@ SystemDialog { active: connections.addingNetwork || opacity > 0.0 width: parent.width sourceComponent: AddNetworkView { + horizontalMargin: connectionDialog.horizontalMargin onAccepted: { connections.provision(config) selectedService.provisioningEap = false @@ -437,7 +452,7 @@ SystemDialog { id: header //% "Select internet connection" title: qsTrId("lipstick-jolla-home-he-connection_select") - topPadding: Screen.sizeCategory >= Screen.Large ? 2*Theme.paddingLarge : Theme.paddingLarge + tight: true } Row { @@ -571,7 +586,8 @@ SystemDialog { anchors.horizontalCenter: parent.horizontalCenter Image { id: addIcon - x: Theme.paddingLarge + + x: connectionDialog.horizontalMargin anchors.verticalCenter: parent.verticalCenter source: "image://theme/icon-m-add" + (addNetworkItem.highlighted ? "?" + Theme.highlightColor : "") } @@ -601,10 +617,9 @@ SystemDialog { model: connections.wifiMode ? wifiListModel : null delegate: WlanItem { - id: item - width: header.width anchors.horizontalCenter: parent.horizontalCenter + horizontalMargin: connectionDialog.horizontalMargin onClicked: { if (selectedService.path !== networkService.path) { connections.resetState() @@ -626,8 +641,9 @@ SystemDialog { onSend: { if (eap) { - if (!formData['Name']) - formData['Name'] = selectedService.name; + if (!formData['Name']) { + formData['Name'] = selectedService.name + } connections.provision(formData) selectedService.provisioningEap = true } else { @@ -761,7 +777,7 @@ SystemDialog { text: qsTrId("lipstick-jolla-home-bt-disable_internet_sharing") onClicked: { connections.disablingTethering = true - connectionAgent.stopTethering() + connectionAgent.stopTethering("wifi") } } }, @@ -914,8 +930,10 @@ SystemDialog { name: "wifi" changesInhibited: networkList.contextMenuOpen || !connectionDialog.visible onPoweredChanged: { - if (powered) - delayedScanRequest.start() + if (powered) { + delayedScanRequest.stop() + requestScan() + } } onScanRequestFinished: { if (delayedServicePath.length > 0) { diff --git a/usr/share/lipstick-windowprompt/connectivity/CredentialsForm.qml b/usr/share/lipstick-windowprompt/connectivity/CredentialsForm.qml index 31b24389..3a4a8e83 100644 --- a/usr/share/lipstick-windowprompt/connectivity/CredentialsForm.qml +++ b/usr/share/lipstick-windowprompt/connectivity/CredentialsForm.qml @@ -5,7 +5,7 @@ ** ****************************************************************************/ -import QtQuick 2.0 +import QtQuick 2.6 import Sailfish.Silica 1.0 ContextMenu { diff --git a/usr/share/lipstick-windowprompt/connectivity/DynamicFields.qml b/usr/share/lipstick-windowprompt/connectivity/DynamicFields.qml index 9090a32f..7c833796 100644 --- a/usr/share/lipstick-windowprompt/connectivity/DynamicFields.qml +++ b/usr/share/lipstick-windowprompt/connectivity/DynamicFields.qml @@ -5,7 +5,7 @@ ** ****************************************************************************/ -import QtQuick 2.0 +import QtQuick 2.6 import Sailfish.Silica 1.0 Column { diff --git a/usr/share/lipstick-windowprompt/connectivity/EapForm.qml b/usr/share/lipstick-windowprompt/connectivity/EapForm.qml index 71e54362..99a26bc7 100644 --- a/usr/share/lipstick-windowprompt/connectivity/EapForm.qml +++ b/usr/share/lipstick-windowprompt/connectivity/EapForm.qml @@ -5,10 +5,10 @@ ** ****************************************************************************/ -import QtQuick 2.0 +import QtQuick 2.6 import Sailfish.Silica 1.0 import Sailfish.Settings.Networking 1.0 -import MeeGo.Connman 0.2 +import Connman 0.2 import Nemo.DBus 2.0 Column { @@ -100,7 +100,7 @@ Column { "clientCertFile": network.clientCertFile, "hidden": network.hidden }) - settingsDBus.call("showAddNetworkDialog"); + settingsDBus.call("showAddNetworkDialog") root.closeDialog() } } @@ -122,7 +122,7 @@ Column { "clientCertFile": network.clientCertFile, "hidden": network.hidden }) - settingsDBus.call("showAddNetworkDialog"); + settingsDBus.call("showAddNetworkDialog") root.closeDialog() } @@ -141,7 +141,7 @@ Column { "clientCertFile": "custom", "hidden": network.hidden }) - settingsDBus.call("showAddNetworkDialog"); + settingsDBus.call("showAddNetworkDialog") root.closeDialog() } } diff --git a/usr/share/lipstick-windowprompt/connectivity/WlanItem.qml b/usr/share/lipstick-windowprompt/connectivity/WlanItem.qml index e331a438..f94ff78e 100644 --- a/usr/share/lipstick-windowprompt/connectivity/WlanItem.qml +++ b/usr/share/lipstick-windowprompt/connectivity/WlanItem.qml @@ -5,7 +5,7 @@ ** ****************************************************************************/ -import QtQuick 2.0 +import QtQuick 2.6 import Sailfish.Silica 1.0 import Sailfish.Settings.Networking 1.0 @@ -15,19 +15,22 @@ ListItem { property bool connected: modelData && (modelData.state == "online" || modelData.state == "ready") property bool highlightContent: connected || menuOpen || down property color baseColor: highlightContent ? Theme.highlightColor : Theme.primaryColor + property int horizontalMargin: Theme.paddingLarge openMenuOnPressAndHold: false highlightedColor: "transparent" Image { id: icon - x: Theme.paddingLarge + + x: listItem.horizontalMargin anchors.verticalCenter: parent.verticalCenter source: "image://theme/icon-m-wlan-" + WlanUtils.getStrengthString(modelData.strength) + "?" + listItem.baseColor } Label { id: serviceName + anchors { left: icon.right leftMargin: Theme.paddingSmall @@ -54,11 +57,12 @@ ListItem { Label { id: bssidLabel + anchors { leftMargin: Theme.paddingMedium verticalCenter: parent.verticalCenter right: secureIcon.visible ? secureIcon.left : parent.right - rightMargin: secureIcon.visible ? Theme.paddingSmall : Theme.paddingLarge + rightMargin: secureIcon.visible ? Theme.paddingSmall : listItem.horizontalMargin } font.pixelSize: Theme.fontSizeExtraSmall visible: !modelData.name @@ -68,7 +72,8 @@ ListItem { Image { id: secureIcon - x: parent.width - width - Theme.paddingLarge + + x: parent.width - width - listItem.horizontalMargin anchors.verticalCenter: parent.verticalCenter source: "image://theme/icon-s-secure?" + listItem.baseColor visible: modelData.security ? modelData.security.indexOf("none") == -1 : false diff --git a/usr/share/lipstick-windowprompt/main.qml b/usr/share/lipstick-windowprompt/main.qml index 08f43fda..c42b7334 100644 --- a/usr/share/lipstick-windowprompt/main.qml +++ b/usr/share/lipstick-windowprompt/main.qml @@ -5,7 +5,7 @@ * License: Proprietary */ -import QtQuick 2.0 +import QtQuick 2.6 import QtQuick.Window 2.0 import Sailfish.Silica 1.0 import com.jolla.lipstick 0.1 @@ -18,7 +18,7 @@ ApplicationWindow { property string _componentName function singleShot(timeout, callback) { - var timer = Qt.createQmlObject("import QtQuick 2.0; Timer {}", root, "singleShot") + var timer = Qt.createQmlObject("import QtQuick 2.6; Timer {}", root, "singleShot") timer.interval = timeout timer.repeat = false timer.triggered.connect(callback) diff --git a/usr/share/lipstick/eventfeed/VKFeedItem.qml b/usr/share/lipstick/eventfeed/VKFeedItem.qml deleted file mode 100644 index 244b3d5e..00000000 --- a/usr/share/lipstick/eventfeed/VKFeedItem.qml +++ /dev/null @@ -1,176 +0,0 @@ -/**************************************************************************** - ** - ** Copyright (C) 2014 - 2016 Jolla Ltd. - ** Copyright (C) 2020 Open Mobile Platform LLC. - ** - ****************************************************************************/ - -import QtQuick 2.0 -import Sailfish.Silica 1.0 -import Sailfish.TextLinking 1.0 -import "shared" - -SocialMediaFeedItem { - id: item - - contentHeight: Math.max(content.height, avatar.height) + Theme.paddingMedium * 3 - width: parent.width - avatarSource: model.icon - - property variant imageList - property bool isRepost: repostType.visible - property string formattedRepostTime - - onRefreshTimeCountChanged: formattedRepostTime = Format.formatDate(model.repostTimestamp, Format.DurationElapsed) - - Column { - id: content - x: item.avatar.width + Theme.paddingMedium - y: item.topMargin - width: parent.width - x - Theme.horizontalPageMargin - - SocialMediaPreviewRow { - downloader: item.downloader - imageList: item.imageList - connectedToNetwork: item.connectedToNetwork - eventsColumnMaxWidth: item.eventsColumnMaxWidth - item.avatar.width - } - - Label { - width: parent.width - truncationMode: TruncationMode.Fade - text: model.name - textFormat: Text.PlainText - } - - Label { - id: repostType - width: parent.width - truncationMode: TruncationMode.Fade - opacity: .6 - text: item.repostTypeText(model.repostType) - font.pixelSize: Theme.fontSizeSmall - color: Theme.primaryColor - visible: text !== "" - textFormat: Text.PlainText - } - - LinkedText { - width: parent.width - maximumLineCount: 15 - elide: Text.ElideRight - wrapMode: Text.Wrap - font.pixelSize: Theme.fontSizeSmall - shortenUrl: true - linkColor: Theme.highlightColor - plainText: model.body - visible: plainText !== "" - } - - Text { - width: parent.width - maximumLineCount: 1 - elide: Text.ElideRight - wrapMode: Text.Wrap - color: Theme.highlightColor - font.pixelSize: Theme.fontSizeSmall - text: item.formattedTime - } - - Column { - width: parent.width - visible: item.isRepost - spacing: Theme.paddingSmall - - Item { - width: 1 - height: Theme.paddingLarge - } - - Item { - width: parent.width - height: repostIcon.height - - Image { - id: repostIcon - source: "image://theme/icon-s-repost" + (item.highlighted ? "?" + Theme.highlightColor : "") - } - - Text { - anchors { - left: repostIcon.right - leftMargin: Theme.paddingMedium - right: parent.right - verticalCenter: repostIcon.verticalCenter - } - elide: Text.ElideRight - opacity: .6 - text: model.repostOwnerName - font.pixelSize: Theme.fontSizeSmall - color: Theme.primaryColor - textFormat: Text.PlainText - } - } - - Item { - width: 1 - height: Theme.paddingSmall - } - - SocialMediaPreviewRow { - downloader: item.downloader - imageList: model.repostImages - connectedToNetwork: item.connectedToNetwork - eventsColumnMaxWidth: item.eventsColumnMaxWidth - item.avatar.width - } - - LinkedText { - width: parent.width - maximumLineCount: 15 - elide: Text.ElideRight - wrapMode: Text.Wrap - font.pixelSize: Theme.fontSizeSmall - shortenUrl: true - color: item.pressed ? Theme.highlightColor : Theme.primaryColor - linkColor: Theme.highlightColor - plainText: model.repostText - visible: plainText !== "" - } - - Text { - text: item.formattedRepostTime - maximumLineCount: 1 - elide: Text.ElideRight - wrapMode: Text.Wrap - color: item.highlighted ? Theme.secondaryHighlightColor : Theme.highlightColor - font.pixelSize: Theme.fontSizeExtraSmall - textFormat: Text.PlainText - } - } - } - - function repostTypeText(repostType) { - if (repostType !== "") { - switch (repostType) { - case "link": - //: User shared a link in VK - //% "Shared link" - return qsTrId("lipstick-jolla-home-la-vk_shared_link") - case "video": - //: User shared a video in VK - //% "Shared video" - return qsTrId("lipstick-jolla-home-la-vk_shared_video") - case "photo": - //: User shared a photo in VK - //% "Shared photo" - return qsTrId("lipstick-jolla-home-la-vk_shared_photo") - } - - //: User shared a post in VK - //% "Shared post" - return qsTrId("lipstick-jolla-home-la-vk_shared_post") - } - - return "" - } -} diff --git a/usr/share/lipstick/eventfeed/shared/SocialMediaFeedItem.qml b/usr/share/lipstick/eventfeed/shared/SocialMediaFeedItem.qml index 0d1e387d..75966bca 100644 --- a/usr/share/lipstick/eventfeed/shared/SocialMediaFeedItem.qml +++ b/usr/share/lipstick/eventfeed/shared/SocialMediaFeedItem.qml @@ -45,7 +45,7 @@ NotificationGroupMember { } } - onRefreshTimeCountChanged: formattedTime = Format.formatDate(timestamp, Format.DurationElapsed) + onRefreshTimeCountChanged: formattedTime = Format.formatDate(timestamp, Format.TimeElapsed) SocialAvatar { id: _avatar diff --git a/usr/share/lipstick/eventfeed/vk-delegate.qml b/usr/share/lipstick/eventfeed/vk-delegate.qml deleted file mode 100644 index 48e99b2a..00000000 --- a/usr/share/lipstick/eventfeed/vk-delegate.qml +++ /dev/null @@ -1,63 +0,0 @@ -/**************************************************************************** - ** - ** Copyright (C) 2014 - 2015 Jolla Ltd. - ** Copyright (C) 2020 Open Mobile Platform LLC. - ** - ****************************************************************************/ - -import QtQuick 2.0 -import Sailfish.Silica 1.0 -import org.nemomobile.social 1.0 -import org.nemomobile.socialcache 1.0 -import "shared" - -SocialMediaAccountDelegate { - id: delegateItem - - //: VK News - //% "News" - headerText: qsTrId("lipstick-jolla-home-la-vk_posts") - headerIcon: "image://theme/graphic-service-vk" - - services: ["Posts", "Notifications"] - socialNetwork: SocialSync.VK - dataType: SocialSync.Notifications - - model: VKPostsModel {} - - delegate: VKFeedItem { - downloader: delegateItem.downloader - imageList: model.images - accountId: model.accounts[0] - userRemovable: true - animateRemoval: defaultAnimateRemoval || delegateItem.removeAllInProgress - - onRemoveRequested: { - delegateItem.model.remove(model.vkId) - } - - onTriggered: { - Qt.openUrlExternally(model.link) - } - - Component.onCompleted: { - refreshTimeCount = Qt.binding(function() { return delegateItem.refreshTimeCount }) - connectedToNetwork = Qt.binding(function() { return delegateItem.connectedToNetwork }) - eventsColumnMaxWidth = Qt.binding(function() { return delegateItem.eventsColumnMaxWidth }) - } - } - - //% "Show more in VK" - expandedLabel: qsTrId("lipstick-jolla-home-la-show-more-in-vk") - userRemovable: true - - onHeaderClicked: Qt.openUrlExternally("https://m.vk.com/feed") - onExpandedClicked: Qt.openUrlExternally("https://m.vk.com/feed") - - onViewVisibleChanged: { - if (viewVisible) { - delegateItem.resetHasSyncableAccounts() - delegateItem.model.refresh() - } - } -} diff --git a/usr/share/maliit/plugins/com/jolla/ContextAwareCommaKey.qml b/usr/share/maliit/plugins/com/jolla/ContextAwareCommaKey.qml index 1fdb04a5..972bf66b 100644 --- a/usr/share/maliit/plugins/com/jolla/ContextAwareCommaKey.qml +++ b/usr/share/maliit/plugins/com/jolla/ContextAwareCommaKey.qml @@ -11,7 +11,7 @@ CharacterKey { captionShifted: caption symView: "," symView2: "," - implicitWidth: punctuationKeyWidthNarrow + implicitWidth: punctuationKeyWidth fixedWidth: !splitActive separator: SeparatorState.HiddenSeparator } diff --git a/usr/share/maliit/plugins/com/jolla/FunctionKey.qml b/usr/share/maliit/plugins/com/jolla/FunctionKey.qml index 688564d1..5eb89dd8 100644 --- a/usr/share/maliit/plugins/com/jolla/FunctionKey.qml +++ b/usr/share/maliit/plugins/com/jolla/FunctionKey.qml @@ -70,7 +70,7 @@ KeyBase { fontSizeMode: Text.HorizontalFit horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter - font.pixelSize: Theme.fontSizeMedium + font.pixelSize: Theme.fontSizeSmall font.family: Theme.fontFamily text: parent.caption } diff --git a/usr/share/maliit/plugins/com/jolla/HorizontalPredictionListView.qml b/usr/share/maliit/plugins/com/jolla/HorizontalPredictionListView.qml index 6f6ffc61..03830812 100644 --- a/usr/share/maliit/plugins/com/jolla/HorizontalPredictionListView.qml +++ b/usr/share/maliit/plugins/com/jolla/HorizontalPredictionListView.qml @@ -69,6 +69,7 @@ PredictionListView { IconButton { id: removeButton + x: label.x + label.width height: delegate.height width: delegate.height diff --git a/usr/share/maliit/plugins/com/jolla/HwrInputHandler.qml b/usr/share/maliit/plugins/com/jolla/HwrInputHandler.qml new file mode 100644 index 00000000..d945be1c --- /dev/null +++ b/usr/share/maliit/plugins/com/jolla/HwrInputHandler.qml @@ -0,0 +1,134 @@ +import QtQuick 2.0 +import com.meego.maliitquick 1.0 +import Sailfish.Silica 1.0 +import com.jolla.hwr 1.0 +import com.jolla.keyboard 1.0 + +InputHandler { + id: hwrHandler + + property string inputMode: keyboard.layout ? keyboard.layout.inputMode : "" + property string preedit: HwrModel.primaryCandidate + + onInputModeChanged: HwrModel.inputMode = inputMode + + onPreeditChanged: { + if (active) { + MInputMethodQuick.sendPreedit(preedit) + } + } + + Component { + id: pasteComponent + PasteButton { + onClicked: { + if (preedit !== "") { + MInputMethodQuick.sendCommit(preedit) + } + MInputMethodQuick.sendCommit(Clipboard.text) + HwrModel.clear() + keyboard.expandedPaste = false + } + } + } + + topItem: Component { + TopItem { + id: topItem + height: Theme.itemSizeSmall + + Rectangle { + height: parent.height + width: parent.width + color: Theme.rgba(Theme.highlightBackgroundColor, .05) + + ListView { + id: listView + model: HwrModel + orientation: ListView.Horizontal + anchors.fill: parent + header: pasteComponent + boundsBehavior: !keyboard.expandedPaste && Clipboard.hasText ? Flickable.DragOverBounds : Flickable.StopAtBounds + + onDraggingChanged: { + if (!dragging && !keyboard.expandedPaste && contentX < -(headerItem.width + Theme.paddingLarge)) { + keyboard.expandedPaste = true + positionViewAtBeginning() + } + } + + delegate: BackgroundItem { + onClicked: hwrHandler.applyCandidate(model.text) + width: candidateText.width + Theme.paddingLarge * 2 + height: topItem.height + + Text { + id: candidateText + anchors.centerIn: parent + color: highlighted ? Theme.highlightColor : Theme.primaryColor + font { pixelSize: Theme.fontSizeSmall; family: Theme.fontFamily } + text: model.text + } + } + } + + Connections { + target: Clipboard + onTextChanged: { + if (Clipboard.hasText) { + // need to have updated width before repositioning view + positionerTimer.restart() + } + } + } + + Connections { + target: HwrModel + onModelReset: keyboard.expandedPaste = false + onCharacterStarted: { + if (hwrHandler.preedit !== "") { + MInputMethodQuick.sendCommit(hwrHandler.preedit) + } + } + } + + Timer { + id: positionerTimer + interval: 10 + onTriggered: listView.positionViewAtBeginning() + } + } + } + } + + onActiveChanged: { + if (!active && preedit !== "") { + MInputMethodQuick.sendCommit(preedit) + HwrModel.clear() + } + } + + function handleKeyClick() { + keyboard.expandedPaste = false + if (preedit !== "") { + if (pressedKey.key === Qt.Key_Space || pressedKey.key === Qt.Key_Return) { + MInputMethodQuick.sendCommit(preedit) + HwrModel.clear() + return true + } else if (pressedKey.key === Qt.Key_Backspace) { + MInputMethodQuick.sendPreedit("") + HwrModel.clear() + return true + } + } + return false + } + + function applyCandidate(text) { + MInputMethodQuick.sendCommit(text) + HwrModel.clear() + if (canvas.phraseEngine) { + HwrModel.setPhraseCandidates(canvas.phraseEngine.phraseCandidates(text)) + } + } +} diff --git a/usr/share/maliit/plugins/com/jolla/HwrLayout.qml b/usr/share/maliit/plugins/com/jolla/HwrLayout.qml index 98f67924..12919a8e 100644 --- a/usr/share/maliit/plugins/com/jolla/HwrLayout.qml +++ b/usr/share/maliit/plugins/com/jolla/HwrLayout.qml @@ -173,7 +173,7 @@ KeyboardLayout { } IconButton { - visible: hwrCanvas.visible + visible: hwrCanvas.visible && !MInputMethodQuick.extensions.keyboardClosingDisabled anchors { right: parent.right top: parent.top @@ -297,7 +297,7 @@ KeyboardLayout { PasteButton { previewWidthLimit: geometry.hwrPastePreviewWidth - visible: !keyboard.inSymView && Clipboard.hasText + visible: !keyboard.inSymView && keyboard.pasteEnabled popupAnchor: 1 // = right anchors { right: parent.right diff --git a/usr/share/maliit/plugins/com/jolla/InputHandler.qml b/usr/share/maliit/plugins/com/jolla/InputHandler.qml index 0fdc0d88..8cab48f8 100644 --- a/usr/share/maliit/plugins/com/jolla/InputHandler.qml +++ b/usr/share/maliit/plugins/com/jolla/InputHandler.qml @@ -123,7 +123,9 @@ Silica.SilicaItem { MInputMethodQuick.sendKey(Qt.Key_Backspace, 0, "\b", Maliit.KeyClick) } else if (pressedKey.key === Qt.Key_Paste) { - MInputMethodQuick.sendCommit(Silica.Clipboard.text) + if (keyboard.pasteEnabled) { + MInputMethodQuick.sendCommit(Silica.Clipboard.text) + } } else { resetShift = false } diff --git a/usr/share/maliit/plugins/com/jolla/KeyboardBase.qml b/usr/share/maliit/plugins/com/jolla/KeyboardBase.qml index 1f966dee..c4e3a200 100644 --- a/usr/share/maliit/plugins/com/jolla/KeyboardBase.qml +++ b/usr/share/maliit/plugins/com/jolla/KeyboardBase.qml @@ -33,7 +33,7 @@ import Sailfish.Silica 1.0 import Sailfish.Silica.Background 1.0 import com.meego.maliitquick 1.0 import com.jolla.keyboard 1.0 -import org.nemomobile.configuration 1.0 +import Nemo.Configuration 1.0 import "touchpointarray.js" as ActivePoints PagedView { @@ -61,6 +61,13 @@ PagedView { property bool inSymView2 // allow chinese input handler to override enter key state property bool chineseOverrideForEnter + property bool pasteEnabled: !_pasteDisabled && Clipboard.hasText + property bool _pasteDisabled + Binding on _pasteDisabled { + // avoid change when keyboard is hiding + when: MInputMethodQuick.active + value: !!MInputMethodQuick.extensions.pasteDisabled + } property bool silenceFeedback property bool layoutChangeAllowed @@ -106,23 +113,23 @@ PagedView { } } - delegate: Loader { - id: layoutLoader + delegate: Item { + id: layoutDelegate - readonly property bool exposed: status === Loader.Ready && PagedView.exposed - readonly property bool current: status === Loader.Ready && PagedView.isCurrentItem + property Item loadedLayout: layoutLoader.item + property Item loader: layoutLoader + readonly property bool exposed: layoutLoader.status === Loader.Ready && PagedView.exposed + readonly property bool current: layoutLoader.status === Loader.Ready && PagedView.isCurrentItem width: keyboard.width - height: status === Loader.Error ? Theme.itemSizeHuge : implicitHeight - - source: keyboard.sourceDirectory + model.file + height: layoutLoader.height onExposedChanged: { // Reset the layout keyboard state when it is dragged into view. - var attributes = exposed && !PagedView.isCurrentItem ? item.attributes : null + var attributes = exposed && !PagedView.isCurrentItem ? layoutLoader.item.attributes : null if (attributes) { - attributes.isShifted = keyboard.shouldUseAutocaps(item) + attributes.isShifted = keyboard.shouldUseAutocaps(layoutLoader.item) attributes.inSymView = false attributes.inSymView2 = false attributes.isShiftLocked = false @@ -130,7 +137,7 @@ PagedView { } onCurrentChanged: { - var attributes = item.attributes + var attributes = layoutLoader.item.attributes if (current) { // Bind to the active keyboad state when made the current layout. @@ -149,16 +156,24 @@ PagedView { } KeyboardBackground { - z: -1 - width: layoutLoader.width - height: layoutLoader.height - + width: layoutDelegate.width + height: layoutDelegate.height transformItem: keyboard } + + Loader { + id: layoutLoader + + anchors.horizontalCenter: parent.horizontalCenter + width: keyboard.portraitMode ? keyboard.width : geometry.keyboardWidthLandscape + height: status === Loader.Error ? Theme.itemSizeHuge : implicitHeight + source: keyboard.sourceDirectory + model.file + } } Popper { id: popper + z: 10 target: lastPressedKey onExpandedChanged: { @@ -185,6 +200,7 @@ PagedView { Timer { id: languageSwitchTimer + interval: 500 onTriggered: { if (canvas.layoutModel.enabledCount > 1) { @@ -196,6 +212,7 @@ PagedView { Timer { id: autocapsTimer + interval: 1 onTriggered: applyAutocaps() } @@ -240,15 +257,22 @@ PagedView { anchors.fill: parent z: -1 - onPressed: keyboard.handlePressed(createPointArray(mouse.x, mouse.y)) + onPressed: { + startX = mouse.x + startY = mouse.y + keyboard.handlePressed(createPointArray(mouse.x, mouse.y)) + } onPositionChanged: keyboard.handleUpdated(createPointArray(mouse.x, mouse.y)) onReleased: keyboard.handleReleased(createPointArray(mouse.x, mouse.y)) onCanceled: keyboard.cancelAllTouchPoints() + property real startX + property real startY + function createPointArray(pointX, pointY) { var pointArray = new Array pointArray.push({"pointId": 1, "x": pointX, "y": pointY, - "startX": pointX, "startY": pointY }) + "startX": startX, "startY": startY }) return pointArray } } @@ -295,6 +319,10 @@ PagedView { var point = ActivePoints.addPoint(touchPoints[i]) updatePressedKey(point) } + + if (ActivePoints.array.length > 1) { + keyboard.interactive = false // disable keyboard drag until all the touchpoints are released + } } function handleUpdated(touchPoints) { @@ -320,7 +348,7 @@ PagedView { mouseArea.preventStealing = true } - if (yDiff > closeSwipeThreshold) { + if (yDiff > closeSwipeThreshold && !MInputMethodQuick.extensions.keyboardClosingDisabled) { // swiped down to close keyboard MInputMethodQuick.userHide() if (point.pressedKey) { @@ -450,9 +478,9 @@ PagedView { var item = layout var current = currentItem - if (current && current.item === layout) { - x -= current.x - y -= current.y + if (current && current.loadedLayout === layout) { + x -= current.x + current.loader.x + y -= current.y + current.loader.y } else { x -= item.x y -= item.y diff --git a/usr/share/maliit/plugins/com/jolla/KeyboardGeometry.qml b/usr/share/maliit/plugins/com/jolla/KeyboardGeometry.qml index 4cdd00d9..ef69d254 100644 --- a/usr/share/maliit/plugins/com/jolla/KeyboardGeometry.qml +++ b/usr/share/maliit/plugins/com/jolla/KeyboardGeometry.qml @@ -10,7 +10,23 @@ QtObject { property real scaleRatio: isLargeScreen ? screen.width / 580 : screen.width / 480 property real verticalScale: isLargeScreen ? screen.width / 768 : scaleRatio - property int keyboardWidthLandscape: screen.height + // extra paddings horizontally or vertically to avoid overlapping rounded corners + // using the biggest to keep symmetry + property int cornerPadding: { + // assuming the roundings are simple with x and y detached the radius amount from edges. + var biggestCorner = Math.max(Screen.topLeftCorner.radius, + Screen.topRightCorner.radius, + Screen.bottomLeftCorner.radius, + Screen.bottomRightCorner.radius) + // 0.7 assumed being enough of the rounding to avoid + return biggestCorner * 0.7 + } + + property int keyboardWidthLandscape: { + var avoidance = Math.max(Screen.topCutout.height, cornerPadding) + // avoiding in both sides to keep symmetry + return screen.height - (avoidance * 2) + } property int keyboardWidthPortrait: screen.width property int keyHeightLandscape: isLargeScreen ? keyHeightPortrait : 58*verticalScale @@ -21,17 +37,15 @@ QtObject { property int shiftKeyWidthLandscape: 110*scaleRatio property int shiftKeyWidthLandscapeNarrow: 98*scaleRatio property int shiftKeyWidthLandscapeSplit: 77*scaleRatio - property int punctuationKeyLandscape: 120*scaleRatio - property int punctuationKeyLandscapeNarrow: 80*scaleRatio - property int symbolKeyWidthLandscapeNarrow: 145*scaleRatio + property int punctuationKeyLandscape: 80*scaleRatio + property int symbolKeyWidthLandscapeNarrow: functionKeyWidthLandscape property int symbolKeyWidthLandscapeNarrowSplit: 100*scaleRatio - property int functionKeyWidthPortrait: 116*scaleRatio + property int functionKeyWidthPortrait: 95*scaleRatio property int shiftKeyWidthPortrait: 72*scaleRatio property int shiftKeyWidthPortraitNarrow: 60*scaleRatio - property int punctuationKeyPortait: 56*scaleRatio - property int punctuationKeyPortraitNarrow: 43*scaleRatio // 3*narrow + symbol narrow == 2*non-narrow + function key - property int symbolKeyWidthPortraitNarrow: 99*scaleRatio + property int punctuationKeyPortait: 43*scaleRatio + property int symbolKeyWidthPortraitNarrow: functionKeyWidthPortrait property int middleBarWidth: keyboardWidthLandscape / 4 diff --git a/usr/share/maliit/plugins/com/jolla/KeyboardLayout.qml b/usr/share/maliit/plugins/com/jolla/KeyboardLayout.qml index ef11ceaf..f39eaf90 100644 --- a/usr/share/maliit/plugins/com/jolla/KeyboardLayout.qml +++ b/usr/share/maliit/plugins/com/jolla/KeyboardLayout.qml @@ -1,7 +1,7 @@ // Copyright (C) 2013 Jolla Ltd. // Contact: Pekka Vuorela -import QtQuick 2.0 +import QtQuick 2.6 import Sailfish.Silica 1.0 import com.meego.maliitquick 1.0 @@ -9,12 +9,14 @@ Column { id: keyboardLayout width: parent ? parent.width : 0 + bottomPadding: portraitMode ? Math.max(MInputMethodQuick.appOrientation === 180 ? Screen.topCutout.height : 0, + geometry.cornerPadding) + : 0 property string type: model ? model.type : "" property bool portraitMode property int keyHeight property int punctuationKeyWidth - property int punctuationKeyWidthNarrow property int shiftKeyWidth property int functionKeyWidth property int shiftKeyWidthNarrow @@ -90,7 +92,7 @@ Column { sourceComponent: active && keyboardLayout.handler ? keyboardLayout.handler.topItem : null width: parent.width visible: active - clip: keyboard.moving + clip: keyboard.moving || keyboard.hasHorizontalPadding asynchronous: false opacity: (canvas.activeIndex === keyboardLayout.layoutIndex) ? 1.0 : 0.0 Behavior on opacity { FadeAnimation {}} @@ -104,7 +106,6 @@ Column { if (portraitMode) { keyHeight = geometry.keyHeightPortrait punctuationKeyWidth = geometry.punctuationKeyPortait - punctuationKeyWidthNarrow = geometry.punctuationKeyPortraitNarrow shiftKeyWidth = geometry.shiftKeyWidthPortrait functionKeyWidth = geometry.functionKeyWidthPortrait shiftKeyWidthNarrow = geometry.shiftKeyWidthPortraitNarrow @@ -114,7 +115,6 @@ Column { } else { keyHeight = geometry.keyHeightLandscape punctuationKeyWidth = geometry.punctuationKeyLandscape - punctuationKeyWidthNarrow = geometry.punctuationKeyLandscapeNarrow functionKeyWidth = geometry.functionKeyWidthLandscape var shouldSplit = keyboard.splitEnabled && splitSupported diff --git a/usr/share/maliit/plugins/com/jolla/KeyboardLayoutSwitchHint.qml b/usr/share/maliit/plugins/com/jolla/KeyboardLayoutSwitchHint.qml index 0dc642dc..95da7452 100644 --- a/usr/share/maliit/plugins/com/jolla/KeyboardLayoutSwitchHint.qml +++ b/usr/share/maliit/plugins/com/jolla/KeyboardLayoutSwitchHint.qml @@ -80,6 +80,7 @@ Loader { FirstTimeUseCounter { id: firstTimeUseCounter + limit: 2 defaultValue: 0 key: "/sailfish/text_input/switch_keyboard_hint_count" diff --git a/usr/share/maliit/plugins/com/jolla/LanguageSelectionCell.qml b/usr/share/maliit/plugins/com/jolla/LanguageSelectionCell.qml index 9b8acf65..1dfe4056 100644 --- a/usr/share/maliit/plugins/com/jolla/LanguageSelectionCell.qml +++ b/usr/share/maliit/plugins/com/jolla/LanguageSelectionCell.qml @@ -16,6 +16,7 @@ SilicaItem { Label { id: textItem + anchors.centerIn: parent color: selectionCell.active ? selectionCell.palette.primaryColor : selectionCell.palette.secondaryColor diff --git a/usr/share/maliit/plugins/com/jolla/LanguageSelectionPopup.qml b/usr/share/maliit/plugins/com/jolla/LanguageSelectionPopup.qml index 7670274a..5492836c 100644 --- a/usr/share/maliit/plugins/com/jolla/LanguageSelectionPopup.qml +++ b/usr/share/maliit/plugins/com/jolla/LanguageSelectionPopup.qml @@ -23,12 +23,14 @@ Rectangle { Column { id: contentColumn + width: parent.width anchors.verticalCenter: parent.verticalCenter } NumberAnimation on height { id: openAnimation + duration: 100 easing.type: Easing.OutQuad to: popup.targetHeight + Theme.paddingLarge @@ -44,6 +46,7 @@ Rectangle { SequentialAnimation { id: fadeAnimation + NumberAnimation { duration: 100 to: 0 diff --git a/usr/share/maliit/plugins/com/jolla/NumberLayoutLandscape.qml b/usr/share/maliit/plugins/com/jolla/NumberLayoutLandscape.qml index 59881d6c..8488c89e 100644 --- a/usr/share/maliit/plugins/com/jolla/NumberLayoutLandscape.qml +++ b/usr/share/maliit/plugins/com/jolla/NumberLayoutLandscape.qml @@ -10,9 +10,6 @@ KeyboardLayout { property real keyWidth: width / 10 - width: geometry.keyboardWidthLandscape - height: 2 * geometry.keyHeightPortrait - layoutIndex: -1 type: "" handler: null @@ -68,7 +65,7 @@ KeyboardLayout { NumberKey { width: main.keyWidth - enabled: Silica.Clipboard.hasText + enabled: keyboard.pasteEnabled opacity: enabled ? (pressed ? 0.6 : 1.0) : 0.3 key: Qt.Key_Paste diff --git a/usr/share/maliit/plugins/com/jolla/NumberLayoutPortrait.qml b/usr/share/maliit/plugins/com/jolla/NumberLayoutPortrait.qml index 65ad4cee..e8675f09 100644 --- a/usr/share/maliit/plugins/com/jolla/NumberLayoutPortrait.qml +++ b/usr/share/maliit/plugins/com/jolla/NumberLayoutPortrait.qml @@ -11,7 +11,6 @@ KeyboardLayout { property real keyWidth: width / 4 portraitMode: true - height: 4 * geometry.keyHeightPortrait layoutIndex: -1 type: "" @@ -33,7 +32,7 @@ KeyboardLayout { } NumberKey { width: main.keyWidth - enabled: Silica.Clipboard.hasText + enabled: keyboard.pasteEnabled separator: SeparatorState.HiddenSeparator opacity: enabled ? (pressed ? 0.6 : 1.0) : 0.3 diff --git a/usr/share/maliit/plugins/com/jolla/PasteButtonBase.qml b/usr/share/maliit/plugins/com/jolla/PasteButtonBase.qml index d0fc8328..2c05a7a9 100644 --- a/usr/share/maliit/plugins/com/jolla/PasteButtonBase.qml +++ b/usr/share/maliit/plugins/com/jolla/PasteButtonBase.qml @@ -12,10 +12,12 @@ BackgroundItem { property alias popupParent: popup.parent height: parent ? parent.height : 0 - width: Clipboard.hasText ? (keyboard.expandedPaste ? pasteRow.width + 2*Theme.paddingMedium - : pasteIcon.width + Theme.paddingMedium) - : 0 + width: keyboard.pasteEnabled + ? (keyboard.expandedPaste ? pasteRow.width + 2*Theme.paddingMedium + : pasteIcon.width + Theme.paddingMedium) + : 0 + visible: keyboard.pasteEnabled preventStealing: popup.visible highlighted: down || popup.visible diff --git a/usr/share/maliit/plugins/com/jolla/PasteInputHandler.qml b/usr/share/maliit/plugins/com/jolla/PasteInputHandler.qml index 7de29191..f5447b5f 100644 --- a/usr/share/maliit/plugins/com/jolla/PasteInputHandler.qml +++ b/usr/share/maliit/plugins/com/jolla/PasteInputHandler.qml @@ -22,7 +22,9 @@ InputHandler { } onPaste: { - MInputMethodQuick.sendCommit(Clipboard.text) + if (keyboard.pasteEnabled) { + MInputMethodQuick.sendCommit(Clipboard.text) + } } topItem: Component { diff --git a/usr/share/maliit/plugins/com/jolla/PeriodKey.qml b/usr/share/maliit/plugins/com/jolla/PeriodKey.qml index da5cfef9..fc784047 100644 --- a/usr/share/maliit/plugins/com/jolla/PeriodKey.qml +++ b/usr/share/maliit/plugins/com/jolla/PeriodKey.qml @@ -7,7 +7,7 @@ CharacterKey { captionShifted: "." accents: "!.?" accentsShifted: "!.?" - implicitWidth: punctuationKeyWidthNarrow + implicitWidth: punctuationKeyWidth fixedWidth: !splitActive separator: SeparatorState.HiddenSeparator } diff --git a/usr/share/maliit/plugins/com/jolla/PhoneNumberLayoutLandscape.qml b/usr/share/maliit/plugins/com/jolla/PhoneNumberLayoutLandscape.qml index a3345bd7..6ae093eb 100644 --- a/usr/share/maliit/plugins/com/jolla/PhoneNumberLayoutLandscape.qml +++ b/usr/share/maliit/plugins/com/jolla/PhoneNumberLayoutLandscape.qml @@ -8,8 +8,7 @@ import com.jolla.keyboard 1.0 KeyboardLayout { id: main - width: geometry.keyboardWidthLandscape - height: 2 * geometry.keyHeightPortrait + property real keyWidth: width / 10 layoutIndex: -1 type: "" @@ -20,80 +19,80 @@ KeyboardLayout { CharacterKey { caption: "1" height: geometry.keyHeightPortrait - width: main.width / 10 + width: main.keyWidth showPopper: false } PhoneKey { caption: "2" secondaryLabel: "abc" height: geometry.keyHeightPortrait - width: main.width / 10 + width: main.keyWidth landscape: true } PhoneKey { caption: "3" secondaryLabel: "def" height: geometry.keyHeightPortrait - width: main.width / 10 + width: main.keyWidth landscape: true } PhoneKey { caption: "4" secondaryLabel: "ghi" height: geometry.keyHeightPortrait - width: main.width / 10 + width: main.keyWidth landscape: true } PhoneKey { caption: "5" secondaryLabel: "jkl" height: geometry.keyHeightPortrait - width: main.width / 10 + width: main.keyWidth landscape: true } PhoneKey { caption: "6" secondaryLabel: "mno" height: geometry.keyHeightPortrait - width: main.width / 10 + width: main.keyWidth landscape: true } PhoneKey { caption: "7" secondaryLabel: "pqrs" height: geometry.keyHeightPortrait - width: main.width / 10 + width: main.keyWidth landscape: true } PhoneKey { caption: "8" secondaryLabel: "tuv" height: geometry.keyHeightPortrait - width: main.width / 10 + width: main.keyWidth landscape: true } PhoneKey { caption: "9" secondaryLabel: "wxyz" height: geometry.keyHeightPortrait - width: main.width / 10 + width: main.keyWidth landscape: true } CharacterKey { caption: "0" height: geometry.keyHeightPortrait - width: main.width / 10 + width: main.keyWidth showPopper: false separator: SeparatorState.HiddenSeparator } } Row { - x: 3 * (main.width / 10) + x: 3 * main.keyWidth NumberKey { - width: main.width / 10 - enabled: Silica.Clipboard.hasText + width: main.keyWidth + enabled: keyboard.pasteEnabled key: Qt.Key_Paste opacity: enabled ? (pressed ? 0.6 : 1.0) : 0.3 @@ -107,24 +106,24 @@ KeyboardLayout { text: "*p" caption: "*p" height: geometry.keyHeightPortrait - width: main.width / 10 + width: main.keyWidth showPopper: false } CharacterKey { caption: "+" height: geometry.keyHeightPortrait - width: main.width / 10 + width: main.keyWidth showPopper: false } CharacterKey { caption: "#" height: geometry.keyHeightPortrait - width: main.width / 10 + width: main.keyWidth showPopper: false separator: SeparatorState.HiddenSeparator } BackspaceKey { - width: main.width / 10 + width: main.keyWidth height: geometry.keyHeightPortrait } EnterKey { diff --git a/usr/share/maliit/plugins/com/jolla/PhoneNumberLayoutPortrait.qml b/usr/share/maliit/plugins/com/jolla/PhoneNumberLayoutPortrait.qml index b9884dcf..ad1fc500 100644 --- a/usr/share/maliit/plugins/com/jolla/PhoneNumberLayoutPortrait.qml +++ b/usr/share/maliit/plugins/com/jolla/PhoneNumberLayoutPortrait.qml @@ -9,7 +9,6 @@ KeyboardLayout { id: main portraitMode: true - height: 4 * geometry.keyHeightPortrait layoutIndex: -1 type: "" @@ -37,7 +36,7 @@ KeyboardLayout { } NumberKey { separator: SeparatorState.HiddenSeparator - enabled: Silica.Clipboard.hasText + enabled: keyboard.pasteEnabled key: Qt.Key_Paste width: main.width / 4 opacity: enabled ? (pressed ? 0.6 : 1.0) diff --git a/usr/share/maliit/plugins/com/jolla/PopperCell.qml b/usr/share/maliit/plugins/com/jolla/PopperCell.qml index ac5a30c9..a8be5894 100644 --- a/usr/share/maliit/plugins/com/jolla/PopperCell.qml +++ b/usr/share/maliit/plugins/com/jolla/PopperCell.qml @@ -7,6 +7,7 @@ import com.jolla.keyboard 1.0 SilicaItem { id: popperCell + width: geometry.accentPopperCellWidth height: geometry.popperHeight @@ -16,6 +17,7 @@ SilicaItem { Label { id: textItem + anchors.top: parent.top anchors.bottom: parent.bottom anchors.horizontalCenter: parent.horizontalCenter diff --git a/usr/share/maliit/plugins/com/jolla/SpacebarKey.qml b/usr/share/maliit/plugins/com/jolla/SpacebarKey.qml index 9b71b405..5e2e2c04 100644 --- a/usr/share/maliit/plugins/com/jolla/SpacebarKey.qml +++ b/usr/share/maliit/plugins/com/jolla/SpacebarKey.qml @@ -28,6 +28,7 @@ CharacterKey { Rectangle { id: background + color: parent.pressed ? characterKey.palette.highlightBackgroundColor : characterKey.palette.primaryColor opacity: parent.pressed ? _pressedOpacity : _normalOpacity radius: geometry.keyRadius @@ -38,6 +39,7 @@ CharacterKey { Label { id: textField + x: Theme.paddingMedium + 2 width: parent.width - 2*x height: parent.height @@ -127,7 +129,6 @@ CharacterKey { destroy() } } - } } } diff --git a/usr/share/maliit/plugins/com/jolla/SpacebarRowDeadKey.qml b/usr/share/maliit/plugins/com/jolla/SpacebarRowDeadKey.qml index 7019e949..0b70bea8 100644 --- a/usr/share/maliit/plugins/com/jolla/SpacebarRowDeadKey.qml +++ b/usr/share/maliit/plugins/com/jolla/SpacebarRowDeadKey.qml @@ -19,12 +19,13 @@ KeyboardRow { } DeadKey { id: deadKey - implicitWidth: punctuationKeyWidthNarrow + + implicitWidth: punctuationKeyWidth fixedWidth: !splitActive separator: SeparatorState.HiddenSeparator } ContextAwareCommaKey { - implicitWidth: punctuationKeyWidthNarrow + implicitWidth: punctuationKeyWidth } SpacebarKey {} SpacebarKey { @@ -33,8 +34,9 @@ KeyboardRow { } PeriodKey { id: periodKey + accentsShifted: accents - implicitWidth: punctuationKeyWidthNarrow + implicitWidth: punctuationKeyWidth } EnterKey {} } diff --git a/usr/share/maliit/plugins/com/jolla/TopItem.qml b/usr/share/maliit/plugins/com/jolla/TopItem.qml index 19e910af..2daaf51e 100644 --- a/usr/share/maliit/plugins/com/jolla/TopItem.qml +++ b/usr/share/maliit/plugins/com/jolla/TopItem.qml @@ -35,5 +35,9 @@ import com.meego.maliitquick 1.0 CloseGestureArea { height: Theme.itemSizeSmall threshold: Math.max(keyboard.height*.3, Theme.itemSizeSmall) - onTriggered: MInputMethodQuick.userHide() + onTriggered: { + if (!MInputMethodQuick.extensions.keyboardClosingDisabled) { + MInputMethodQuick.userHide() + } + } } diff --git a/usr/share/maliit/plugins/com/jolla/VerticalPredictionListView.qml b/usr/share/maliit/plugins/com/jolla/VerticalPredictionListView.qml index 485f6e68..b9096673 100644 --- a/usr/share/maliit/plugins/com/jolla/VerticalPredictionListView.qml +++ b/usr/share/maliit/plugins/com/jolla/VerticalPredictionListView.qml @@ -13,8 +13,14 @@ PredictionListView { clip: true + Component.onCompleted: { + if (Clipboard.hasText) { + stateChange.restart() + } + } + onPredictionsChanged: { - if (!clipboardChange.running) { + if (!stateChange.running) { view.positionViewAtIndex(0, ListView.Beginning) } } @@ -90,11 +96,19 @@ PredictionListView { target: Clipboard onTextChanged: { verticalList.positionViewAtBeginning() - clipboardChange.restart() + stateChange.restart() + } + } + Connections { + target: MInputMethodQuick + onFocusTargetChanged: { + verticalList.positionViewAtBeginning() + stateChange.restart() } } + Timer { - id: clipboardChange + id: stateChange interval: 1000 } } diff --git a/usr/share/maliit/plugins/com/jolla/Xt9CpInputHandler.qml b/usr/share/maliit/plugins/com/jolla/Xt9CpInputHandler.qml index e40ea678..762d564d 100644 --- a/usr/share/maliit/plugins/com/jolla/Xt9CpInputHandler.qml +++ b/usr/share/maliit/plugins/com/jolla/Xt9CpInputHandler.qml @@ -3,7 +3,7 @@ import com.meego.maliitquick 1.0 import com.jolla.keyboard 1.0 import com.jolla.xt9cp 1.0 import Sailfish.Silica 1.0 -import org.nemomobile.configuration 1.0 +import Nemo.Configuration 1.0 InputHandler { id: handler diff --git a/usr/share/maliit/plugins/com/jolla/layouts/bg.qml b/usr/share/maliit/plugins/com/jolla/layouts/bg.qml index 23f41e4c..748030ae 100644 --- a/usr/share/maliit/plugins/com/jolla/layouts/bg.qml +++ b/usr/share/maliit/plugins/com/jolla/layouts/bg.qml @@ -89,11 +89,11 @@ KeyboardLayout { CharacterKey { caption: "-" captionShifted: "-" - implicitWidth: punctuationKeyWidthNarrow + implicitWidth: punctuationKeyWidth fixedWidth: !splitActive } ContextAwareCommaKey { - implicitWidth: punctuationKeyWidthNarrow + implicitWidth: punctuationKeyWidth } SpacebarKey {} SpacebarKey { @@ -101,7 +101,7 @@ KeyboardLayout { active: splitActive } PeriodKey { - implicitWidth: punctuationKeyWidthNarrow + implicitWidth: punctuationKeyWidth } EnterKey {} } diff --git a/usr/share/maliit/plugins/com/jolla/layouts/bn.qml b/usr/share/maliit/plugins/com/jolla/layouts/bn.qml index 110f8c25..0563cddb 100644 --- a/usr/share/maliit/plugins/com/jolla/layouts/bn.qml +++ b/usr/share/maliit/plugins/com/jolla/layouts/bn.qml @@ -88,7 +88,7 @@ KeyboardLayout { captionShifted: "." symView: "." symView2: "." - implicitWidth: punctuationKeyWidthNarrow + implicitWidth: punctuationKeyWidth fixedWidth: !splitActive } SmallCharacterKey { @@ -109,7 +109,7 @@ KeyboardLayout { captionShifted: "?" symView: "!" symView2: "!" - implicitWidth: punctuationKeyWidthNarrow + implicitWidth: punctuationKeyWidth fixedWidth: !splitActive separator: SeparatorState.HiddenSeparator } diff --git a/usr/share/maliit/plugins/com/jolla/layouts/fr.qml b/usr/share/maliit/plugins/com/jolla/layouts/fr.qml index e311b32e..2a9bddf7 100644 --- a/usr/share/maliit/plugins/com/jolla/layouts/fr.qml +++ b/usr/share/maliit/plugins/com/jolla/layouts/fr.qml @@ -105,11 +105,11 @@ KeyboardLayout { CharacterKey { caption: "'" captionShifted: "'" - implicitWidth: punctuationKeyWidthNarrow + implicitWidth: punctuationKeyWidth fixedWidth: !splitActive } ContextAwareCommaKey { - implicitWidth: punctuationKeyWidthNarrow + implicitWidth: punctuationKeyWidth } SpacebarKey {} SpacebarKey { @@ -117,7 +117,7 @@ KeyboardLayout { languageLabel: "" } PeriodKey { - implicitWidth: punctuationKeyWidthNarrow + implicitWidth: punctuationKeyWidth } EnterKey {} } diff --git a/usr/share/maliit/plugins/com/jolla/layouts/gu.qml b/usr/share/maliit/plugins/com/jolla/layouts/gu.qml index 0edb2ac7..51e1a23c 100644 --- a/usr/share/maliit/plugins/com/jolla/layouts/gu.qml +++ b/usr/share/maliit/plugins/com/jolla/layouts/gu.qml @@ -88,7 +88,7 @@ KeyboardLayout { captionShifted: "." symView: "." symView2: "." - implicitWidth: punctuationKeyWidthNarrow + implicitWidth: punctuationKeyWidth fixedWidth: !splitActive } SmallCharacterKey { @@ -109,7 +109,7 @@ KeyboardLayout { captionShifted: "?" symView: "!" symView2: "!" - implicitWidth: punctuationKeyWidthNarrow + implicitWidth: punctuationKeyWidth fixedWidth: !splitActive separator: SeparatorState.HiddenSeparator } diff --git a/usr/share/maliit/plugins/com/jolla/layouts/hi.qml b/usr/share/maliit/plugins/com/jolla/layouts/hi.qml index 66148ba3..1901b3f5 100644 --- a/usr/share/maliit/plugins/com/jolla/layouts/hi.qml +++ b/usr/share/maliit/plugins/com/jolla/layouts/hi.qml @@ -100,7 +100,7 @@ KeyboardLayout { captionShifted: "." symView: "." symView2: "." - implicitWidth: punctuationKeyWidthNarrow + implicitWidth: punctuationKeyWidth fixedWidth: !splitActive } DiaCharacterKey { @@ -121,7 +121,7 @@ KeyboardLayout { captionShifted: "?" symView: "!" symView2: "!" - implicitWidth: punctuationKeyWidthNarrow + implicitWidth: punctuationKeyWidth fixedWidth: !splitActive separator: SeparatorState.HiddenSeparator } diff --git a/usr/share/maliit/plugins/com/jolla/layouts/kk.qml b/usr/share/maliit/plugins/com/jolla/layouts/kk.qml index 64d5b8b9..8af4bb77 100644 --- a/usr/share/maliit/plugins/com/jolla/layouts/kk.qml +++ b/usr/share/maliit/plugins/com/jolla/layouts/kk.qml @@ -96,11 +96,11 @@ KeyboardLayout { CharacterKey { caption: "-" captionShifted: "-" - implicitWidth: punctuationKeyWidthNarrow + implicitWidth: punctuationKeyWidth fixedWidth: !splitActive } ContextAwareCommaKey { - implicitWidth: punctuationKeyWidthNarrow + implicitWidth: punctuationKeyWidth } SpacebarKey {} SpacebarKey { @@ -108,7 +108,7 @@ KeyboardLayout { active: splitActive } PeriodKey { - implicitWidth: punctuationKeyWidthNarrow + implicitWidth: punctuationKeyWidth } EnterKey {} } diff --git a/usr/share/maliit/plugins/com/jolla/layouts/kn.qml b/usr/share/maliit/plugins/com/jolla/layouts/kn.qml index 6d937396..c5c31458 100644 --- a/usr/share/maliit/plugins/com/jolla/layouts/kn.qml +++ b/usr/share/maliit/plugins/com/jolla/layouts/kn.qml @@ -100,7 +100,7 @@ KeyboardLayout { captionShifted: "." symView: "." symView2: "." - implicitWidth: punctuationKeyWidthNarrow + implicitWidth: punctuationKeyWidth fixedWidth: !splitActive } TinyCharacterKey { @@ -121,7 +121,7 @@ KeyboardLayout { captionShifted: "?" symView: "!" symView2: "!" - implicitWidth: punctuationKeyWidthNarrow + implicitWidth: punctuationKeyWidth fixedWidth: !splitActive separator: SeparatorState.HiddenSeparator } diff --git a/usr/share/maliit/plugins/com/jolla/layouts/ml.qml b/usr/share/maliit/plugins/com/jolla/layouts/ml.qml index 6aca47e3..8c1367d2 100644 --- a/usr/share/maliit/plugins/com/jolla/layouts/ml.qml +++ b/usr/share/maliit/plugins/com/jolla/layouts/ml.qml @@ -88,7 +88,7 @@ KeyboardLayout { captionShifted: "." symView: "." symView2: "." - implicitWidth: punctuationKeyWidthNarrow + implicitWidth: punctuationKeyWidth fixedWidth: !splitActive } TinyCharacterKey { @@ -109,7 +109,7 @@ KeyboardLayout { captionShifted: "?" symView: "!" symView2: "!" - implicitWidth: punctuationKeyWidthNarrow + implicitWidth: punctuationKeyWidth fixedWidth: !splitActive separator: SeparatorState.HiddenSeparator } diff --git a/usr/share/maliit/plugins/com/jolla/layouts/mr.qml b/usr/share/maliit/plugins/com/jolla/layouts/mr.qml index 8adf847e..7c5e7ed4 100644 --- a/usr/share/maliit/plugins/com/jolla/layouts/mr.qml +++ b/usr/share/maliit/plugins/com/jolla/layouts/mr.qml @@ -101,7 +101,7 @@ KeyboardLayout { captionShifted: "." symView: "." symView2: "." - implicitWidth: punctuationKeyWidthNarrow + implicitWidth: punctuationKeyWidth fixedWidth: !splitActive } DiaCharacterKey { @@ -122,7 +122,7 @@ KeyboardLayout { captionShifted: "?" symView: "!" symView2: "!" - implicitWidth: punctuationKeyWidthNarrow + implicitWidth: punctuationKeyWidth fixedWidth: !splitActive separator: SeparatorState.HiddenSeparator } diff --git a/usr/share/maliit/plugins/com/jolla/layouts/pa.qml b/usr/share/maliit/plugins/com/jolla/layouts/pa.qml index 5471a35c..09d2a663 100644 --- a/usr/share/maliit/plugins/com/jolla/layouts/pa.qml +++ b/usr/share/maliit/plugins/com/jolla/layouts/pa.qml @@ -88,7 +88,7 @@ KeyboardLayout { captionShifted: "." symView: "." symView2: "." - implicitWidth: punctuationKeyWidthNarrow + implicitWidth: punctuationKeyWidth fixedWidth: !splitActive } SmallCharacterKey { @@ -109,7 +109,7 @@ KeyboardLayout { captionShifted: "?" symView: "!" symView2: "!" - implicitWidth: punctuationKeyWidthNarrow + implicitWidth: punctuationKeyWidth fixedWidth: !splitActive separator: SeparatorState.HiddenSeparator } diff --git a/usr/share/maliit/plugins/com/jolla/layouts/ru.qml b/usr/share/maliit/plugins/com/jolla/layouts/ru.qml index 0c13016c..d336ae4d 100644 --- a/usr/share/maliit/plugins/com/jolla/layouts/ru.qml +++ b/usr/share/maliit/plugins/com/jolla/layouts/ru.qml @@ -96,11 +96,11 @@ KeyboardLayout { CharacterKey { caption: "-" captionShifted: "-" - implicitWidth: punctuationKeyWidthNarrow + implicitWidth: punctuationKeyWidth fixedWidth: !splitActive } ContextAwareCommaKey { - implicitWidth: punctuationKeyWidthNarrow + implicitWidth: punctuationKeyWidth } SpacebarKey {} SpacebarKey { @@ -108,7 +108,7 @@ KeyboardLayout { active: splitActive } PeriodKey { - implicitWidth: punctuationKeyWidthNarrow + implicitWidth: punctuationKeyWidth } EnterKey {} } diff --git a/usr/share/maliit/plugins/com/jolla/layouts/sk.qml b/usr/share/maliit/plugins/com/jolla/layouts/sk.qml index 51404c3c..0c70fa84 120000 --- a/usr/share/maliit/plugins/com/jolla/layouts/sk.qml +++ b/usr/share/maliit/plugins/com/jolla/layouts/sk.qml @@ -1 +1,212 @@ -cs.qml \ No newline at end of file +/* + * Copyright (C) 2014 Jolla ltd and/or its subsidiary(-ies). All rights reserved. + * + * Contact: Pekka Vuorela + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this list + * of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * Neither the name of Jolla Ltd nor the names of its contributors may be + * used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + */ + +import QtQuick 2.0 +import ".." + +KeyboardLayout { + splitSupported: true + + KeyboardRow { + CharacterKey { caption: "q"; captionShifted: "Q"; symView: "1"; symView2: "€" } + CharacterKey { caption: "w"; captionShifted: "W"; symView: "2"; symView2: "£" } + AccentedCharacterKey { + caption: "e" + captionShifted: "E" + symView: "3" + symView2: "$" + accents: "ěeéęèë€" + accentsShifted: "ĚEÉĘÈË€" + deadKeyAccents: "´éˇě" + deadKeyAccentsShifted: "´ÉˇĚ" + } + AccentedCharacterKey { + caption: "r" + captionShifted: "R" + symView: "4" + symView2: "¥" + accents: "řrŕ" + accentsShifted: "ŘRŔ" + deadKeyAccents: "ˇř´ŕ" + deadKeyAccentsShifted: "ˇŘ´Ŕ" + } + AccentedCharacterKey { + caption: "t" + captionShifted: "T" + symView: "5" + symView2: "₹" + accents: "ťtţ" + accentsShifted: "ŤTŢ" + deadKeyAccents: "ˇť" + deadKeyAccentsShifted: "ˇŤ" + } + AccentedCharacterKey { + caption: "z" + captionShifted: "Z" + symView: "6" + symView2: "%" + accents: "žzźż" + accentsShifted: "ŽZŹŻ" + deadKeyAccents: "ˇž" + deadKeyAccentsShifted: "ˇŽ" + } + AccentedCharacterKey { + caption: "u" + captionShifted: "U" + symView: "7" + symView2: "<" + accents: "ûüúuůűù" + accentsShifted: "ÛÜÚUŮŰÙ" + deadKeyAccents: "´ú" + deadKeyAccentsShifted: "´Ú" + } + AccentedCharacterKey { + caption: "i" + captionShifted: "I" + symView: "8" + symView2: ">" + accents: "îìíiï" + accentsShifted: "ÎÌÍIÏ" + deadKeyAccents: "´í" + deadKeyAccentsShifted: "´Í" + } + AccentedCharacterKey { + caption: "o" + captionShifted: "O" + symView: "9" + symView2: "[" + accents: "öőøòóôoõ" + accentsShifted: "ÖŐØÒÓÔOÕ" + deadKeyAccents: "´ó" + deadKeyAccentsShifted: "´Ó" + } + CharacterKey { caption: "p"; captionShifted: "P"; symView: "0"; symView2: "]" } + } + + KeyboardRow { + AccentedCharacterKey { + caption: "a" + captionShifted: "A" + symView: "*" + symView2: "`" + accents: "aäáăâąàã" + accentsShifted: "AÄÁĂÂĄÀÃ" + deadKeyAccents: "´á" + deadKeyAccentsShifted: "´Á" + } + AccentedCharacterKey { + caption: "s" + captionShifted: "S" + symView: "#" + symView2: "^" + accents: "sšßśş$" + accentsShifted: "SŠẞŚŞ$" + deadKeyAccents: "ˇš" + deadKeyAccentsShifted: "ˇŠ" + } + AccentedCharacterKey { + caption: "d" + captionShifted: "D" + symView: "+" + symView2: "|" + accents: "ďdđ" + accentsShifted: "ĎDĐ" + deadKeyAccents: "ˇď" + deadKeyAccentsShifted: "ˇĎ" + } + CharacterKey { caption: "f"; captionShifted: "F"; symView: "-"; symView2: "_" } + CharacterKey { caption: "g"; captionShifted: "G"; symView: "="; symView2: "§" } + CharacterKey { caption: "h"; captionShifted: "H"; symView: "("; symView2: "{" } + CharacterKey { caption: "j"; captionShifted: "J"; symView: ")"; symView2: "}" } + CharacterKey { caption: "k"; captionShifted: "K"; symView: "!"; symView2: "¡" } + AccentedCharacterKey { + caption: "l" + captionShifted: "L" + symView: "?" + symView2: "¿"; + accents: "ľĺlł" + accentsShifted: "ĽĹLŁ" + deadKeyAccents: "ˇľ´ĺ" + deadKeyAccentsShifted: "ˇĽ´Ĺ" + } + DeadKey { + caption: "ˇ" + captionShifted: "ˇ" + } + } + + KeyboardRow { + splitIndex: 5 + + ShiftKey {} + + AccentedCharacterKey { + caption: "y" + captionShifted: "Y" + symView: "@" + symView2: "«" + accents: "ýy¥" + accentsShifted: "ÝY¥" + deadKeyAccents: "´ý" + deadKeyAccentsShifted: "´Ý" + } + CharacterKey { caption: "x"; captionShifted: "X"; symView: "&"; symView2: "»" } + AccentedCharacterKey { + caption: "c" + captionShifted: "C" + symView: "/" + symView2: "\"" + accents: "čcćç" + accentsShifted: "ČCĆÇ" + deadKeyAccents: "ˇč" + deadKeyAccentsShifted: "ˇČ" + } + CharacterKey { caption: "v"; captionShifted: "V"; symView: "\\"; symView2: "“" } + CharacterKey { caption: "b"; captionShifted: "B"; symView: "'"; symView2: "”" } + + AccentedCharacterKey { + caption: "n" + captionShifted: "N" + symView: ";" + symView2: "„" + accents: "ňńnñ" + accentsShifted: "ŇŃNÑ" + deadKeyAccents: "ˇň" + deadKeyAccentsShifted: "ˇŇ" + } + CharacterKey { caption: "m"; captionShifted: "M"; symView: ":"; symView2: "~" } + + BackspaceKey {} + } + + SpacebarRowDeadKey { + deadKeyCaption: "´" + deadKeyCaptionShifted: "´" + } +} diff --git a/usr/share/maliit/plugins/com/jolla/layouts/ta.qml b/usr/share/maliit/plugins/com/jolla/layouts/ta.qml index cb24f7f6..d6cfb0af 100644 --- a/usr/share/maliit/plugins/com/jolla/layouts/ta.qml +++ b/usr/share/maliit/plugins/com/jolla/layouts/ta.qml @@ -85,7 +85,7 @@ KeyboardLayout { captionShifted: "." symView: "." symView2: "." - implicitWidth: punctuationKeyWidthNarrow + implicitWidth: punctuationKeyWidth fixedWidth: !splitActive } TinyCharacterKey { @@ -106,7 +106,7 @@ KeyboardLayout { captionShifted: "?" symView: "!" symView2: "!" - implicitWidth: punctuationKeyWidthNarrow + implicitWidth: punctuationKeyWidth fixedWidth: !splitActive separator: SeparatorState.HiddenSeparator } diff --git a/usr/share/maliit/plugins/com/jolla/layouts/te.qml b/usr/share/maliit/plugins/com/jolla/layouts/te.qml index e7b2b9e2..a12f9eba 100644 --- a/usr/share/maliit/plugins/com/jolla/layouts/te.qml +++ b/usr/share/maliit/plugins/com/jolla/layouts/te.qml @@ -92,7 +92,7 @@ KeyboardLayout { captionShifted: "." symView: "." symView2: "." - implicitWidth: punctuationKeyWidthNarrow + implicitWidth: punctuationKeyWidth fixedWidth: !splitActive } TinyCharacterKey { @@ -113,7 +113,7 @@ KeyboardLayout { captionShifted: "?" symView: "!" symView2: "!" - implicitWidth: punctuationKeyWidthNarrow + implicitWidth: punctuationKeyWidth fixedWidth: !splitActive separator: SeparatorState.HiddenSeparator } diff --git a/usr/share/maliit/plugins/com/jolla/layouts/tt.qml b/usr/share/maliit/plugins/com/jolla/layouts/tt.qml index 39b1ca21..3ce97659 100644 --- a/usr/share/maliit/plugins/com/jolla/layouts/tt.qml +++ b/usr/share/maliit/plugins/com/jolla/layouts/tt.qml @@ -98,11 +98,11 @@ KeyboardLayout { CharacterKey { caption: "-" captionShifted: "-" - implicitWidth: punctuationKeyWidthNarrow + implicitWidth: punctuationKeyWidth fixedWidth: !splitActive } ContextAwareCommaKey { - implicitWidth: punctuationKeyWidthNarrow + implicitWidth: punctuationKeyWidth } SpacebarKey {} SpacebarKey { @@ -110,7 +110,7 @@ KeyboardLayout { active: splitActive } PeriodKey { - implicitWidth: punctuationKeyWidthNarrow + implicitWidth: punctuationKeyWidth } EnterKey {} } diff --git a/usr/share/maliit/plugins/com/jolla/layouts/uk.qml b/usr/share/maliit/plugins/com/jolla/layouts/uk.qml new file mode 100644 index 00000000..5ebdfd2b --- /dev/null +++ b/usr/share/maliit/plugins/com/jolla/layouts/uk.qml @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2013 – 2015, Oleksii Serdiuk + * Copyright (c) 2013 – 2022, Jolla Ltd. + * All rights reserved. + * + * Contact: Pekka Vuorela + * Contact: Oleksii Serdiuk + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of the Jolla Ltd. nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + */ + +import QtQuick 2.0 +import ".." +import com.jolla.keyboard 1.0 + +KeyboardLayout { + splitSupported: true + + KeyboardRow { + CharacterKey { caption: "й"; captionShifted: "Й"; symView: "1"; symView2: "€" } + CharacterKey { caption: "ц"; captionShifted: "Ц"; symView: "2"; symView2: "£" } + CharacterKey { caption: "у"; captionShifted: "У"; symView: "3"; symView2: "$" } + CharacterKey { caption: "к"; captionShifted: "К"; symView: "4"; symView2: "¥" } + CharacterKey { caption: "е"; captionShifted: "Е"; symView: "5"; symView2: "₹" } + CharacterKey { caption: "н"; captionShifted: "Н"; symView: "6"; symView2: "₴" } + CharacterKey { caption: "г"; captionShifted: "Г"; symView: "7"; symView2: "<"; accents: "гґ₴"; accentsShifted: "ГҐ₴" } + CharacterKey { caption: "ш"; captionShifted: "Ш"; symView: "8"; symView2: ">" } + CharacterKey { caption: "щ"; captionShifted: "Щ"; symView: "9"; symView2: "[" } + CharacterKey { caption: "з"; captionShifted: "З"; symView: "0"; symView2: "]" } + CharacterKey { caption: "х"; captionShifted: "Х"; symView: "№"; symView2: "¢" } + FittedCharacterKey { caption: "ї"; captionShifted: "Ї"; symView: "%"; symView2: "‰" } + } + + KeyboardRow { + CharacterKey { caption: "ф"; captionShifted: "Ф"; symView: "*"; symView2: "`" } + CharacterKey { caption: "і"; captionShifted: "І"; symView: "#"; symView2: "√" } + CharacterKey { caption: "в"; captionShifted: "В"; symView: "+"; symView2: "±" } + CharacterKey { caption: "а"; captionShifted: "А"; symView: "×"; symView2: "_" } + CharacterKey { caption: "п"; captionShifted: "П"; symView: "="; symView2: "≈" } + CharacterKey { caption: "р"; captionShifted: "Р"; symView: "("; symView2: "{" } + CharacterKey { caption: "о"; captionShifted: "О"; symView: ")"; symView2: "}" } + CharacterKey { caption: "л"; captionShifted: "Л"; symView: "\""; symView2: "°" } + CharacterKey { caption: "д"; captionShifted: "Д"; symView: "~"; symView2: "·" } + CharacterKey { caption: "ж"; captionShifted: "Ж"; symView: "²"; symView2: "³" } + CharacterKey { caption: "є"; captionShifted: "Є"; symView: "!"; symView2: "¡" } + CharacterKey { caption: "ґ"; captionShifted: "Ґ"; symView: "?"; symView2: "¿" } + } + + KeyboardRow { + splitIndex: 6 + + ShiftKey { + implicitWidth: shiftKeyWidthNarrow + } + + CharacterKey { caption: "я"; captionShifted: "Я"; symView: "@"; symView2: "«" } + CharacterKey { caption: "ч"; captionShifted: "Ч"; symView: "&"; symView2: "»" } + CharacterKey { caption: "с"; captionShifted: "С"; symView: "/"; symView2: "÷" } + CharacterKey { caption: "м"; captionShifted: "М"; symView: "\\"; symView2: "“" } + CharacterKey { caption: "и"; captionShifted: "И"; symView: "-"; symView2: "”" } + CharacterKey { caption: "т"; captionShifted: "Т"; symView: ";"; symView2: "„" } + CharacterKey { caption: "ь"; captionShifted: "Ь"; symView: ":"; symView2: "©" } + CharacterKey { caption: "б"; captionShifted: "Б"; symView: "^"; symView2: "®" } + CharacterKey { caption: "ю"; captionShifted: "Ю"; symView: "|"; symView2: "§" } + + BackspaceKey { + implicitWidth: shiftKeyWidthNarrow + } + } + + KeyboardRow { + splitIndex: 4 + + SymbolKey { + symbolCaption: "АБВ" + implicitWidth: symbolKeyWidthNarrow + } + + CharacterKey { + caption: "'" + captionShifted: "'" + implicitWidth: punctuationKeyWidth + fixedWidth: !splitActive + } + ContextAwareCommaKey { + implicitWidth: punctuationKeyWidth + } + SpacebarKey {} + SpacebarKey { + languageLabel: "" + active: splitActive + } + PeriodKey { + implicitWidth: punctuationKeyWidth + } + EnterKey {} + } +} diff --git a/usr/share/maliit/plugins/com/jolla/layouts/zh_hwr_simplified.qml b/usr/share/maliit/plugins/com/jolla/layouts/zh_hwr_simplified.qml new file mode 100644 index 00000000..8a95092d --- /dev/null +++ b/usr/share/maliit/plugins/com/jolla/layouts/zh_hwr_simplified.qml @@ -0,0 +1,9 @@ +// Copyright (C) 2013 Jolla Ltd. +// Contact: Pekka Vuorela + +import QtQuick 2.0 +import ".." + +HwrLayout { + inputMode: "simplified" +} diff --git a/usr/share/maliit/plugins/com/jolla/layouts/zh_hwr_traditional.qml b/usr/share/maliit/plugins/com/jolla/layouts/zh_hwr_traditional.qml new file mode 100644 index 00000000..73d8258d --- /dev/null +++ b/usr/share/maliit/plugins/com/jolla/layouts/zh_hwr_traditional.qml @@ -0,0 +1,9 @@ +// Copyright (C) 2013 Jolla Ltd. +// Contact: Pekka Vuorela + +import QtQuick 2.0 +import ".." + +HwrLayout { + inputMode: "traditional" +} diff --git a/usr/share/maliit/plugins/com/jolla/touchpointarray.js b/usr/share/maliit/plugins/com/jolla/touchpointarray.js index 83929cf8..5e53e5eb 100644 --- a/usr/share/maliit/plugins/com/jolla/touchpointarray.js +++ b/usr/share/maliit/plugins/com/jolla/touchpointarray.js @@ -48,8 +48,16 @@ function addPoint(touchEvent) { point.pointId = touchEvent.pointId point.x = touchEvent.x point.y = touchEvent.y - point.startX = touchEvent.startX - point.startY = touchEvent.startY + + // Workaround: if synthesized from the mouse events the startPosition is invalid (0, 0) + if (touchEvent.startX === 0 && touchEvent.startY === 0) { + point.startX = touchEvent.x + point.startY = touchEvent.y + } else { + point.startX = touchEvent.startX + point.startY = touchEvent.startY + } + point.pressedKey = null point.initialKey = null diff --git a/usr/share/nemo-transferengine/plugins/sharing/SigningShare.qml b/usr/share/nemo-transferengine/plugins/sharing/SigningShare.qml new file mode 100644 index 00000000..365f2894 --- /dev/null +++ b/usr/share/nemo-transferengine/plugins/sharing/SigningShare.qml @@ -0,0 +1,29 @@ +/**************************************************************************************** +** +** Copyright (c) 2013 - 2021 Jolla Ltd. +** Copyright (c) 2021 Open Mobile Platform LLC +** All rights reserved. +** +** License: Proprietary. +** +****************************************************************************************/ +import QtQuick 2.6 +import Sailfish.Silica 1.0 +import Nemo.DBus 2.0 + +Item { + property var shareAction + + Component.onCompleted: { + settingsApp.call("share", [shareAction.toConfiguration()]) + shareAction.done() + } + + DBusInterface { + id: settingsApp + + service: "com.jolla.settings" + path: "/share_signing_keys" + iface: "org.sailfishos.share" + } +} diff --git a/usr/share/sailfish-browser/browser.qml b/usr/share/sailfish-browser/browser.qml index 48b9e668..5710cdfc 100644 --- a/usr/share/sailfish-browser/browser.qml +++ b/usr/share/sailfish-browser/browser.qml @@ -21,14 +21,14 @@ BrowserWindow { cover = Qt.resolvedUrl("cover/NoTabsCover.qml") } else { if (cover != null && window.webView) { - window.webView.clearSurface(); + window.webView.clearSurface() } cover = null } } //% "Web browsing" - activityDisabledByMdm: qsTrId("sailfish_browser-la-web_browsing"); + activityDisabledByMdm: qsTrId("sailfish_browser-la-web_browsing") initialPage: Component { BrowserPage { id: browserPage diff --git a/usr/share/sailfish-browser/pages/BrowserPage.qml b/usr/share/sailfish-browser/pages/BrowserPage.qml index 99ee253e..4373eeef 100644 --- a/usr/share/sailfish-browser/pages/BrowserPage.qml +++ b/usr/share/sailfish-browser/pages/BrowserPage.qml @@ -308,7 +308,7 @@ Page { onActiveChanged: { var isFullScreen = webView.contentItem && webView.contentItem.fullscreen if (!isFullScreen && active && !overlay.enteringNewTabUrl) { - if (webView.tabModel.count !== 0 || (WebUtils.homePage !== "about:blank" && WebUtils.homePage.length > 0)) { + if (webView.hasInitialUrl || webView.tabModel.count !== 0 || (WebUtils.homePage !== "about:blank" && WebUtils.homePage.length > 0)) { overlay.animator.showChrome() } else { overlay.startPage() diff --git a/usr/share/sailfish-browser/pages/EditLoginPage.qml b/usr/share/sailfish-browser/pages/EditLoginPage.qml index 74922f12..8d18ff32 100644 --- a/usr/share/sailfish-browser/pages/EditLoginPage.qml +++ b/usr/share/sailfish-browser/pages/EditLoginPage.qml @@ -24,7 +24,7 @@ Dialog { canAccept: loginModel.canModify(uid, username, password) onAcceptBlocked: usernameField.errorHighlight = true - onAccepted: loginModel.modify(uid, username, password); + onAccepted: loginModel.modify(uid, username, password) SilicaFlickable { anchors.fill: parent @@ -71,7 +71,7 @@ Dialog { on_EchoModeToggleClicked: { if (_usePasswordEchoMode) { - secureAction.perform(function () { _usePasswordEchoMode = false }); + secureAction.perform(function () { _usePasswordEchoMode = false }) } else { _usePasswordEchoMode = true } diff --git a/usr/share/sailfish-browser/pages/LoginsPage.qml b/usr/share/sailfish-browser/pages/LoginsPage.qml index 4be3ef48..7efc5989 100644 --- a/usr/share/sailfish-browser/pages/LoginsPage.qml +++ b/usr/share/sailfish-browser/pages/LoginsPage.qml @@ -131,7 +131,7 @@ Page { visible: !secureAction.available //% "Copy username" text: qsTrId("sailfish_browser-me-login_copy_username") - onClicked: copyUsername(model.username); + onClicked: copyUsername(model.username) } MenuItem { visible: secureAction.available @@ -173,7 +173,7 @@ Page { //% "Delete" text: qsTrId("sailfish_browser-me-login_delete") onClicked: { - remove(model.uid); + remove(model.uid) } } } @@ -225,7 +225,7 @@ Page { text: qsTrId("sailfish_browser-me-login_copy_password") parent: menu._contentColumn // context menu touch requires menu items are children of content area onClicked: { - secureAction.perform(copyPassword.bind(null, _copyOptions.password)); + secureAction.perform(copyPassword.bind(null, _copyOptions.password)) menu.close() } } diff --git a/usr/share/sailfish-browser/pages/PrivacySettingsPage.qml b/usr/share/sailfish-browser/pages/PrivacySettingsPage.qml index dabaafb5..ef727f57 100644 --- a/usr/share/sailfish-browser/pages/PrivacySettingsPage.qml +++ b/usr/share/sailfish-browser/pages/PrivacySettingsPage.qml @@ -12,7 +12,7 @@ import QtQuick 2.1 import Sailfish.Silica 1.0 -import org.nemomobile.configuration 1.0 +import Nemo.Configuration 1.0 import Sailfish.Browser 1.0 Page { diff --git a/usr/share/sailfish-browser/pages/SettingsPage.qml b/usr/share/sailfish-browser/pages/SettingsPage.qml index b83f2f10..6d9e3d42 100644 --- a/usr/share/sailfish-browser/pages/SettingsPage.qml +++ b/usr/share/sailfish-browser/pages/SettingsPage.qml @@ -12,7 +12,7 @@ import QtQuick 2.6 import Sailfish.Silica 1.0 import Sailfish.Browser 1.0 -import org.nemomobile.configuration 1.0 +import Nemo.Configuration 1.0 import com.jolla.settings.system 1.0 import Sailfish.WebEngine 1.0 import Sailfish.Pickers 1.0 @@ -182,7 +182,7 @@ Page { leftMargin: Theme.horizontalPageMargin + Theme.paddingLarge + _textSwitchIconCenter _label.anchors.leftMargin: Theme.paddingMedium + _textSwitchIconCenter - onCheckedChanged: WebEngineSettings.javascriptEnabled = checked; + onCheckedChanged: WebEngineSettings.javascriptEnabled = checked } BackgroundItem { @@ -307,6 +307,43 @@ Page { } } } + + SectionHeader { + //: Section Header for Appearance settings + //% "Appearance" + text: qsTrId("settings_browser-la-appearance") + } + + BrowserComboBox { + //% "Preferred color scheme" + label: qsTrId("settings_browser-la-color_scheme") + iconSource: "image://theme/icon-m-night" + currentIndex: WebEngineSettings.colorScheme + + //% "The website style to use when available" + description: qsTrId("sailfish_browser-me-website_color_scheme") + + menu: ContextMenu { + MenuItem { + //: Option to prefer a website's light color scheme + //% "Light" + text: qsTrId("sailfish_browser-me-prefers_light_mode") + onClicked: WebEngineSettings.colorScheme = WebEngineSettings.PrefersLightMode + } + MenuItem { + //: Option to prefer a website's dark color scheme + //% "Dark" + text: qsTrId("sailfish_browser-me-prefers_dark_mode") + onClicked: WebEngineSettings.colorScheme = WebEngineSettings.PrefersDarkMode + } + MenuItem { + //: Option for the website's color scheme to match the ambience + //% "Match ambience" + text: qsTrId("sailfish_browser-me-follow_ambience") + onClicked: WebEngineSettings.colorScheme = WebEngineSettings.FollowsAmbience + } + } + } } } diff --git a/usr/share/sailfish-browser/pages/SitePermissionPage.qml b/usr/share/sailfish-browser/pages/SitePermissionPage.qml index 6e09bc61..9f75c09d 100644 --- a/usr/share/sailfish-browser/pages/SitePermissionPage.qml +++ b/usr/share/sailfish-browser/pages/SitePermissionPage.qml @@ -29,7 +29,7 @@ Page { } function _getCookieCapability() { - switch(WebEngineSettings.cookieBehavior) { + switch (WebEngineSettings.cookieBehavior) { case WebEngineSettings.AcceptAll: return PermissionManager.Allow case WebEngineSettings.BlockAll: diff --git a/usr/share/sailfish-browser/pages/components/AddHomePageDialog.qml b/usr/share/sailfish-browser/pages/components/AddHomePageDialog.qml index 2a4f3646..2fe25147 100644 --- a/usr/share/sailfish-browser/pages/components/AddHomePageDialog.qml +++ b/usr/share/sailfish-browser/pages/components/AddHomePageDialog.qml @@ -11,7 +11,7 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 import Sailfish.Policy 1.0 -import org.nemomobile.configuration 1.0 +import Nemo.Configuration 1.0 Dialog { id: dialog diff --git a/usr/share/sailfish-browser/pages/components/CertificateInfo.qml b/usr/share/sailfish-browser/pages/components/CertificateInfo.qml index a9cee600..5f0c7076 100644 --- a/usr/share/sailfish-browser/pages/components/CertificateInfo.qml +++ b/usr/share/sailfish-browser/pages/components/CertificateInfo.qml @@ -100,7 +100,7 @@ SilicaFlickable { x: Theme.horizontalPageMargin horizontalAlignment: Text.AlignHCenter wrapMode: Text.Wrap - color: Theme.secondaryHighlightColor + color: Theme.secondaryHighlightColor visible: !_secure //% "Do not enter personal data, passwords, card details on this site" text: qsTrId("sailfish_browser-sh-do_not-enter_personal_data") @@ -157,7 +157,7 @@ SilicaFlickable { x: Theme.horizontalPageMargin horizontalAlignment: Text.AlignHCenter wrapMode: Text.Wrap - color: Theme.highlightColor + color: Theme.highlightColor //% "Current permissions" text: qsTrId("sailfish_browser-sh-current-permissions") } diff --git a/usr/share/sailfish-browser/pages/components/ConfigDialog.qml b/usr/share/sailfish-browser/pages/components/ConfigDialog.qml index 437c66d9..5de73a2a 100644 --- a/usr/share/sailfish-browser/pages/components/ConfigDialog.qml +++ b/usr/share/sailfish-browser/pages/components/ConfigDialog.qml @@ -35,15 +35,15 @@ Dialog { if (value === "") { prefsList.model = prefsListModel } else { - filterListModel.clear(); + filterListModel.clear() for (var i=0; i -1) || (url.indexOf('#') > -1) ) ? encodeURI(url) : url + urlCopyNotice.show() + } + } + + Notice { + id: urlCopyNotice + duration: Notice.Short + verticalOffset: -Theme.itemSizeMedium + //: Url copied to clipboard from toolbar (long press). + //% "Url copied to clipboard" + text: qsTrId("sailfish_browser-la-url_copied_to_clipboard") + } + Label { anchors.verticalCenter: parent.verticalCenter width: parent.width + Theme.paddingMedium - color: touchArea.down ? Theme.highlightColor : Theme.primaryColor + color: touchArea.highlighted ? Theme.highlightColor : Theme.primaryColor text: { if (findInPageActive) { diff --git a/usr/share/sailfish-browser/shared/OverlayAnimator.qml b/usr/share/sailfish-browser/shared/OverlayAnimator.qml index 4498b517..46e94919 100644 --- a/usr/share/sailfish-browser/shared/OverlayAnimator.qml +++ b/usr/share/sailfish-browser/shared/OverlayAnimator.qml @@ -8,6 +8,7 @@ */ import QtQuick 2.2 +import Sailfish.Silica 1.0 Item { id: animator @@ -18,7 +19,7 @@ Item { property bool portrait property bool atTop property bool atBottom: true - property int transitionDuration: !_immediate ? (state === _certOverlay ? proportionalDuration : 400) : 0 + property int transitionDuration: !_immediate ? (state === _certOverlay ? proportionalDuration : 250) : 0 readonly property bool allowContentUse: state === _chromeVisible || state === _fullscreenWebPage && state !== _doubleToolBar readonly property bool dragging: state === _draggingOverlay readonly property bool secondaryTools: state === _doubleToolBar @@ -36,6 +37,7 @@ Item { readonly property string _draggingOverlay: "draggingOverlay" readonly property string _certOverlay: "certOverlay" readonly property string _noOverlay: "noOverlay" + property var _previousYs property int proportionalDuration: 400 function showSecondaryTools() { @@ -115,6 +117,44 @@ Item { direction = goingUp ? "upwards" : (goingDown ? "downwards" : "") } + Connections { + target: overlay + onYChanged: { + if (!_previousYs) + _previousYs = [] + + if (_previousYs.length > 0) { + var lastPos = _previousYs[_previousYs.length-1] + // Filter out movement a bit, padding medium as a hysteresis + var hasMoved = Math.abs(lastPos - overlay.y) > Theme.paddingMedium + if (hasMoved) _previousYs.push(overlay.y) + } else { + _previousYs.push(overlay.y) + } + + if (_previousYs.length > 5) + _previousYs.shift() + + var tmpDirection = "" + var directionChanged = false + for (var i = 1; i < _previousYs.length && _previousYs.length > 2; ++i) { + var dir = _previousYs[i-1] > _previousYs[i] ? "upwards" : "downwards" + if (tmpDirection !== "" && dir !== tmpDirection) { + directionChanged = true + break + } else { + tmpDirection = dir + } + } + + if (directionChanged) { + _previousYs = [] + } else { + direction = tmpDirection + } + } + } + Connections { target: webView ignoreUnknownSignals: true @@ -237,6 +277,7 @@ Item { // Target reached, clear it. if (atBottom || atTop) { direction = "" + _previousYs = [] } if (isOpenedState()) { opened = true diff --git a/usr/share/sailfish-browser/shared/ResourceController.qml b/usr/share/sailfish-browser/shared/ResourceController.qml index 138869b2..deb7338d 100644 --- a/usr/share/sailfish-browser/shared/ResourceController.qml +++ b/usr/share/sailfish-browser/shared/ResourceController.qml @@ -15,8 +15,8 @@ import QtQuick 2.0 import Nemo.KeepAlive 1.2 import Sailfish.WebEngine 1.0 import Nemo.DBus 2.0 -import MeeGo.Connman 0.2 -import org.nemomobile.policy 1.0 +import Connman 0.2 +import Nemo.Policy 1.0 import Nemo.Connectivity 1.0 // QtObject cannot have children diff --git a/usr/share/sailfish-browser/shared/WebView.qml b/usr/share/sailfish-browser/shared/WebView.qml index dd915311..5c26e8f1 100644 --- a/usr/share/sailfish-browser/shared/WebView.qml +++ b/usr/share/sailfish-browser/shared/WebView.qml @@ -162,7 +162,7 @@ WebContainer { onLoginSaved: { FaviconManager.grabIcon("logins", webPage, Qt.size(Theme.iconSizeMedium, - Theme.iconSizeMedium)); + Theme.iconSizeMedium)) } } @@ -268,7 +268,7 @@ WebContainer { // Refresh timers (if any) keep working even for suspended views. Hence // suspend the view again explicitly if browser content window is in not visible (background). if (loaded && !webView.visible) { - suspendView(); + suspendView() } } @@ -323,7 +323,7 @@ WebContainer { } case "embed:find": { // Found, or found wrapped - if( data.r == 0 || data.r == 2) { + if (data.r == 0 || data.r == 2) { webView.findInPageHasResult = true } else { webView.findInPageHasResult = false diff --git a/usr/share/sailfish-captiveportal/pages/components/CaptivePortalOverlay.qml b/usr/share/sailfish-captiveportal/pages/components/CaptivePortalOverlay.qml index 6f8e41c3..429ce94b 100644 --- a/usr/share/sailfish-captiveportal/pages/components/CaptivePortalOverlay.qml +++ b/usr/share/sailfish-captiveportal/pages/components/CaptivePortalOverlay.qml @@ -30,7 +30,7 @@ Shared.Background { function loadPage(url) { if (webView && webView.tabModel.count === 0) { - webView.clearSurface(); + webView.clearSurface() } // let gecko figure out how to handle malformed URLs var pageUrl = url diff --git a/usr/share/sailfish-captiveportal/shared/OverlayAnimator.qml b/usr/share/sailfish-captiveportal/shared/OverlayAnimator.qml index 4498b517..46e94919 100644 --- a/usr/share/sailfish-captiveportal/shared/OverlayAnimator.qml +++ b/usr/share/sailfish-captiveportal/shared/OverlayAnimator.qml @@ -8,6 +8,7 @@ */ import QtQuick 2.2 +import Sailfish.Silica 1.0 Item { id: animator @@ -18,7 +19,7 @@ Item { property bool portrait property bool atTop property bool atBottom: true - property int transitionDuration: !_immediate ? (state === _certOverlay ? proportionalDuration : 400) : 0 + property int transitionDuration: !_immediate ? (state === _certOverlay ? proportionalDuration : 250) : 0 readonly property bool allowContentUse: state === _chromeVisible || state === _fullscreenWebPage && state !== _doubleToolBar readonly property bool dragging: state === _draggingOverlay readonly property bool secondaryTools: state === _doubleToolBar @@ -36,6 +37,7 @@ Item { readonly property string _draggingOverlay: "draggingOverlay" readonly property string _certOverlay: "certOverlay" readonly property string _noOverlay: "noOverlay" + property var _previousYs property int proportionalDuration: 400 function showSecondaryTools() { @@ -115,6 +117,44 @@ Item { direction = goingUp ? "upwards" : (goingDown ? "downwards" : "") } + Connections { + target: overlay + onYChanged: { + if (!_previousYs) + _previousYs = [] + + if (_previousYs.length > 0) { + var lastPos = _previousYs[_previousYs.length-1] + // Filter out movement a bit, padding medium as a hysteresis + var hasMoved = Math.abs(lastPos - overlay.y) > Theme.paddingMedium + if (hasMoved) _previousYs.push(overlay.y) + } else { + _previousYs.push(overlay.y) + } + + if (_previousYs.length > 5) + _previousYs.shift() + + var tmpDirection = "" + var directionChanged = false + for (var i = 1; i < _previousYs.length && _previousYs.length > 2; ++i) { + var dir = _previousYs[i-1] > _previousYs[i] ? "upwards" : "downwards" + if (tmpDirection !== "" && dir !== tmpDirection) { + directionChanged = true + break + } else { + tmpDirection = dir + } + } + + if (directionChanged) { + _previousYs = [] + } else { + direction = tmpDirection + } + } + } + Connections { target: webView ignoreUnknownSignals: true @@ -237,6 +277,7 @@ Item { // Target reached, clear it. if (atBottom || atTop) { direction = "" + _previousYs = [] } if (isOpenedState()) { opened = true diff --git a/usr/share/sailfish-captiveportal/shared/ResourceController.qml b/usr/share/sailfish-captiveportal/shared/ResourceController.qml index 138869b2..deb7338d 100644 --- a/usr/share/sailfish-captiveportal/shared/ResourceController.qml +++ b/usr/share/sailfish-captiveportal/shared/ResourceController.qml @@ -15,8 +15,8 @@ import QtQuick 2.0 import Nemo.KeepAlive 1.2 import Sailfish.WebEngine 1.0 import Nemo.DBus 2.0 -import MeeGo.Connman 0.2 -import org.nemomobile.policy 1.0 +import Connman 0.2 +import Nemo.Policy 1.0 import Nemo.Connectivity 1.0 // QtObject cannot have children diff --git a/usr/share/sailfish-captiveportal/shared/WebView.qml b/usr/share/sailfish-captiveportal/shared/WebView.qml index dd915311..5c26e8f1 100644 --- a/usr/share/sailfish-captiveportal/shared/WebView.qml +++ b/usr/share/sailfish-captiveportal/shared/WebView.qml @@ -162,7 +162,7 @@ WebContainer { onLoginSaved: { FaviconManager.grabIcon("logins", webPage, Qt.size(Theme.iconSizeMedium, - Theme.iconSizeMedium)); + Theme.iconSizeMedium)) } } @@ -268,7 +268,7 @@ WebContainer { // Refresh timers (if any) keep working even for suspended views. Hence // suspend the view again explicitly if browser content window is in not visible (background). if (loaded && !webView.visible) { - suspendView(); + suspendView() } } @@ -323,7 +323,7 @@ WebContainer { } case "embed:find": { // Found, or found wrapped - if( data.r == 0 || data.r == 2) { + if (data.r == 0 || data.r == 2) { webView.findInPageHasResult = true } else { webView.findInPageHasResult = false diff --git a/usr/share/sailfish-installationhandler/SideloadDialog.qml b/usr/share/sailfish-installationhandler/SideloadDialog.qml index 1773cec6..7f5030d4 100644 --- a/usr/share/sailfish-installationhandler/SideloadDialog.qml +++ b/usr/share/sailfish-installationhandler/SideloadDialog.qml @@ -12,7 +12,7 @@ SystemDialog { signal requestInstall - title: rpmInfo.name + title: packageName contentHeight: contentColumn.height Column { @@ -21,22 +21,22 @@ SystemDialog { SystemDialogHeader { id: header - - title: rpmInfo.name - description: rpmInfo.summary + title: packageName + description: packageSummary } + Label { width: header.width anchors.horizontalCenter: parent.horizontalCenter - visible: text != "" + visible: packageVersion != "" color: Theme.highlightColor horizontalAlignment: Text.AlignHCenter wrapMode: Text.WrapAtWordBoundaryOrAnywhere font.pixelSize: Theme.fontSizeExtraSmall - //: %1 replaced with package name + //: %1 replaced with package version //% "Version %1" - text: qsTrId("installation_handler-la-version").arg(rpmInfo.version) + text: qsTrId("installation_handler-la-version").arg(packageVersion) } SystemDialogIconButton { diff --git a/usr/share/sailfish-office/CoverFileItem.qml b/usr/share/sailfish-office/CoverFileItem.qml new file mode 100644 index 00000000..fb6af46d --- /dev/null +++ b/usr/share/sailfish-office/CoverFileItem.qml @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2013-2014 Jolla Ltd. + * Contact: Robin Burchell + * + * 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; version 2 only. + * + * 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. + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 + +Item { + id: root + + property alias text: label.text + property bool multiLine + property string iconSource + property int iconSize + + Image { + id: icon + + anchors { + left: parent.left + leftMargin: Theme.paddingLarge - Theme.paddingSmall // counter the padding inside the icon + verticalCenter: root.multiLine ? undefined : parent.verticalCenter + } + source: root.iconSource !== "" ? root.iconSource + : "image://theme/icon-m-document" + + sourceSize { + width: root.iconSize + height: root.iconSize + } + } + Label { + id: label + + anchors { + left: icon.right + leftMargin: Theme.paddingMedium + verticalCenter: root.multiLine ? undefined : parent.verticalCenter + right: parent.right + rightMargin: Theme.paddingLarge - Theme.paddingSmall // counter the margin caused by fading + } + + truncationMode: root.multiLine ? TruncationMode.None : TruncationMode.Fade + wrapMode: root.multiLine ? Text.WrapAnywhere : Text.NoWrap + } +} diff --git a/usr/share/sailfish-office/CoverPage.qml b/usr/share/sailfish-office/CoverPage.qml new file mode 100644 index 00000000..ff586214 --- /dev/null +++ b/usr/share/sailfish-office/CoverPage.qml @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2013-2014 Jolla Ltd. + * Contact: Robin Burchell + * + * 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; version 2 only. + * + * 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. + */ + +import QtQuick 2.4 +import Sailfish.Silica 1.0 +import Sailfish.Office 1.0 + +CoverBackground { + id: root + + property alias preview: previewLoader.sourceComponent + + CoverPlaceholder { + //: Cover placeholder shown when there are no documents + //% "No documents" + text: qsTrId("sailfish-office-la-cover_no_documents") + icon.source: "image://theme/icon-launcher-office" + visible: previewLoader.status !== Loader.Ready && fileListView.count == 0 + } + + property int iconSize: Math.round(Theme.iconSizeMedium * 0.8) + + ListView { + id: fileListView + + property int itemHeight: height/maxItemCount + property int maxItemCount: Math.round(height/(Math.max(fontMetrics.height, iconSize) + Theme.paddingSmall)) + clip: true + interactive: false + model: window.fileListModel + visible: previewLoader.status !== Loader.Ready + anchors { + fill: parent + topMargin: Theme.paddingLarge + bottomMargin: Theme.paddingLarge + } + + delegate: CoverFileItem { + width: fileListView.width + height: fileListView.itemHeight + text: model.fileName + iconSource: window.mimeToIcon(model.fileMimeType) + iconSize: root.iconSize + } + FontMetrics { + id: fontMetrics + font.pixelSize: Theme.fontSizeMedium + } + } + + Loader { + id: previewLoader + + width: root.width + height: root.height + + active: root.status === Cover.Active + sourceComponent: window.coverPreview + } +} diff --git a/usr/share/sailfish-office/FileListPage.qml b/usr/share/sailfish-office/FileListPage.qml new file mode 100644 index 00000000..31d16c6a --- /dev/null +++ b/usr/share/sailfish-office/FileListPage.qml @@ -0,0 +1,319 @@ +/* + * Copyright (C) 2019 - 2021 Open Mobile Platform LLC. + * Copyright (C) 2013-2014 Jolla Ltd. + * Contact: Robin Burchell + * + * 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; version 2 only. + * + * 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. + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Sailfish.Office 1.0 +import Sailfish.Office.Files 1.0 +import Sailfish.Share 1.0 + +Page { + id: page + + property alias model: filteredModel.sourceModel + property string title + property string searchText: searchField.text + property bool searchEnabled + property QtObject provider + + property string deletingSource + + function deleteSource(source) { + pageStack.pop() + deletingSource = source + var popup = Remorse.popupAction( + page, + Remorse.deletedText, + function() { + provider.deleteFile(deletingSource) + deletingSource = "" + }) + popup.canceled.connect(function() { deletingSource = "" }) + } + + allowedOrientations: Orientation.All + + onSearchEnabledChanged: { + if (pageStack.currentPage.status == PageStatus.Active) { + if (searchEnabled) { + searchField.forceActiveFocus() + } else { + searchField.focus = false + } + } + if (!searchEnabled) { + searchField.text = "" + } + } + + function getSortParameterName(parameter) { + if (parameter === FilterModel.Name) { + //% "name" + return qsTrId("sailfish_office-me-sort_by_name") + } else if (parameter === FilterModel.Type) { + //% "type" + return qsTrId("sailfish_office-me-sort_by_type") + } else if (parameter === FilterModel.Date) { + //% "date" + return qsTrId("sailfish_office-me-sort_by_date") + } + + return "" + } + + FilterModel { + id: filteredModel + filterRegExp: RegExp(searchText, "i") + } + + SilicaListView { + id: listView + + anchors.fill: parent + model: filteredModel + currentIndex: -1 // otherwise currentItem will steal focus + + header: Item { + width: listView.width + height: headerContent.height + } + + Column { + id: headerContent + + parent: listView.headerItem + width: parent.width + height: pageHeader.height + (searchEnabled ? searchField.height : 0) + Behavior on height { + NumberAnimation { + duration: 150 + easing.type: Easing.InOutQuad + } + } + + PageHeader { + id: pageHeader + //: Application title + //% "Documents" + title: qsTrId("sailfish-office-he-apptitle") + } + + SearchField { + id: searchField + + width: parent.width + opacity: page.searchEnabled ? 1.0 : 0.0 + visible: opacity > 0 + + //: Document search field placeholder text + //% "Search documents" + placeholderText: qsTrId("sailfish-office-tf-search-documents") + + // We prefer lowercase + inputMethodHints: Qt.ImhNoAutoUppercase | Qt.ImhPreferLowercase | Qt.ImhNoPredictiveText + EnterKey.iconSource: "image://theme/icon-m-enter-close" + EnterKey.onClicked: focus = false + + Behavior on opacity { FadeAnimation { duration: 150 } } + } + } + + Connections { + target: searchField.activeFocus ? listView : null + ignoreUnknownSignals: true + onContentYChanged: { + if (listView.contentY > (Screen.height / 2)) { + searchField.focus = false + } + } + } + + PullDownMenu { + id: menu + + property bool _searchEnabled + + // avoid changing text state while menu is open + onActiveChanged: { + if (active) { + _searchEnabled = page.searchEnabled + } + } + + MenuItem { + text: !menu._searchEnabled ? //% "Show search" + qsTrId("sailfish-office-me-show_search") + //% "Hide search" + : qsTrId("sailfish-office-me-hide_search") + onClicked: page.searchEnabled = !page.searchEnabled + } + + MenuItem { + //% "Sort by: %1" + text: qsTrId("sailfish-office-me-sort_by").arg(getSortParameterName(filteredModel.sortParameter)) + onClicked: { + var obj = pageStack.animatorPush("SortTypeSelectionPage.qml") + obj.pageCompleted.connect(function(page) { + page.sortSelected.connect(function(sortParameter) { + filteredModel.sortParameter = sortParameter + pageStack.pop() + }) + }) + } + } + } + + InfoLabel { + parent: listView.contentItem + y: listView.headerItem.y + pageHeader.height + searchField.height + + (page.isPortrait ? Theme.itemSizeMedium : Theme.paddingLarge) + text: page.provider.error ? //% "Error getting document list" + qsTrId("sailfish-office-la-error_getting_documents") + : page.provider.count == 0 + ? //: View placeholder shown when there are no documents + //% "No documents" + qsTrId("sailfish-office-la-no_documents") + : //% "No documents found" + qsTrId("sailfish-office-la-not-found") + opacity: (page.provider.ready && page.provider.count == 0) + || (searchText.length > 0 && listView.count == 0) + || page.provider.error + ? 1.0 : 0.0 + Behavior on opacity { FadeAnimator {} } + } + + delegate: ListItem { + id: listItem + + hidden: deletingSource === model.filePath + contentHeight: Math.max(Theme.itemSizeMedium, labels.height + 2 * Theme.paddingMedium) + + Image { + id: icon + anchors { + left: parent.left + leftMargin: Theme.horizontalPageMargin + verticalCenter: parent.verticalCenter + } + source: window.mimeToIcon(model.fileMimeType) + (highlighted ? "?" + Theme.highlightColor : "") + } + Column { + id: labels + anchors { + left: icon.right + leftMargin: Theme.paddingMedium + right: parent.right + rightMargin: Theme.horizontalPageMargin + verticalCenter: parent.verticalCenter + } + Label { + id: label + width: parent.width + color: listItem.highlighted ? Theme.highlightColor : Theme.primaryColor + text: searchText.length > 0 ? Theme.highlightText(model.fileName, searchText, Theme.highlightColor) + : model.fileName + textFormat: searchText.length > 0 ? Text.StyledText : Text.PlainText + font.pixelSize: Theme.fontSizeMedium + truncationMode: TruncationMode.Fade + } + Item { + width: parent.width + height: sizeLabel.height + Label { + id: sizeLabel + text: Format.formatFileSize(model.fileSize) + font.pixelSize: Theme.fontSizeExtraSmall + color: listItem.highlighted ? Theme.secondaryHighlightColor : Theme.secondaryColor + } + Label { + anchors.right: parent.right + text: Format.formatDate(model.fileDate, Format.Timepoint) + font.pixelSize: Theme.fontSizeExtraSmall + color: listItem.highlighted ? Theme.secondaryHighlightColor : Theme.secondaryColor + } + } + } + + onClicked: { + switch(model.fileDocumentClass) { + case DocumentListModel.TextDocument: + pageStack.animatorPush("Sailfish.Office.TextDocumentPage", + { title: model.fileName, source: model.filePath, mimeType: model.fileMimeType, provider: page.provider }) + break + case DocumentListModel.PlainTextDocument: + pageStack.animatorPush("Sailfish.Office.PlainTextDocumentPage", + { title: model.fileName, source: model.filePath, mimeType: model.fileMimeType, provider: page.provider }) + break + case DocumentListModel.SpreadSheetDocument: + pageStack.animatorPush("Sailfish.Office.SpreadsheetPage", + { title: model.fileName, source: model.filePath, mimeType: model.fileMimeType, provider: page.provider }) + break + case DocumentListModel.PresentationDocument: + pageStack.animatorPush("Sailfish.Office.PresentationPage", + { title: model.fileName, source: model.filePath, mimeType: model.fileMimeType, provider: page.provider }) + break + case DocumentListModel.PDFDocument: + pageStack.animatorPush("Sailfish.Office.PDFDocumentPage", + { title: model.fileName, source: model.filePath, mimeType: model.fileMimeType, provider: page.provider }) + break + default: + console.log("Unknown file format for file " + model.fileName + " with stated mimetype " + model.fileMimeType) + break + } + } + + function deleteFile() { + remorseDelete(function() { page.provider.deleteFile(model.filePath) }) + } + + // TODO: transitions disabled until they don't anymore confuse SilicaListView positioning. JB#33215 + //ListView.onAdd: AddAnimation { target: listItem } + //ListView.onRemove: RemoveAnimation { target: listItem } + + menu: Component { + ContextMenu { + id: contextMenu + MenuItem { + //: Share a file + //% "Share" + text: qsTrId("sailfish-office-la-share") + onClicked: { + shareAction.resources = [model.filePath] + shareAction.trigger() + } + ShareAction { + id: shareAction + mimeType: model.fileMimeType + } + } + MenuItem { + //: Delete a file from the device + //% "Delete" + text: qsTrId("sailfish-office-me-delete") + onClicked: { + listItem.deleteFile() + } + } + } + } + } + + VerticalScrollDecorator { } + } +} diff --git a/usr/share/sailfish-office/Main.qml b/usr/share/sailfish-office/Main.qml new file mode 100644 index 00000000..b0481bf3 --- /dev/null +++ b/usr/share/sailfish-office/Main.qml @@ -0,0 +1,154 @@ +/* + * Copyright (c) 2013 - 2022 Jolla Ltd. + * + * 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; version 2 only. + * + * 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. + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Sailfish.Office 1.0 +import Sailfish.Office.Files 1.0 +import Nemo.FileManager 1.0 + +ApplicationWindow { + id: window + + readonly property Component coverPreview: pageStack.currentPage && (pageStack.currentPage.preview || null) + + property QtObject fileListModel: trackerProvider.model + property Page _mainPage + + allowedOrientations: defaultAllowedOrientations + _defaultLabelFormat: Text.PlainText + _defaultPageOrientations: Orientation.All + cover: Qt.resolvedUrl("CoverPage.qml") + initialPage: Component { + FileListPage { + id: fileListPage + + model: trackerProvider.model + provider: trackerProvider + Component.onCompleted: window._mainPage = fileListPage + } + } + + TrackerDocumentProvider { + id: trackerProvider + } + + FileInfo { + id: fileInfo + } + + // file = file or url + function openFile(file) { + fileInfo.url = file + + pageStack.pop(window._mainPage, PageStackAction.Immediate) + + var handler = "" + + switch (fileInfo.mimeType) { + case "application/vnd.oasis.opendocument.spreadsheet": + case "application/x-kspread": + case "application/vnd.ms-excel": + case "text/csv": + case "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": + case "application/vnd.openxmlformats-officedocument.spreadsheetml.template": + handler = "Sailfish.Office.SpreadsheetPage" + break + + case "application/vnd.oasis.opendocument.presentation": + case "application/vnd.oasis.opendocument.presentation-template": + case "application/x-kpresenter": + case "application/vnd.ms-powerpoint": + case "application/vnd.openxmlformats-officedocument.presentationml.presentation": + case "application/vnd.openxmlformats-officedocument.presentationml.template": + handler = "Sailfish.Office.PresentationPage" + break + + case "application/vnd.oasis.opendocument.text-master": + case "application/vnd.oasis.opendocument.text": + case "application/vnd.oasis.opendocument.text-template": + case "application/msword": + case "application/rtf": + case "application/x-mswrite": + case "application/vnd.openxmlformats-officedocument.wordprocessingml.document": + case "application/vnd.openxmlformats-officedocument.wordprocessingml.template": + case "application/vnd.ms-works": + handler = "Sailfish.Office.TextDocumentPage" + break + + case "text/plain": + handler = "Sailfish.Office.PlainTextDocumentPage" + break + + case "application/pdf": + handler = "Sailfish.Office.PDFDocumentPage" + break + + default: + console.log("Warning: Unrecognised file type for file " + fileInfo.file) + } + + if (handler != "") { + pageStack.push(handler, + { title: fileInfo.fileName, source: fileInfo.url, mimeType: fileInfo.mimeType }, + PageStackAction.Immediate) + } + + activate() + } + + function mimeToIcon(fileMimeType) { + var iconType = "other" + switch (fileMimeType) { + case "text/x-vnote": + iconType = "note" + break + case "application/pdf": + iconType = "pdf" + break + case "application/vnd.oasis.opendocument.spreadsheet": + case "application/x-kspread": + case "application/vnd.ms-excel": + case "text/csv": + case "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": + case "application/vnd.openxmlformats-officedocument.spreadsheetml.template": + iconType = "spreadsheet" + break + case "application/vnd.oasis.opendocument.presentation": + case "application/vnd.oasis.opendocument.presentation-template": + case "application/x-kpresenter": + case "application/vnd.ms-powerpoint": + case "application/vnd.openxmlformats-officedocument.presentationml.presentation": + case "application/vnd.openxmlformats-officedocument.presentationml.template": + iconType = "presentation" + break + case "text/plain": + case "application/vnd.oasis.opendocument.text-master": + case "application/vnd.oasis.opendocument.text": + case "application/vnd.oasis.opendocument.text-template": + case "application/msword": + case "application/rtf": + case "application/x-mswrite": + case "application/vnd.openxmlformats-officedocument.wordprocessingml.document": + case "application/vnd.openxmlformats-officedocument.wordprocessingml.template": + case "application/vnd.ms-works": + iconType = "formatted" + break + } + return "image://theme/icon-m-file-" + iconType + } +} diff --git a/usr/share/sailfish-office/SortTypeSelectionPage.qml b/usr/share/sailfish-office/SortTypeSelectionPage.qml new file mode 100644 index 00000000..6cd9368e --- /dev/null +++ b/usr/share/sailfish-office/SortTypeSelectionPage.qml @@ -0,0 +1,56 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Sailfish.Office.Files 1.0 + +Page { + id: root + + signal sortSelected(int sortType) + + SilicaListView { + anchors.fill: parent + model: sortModel + + header: PageHeader { + //% "Sort by" + title: qsTrId("sailfish_office-he-sort_by") + } + + delegate: BackgroundItem { + Label { + x: Theme.horizontalPageMargin + anchors.verticalCenter: parent.verticalCenter + text: name + color: highlighted ? Theme.highlightColor : Theme.primaryColor + } + + onClicked: root.sortSelected(sortType) + } + VerticalScrollDecorator {} + } + + ListModel { + id: sortModel + + ListElement { + sortType: FilterModel.Name + //: Sort by name + //% "Name" + name: qsTrId("sailfish_office-me-sort_name") + } + + ListElement { + sortType: FilterModel.Type + //: Sort by type + //% "Type" + name: qsTrId("sailfish_office-me-sort_type") + } + + ListElement { + sortType: FilterModel.Date + //: Sort by date + //% "Date" + name: qsTrId("sailfish_office-me-sort_date") + } + } +} diff --git a/usr/share/sailfish-share/ShareMethodItem.qml b/usr/share/sailfish-share/ShareMethodItem.qml index 1d3c0d29..85147452 100644 --- a/usr/share/sailfish-share/ShareMethodItem.qml +++ b/usr/share/sailfish-share/ShareMethodItem.qml @@ -1,10 +1,37 @@ /**************************************************************************************** -** -** Copyright (c) 2013 - 2021 Jolla Ltd. +** Copyright (c) 2013 - 2023 Jolla Ltd. ** Copyright (c) 2021 Open Mobile Platform LLC. +** ** All rights reserved. ** -** License: Proprietary. +** This file is part of Sailfish Transfer Engine component package. +** +** You may use this file under the terms of BSD license as follows: +** +** Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are met: +** +** 1. Redistributions of source code must retain the above copyright notice, this +** list of conditions and the following disclaimer. +** +** 2. Redistributions in binary form must reproduce the above copyright notice, +** this list of conditions and the following disclaimer in the documentation +** and/or other materials provided with the distribution. +** +** 3. Neither the name of the copyright holder nor the names of its +** contributors may be used to endorse or promote products derived from +** this software without specific prior written permission. +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +** AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +** IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +** DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +** FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +** DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +** SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +** CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +** OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ** ****************************************************************************************/ import QtQuick 2.6 @@ -44,6 +71,7 @@ BackgroundItem { } truncationMode: TruncationMode.Fade text: model.displayName + textFormat: Text.PlainText } Label { @@ -60,6 +88,7 @@ BackgroundItem { text: model.subtitle font.pixelSize: Theme.fontSizeExtraSmall color: highlighted ? Theme.secondaryHighlightColor : Theme.secondaryColor + textFormat: Text.PlainText } } diff --git a/usr/share/sailfish-share/ShareSystemDialog.qml b/usr/share/sailfish-share/ShareSystemDialog.qml index f1555c91..96aba992 100644 --- a/usr/share/sailfish-share/ShareSystemDialog.qml +++ b/usr/share/sailfish-share/ShareSystemDialog.qml @@ -1,9 +1,37 @@ /**************************************************************************************** -** ** Copyright (c) 2021 Open Mobile Platform LLC. +** Copyright (c) 2023 Jolla Ltd. +** ** All rights reserved. ** -** License: Proprietary. +** This file is part of Sailfish Transfer Engine component package. +** +** You may use this file under the terms of BSD license as follows: +** +** Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are met: +** +** 1. Redistributions of source code must retain the above copyright notice, this +** list of conditions and the following disclaimer. +** +** 2. Redistributions in binary form must reproduce the above copyright notice, +** this list of conditions and the following disclaimer in the documentation +** and/or other materials provided with the distribution. +** +** 3. Neither the name of the copyright holder nor the names of its +** contributors may be used to endorse or promote products derived from +** this software without specific prior written permission. +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +** AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +** IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +** DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +** FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +** DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +** SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +** CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +** OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ** ****************************************************************************************/ import QtQuick 2.6 @@ -215,6 +243,7 @@ SystemDialog { running: !delayLoadingIndicator.running && !shareMethodsColumn.visible && shareMethodLoader.status === Loader.Null + && !sharingMethodsModel.ready } Timer { diff --git a/usr/share/sailfish-share/main.qml b/usr/share/sailfish-share/main.qml index 89c2bd52..2b1a27b0 100644 --- a/usr/share/sailfish-share/main.qml +++ b/usr/share/sailfish-share/main.qml @@ -1,9 +1,37 @@ /**************************************************************************************** -** ** Copyright (c) 2021 Open Mobile Platform LLC. +** Copyright (c) 2023 Jolla Ltd. +** ** All rights reserved. ** -** License: Proprietary. +** This file is part of Sailfish Transfer Engine component package. +** +** You may use this file under the terms of BSD license as follows: +** +** Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are met: +** +** 1. Redistributions of source code must retain the above copyright notice, this +** list of conditions and the following disclaimer. +** +** 2. Redistributions in binary form must reproduce the above copyright notice, +** this list of conditions and the following disclaimer in the documentation +** and/or other materials provided with the distribution. +** +** 3. Neither the name of the copyright holder nor the names of its +** contributors may be used to endorse or promote products derived from +** this software without specific prior written permission. +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +** AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +** IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +** DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +** FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +** DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +** SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +** CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +** OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ** ****************************************************************************************/ import QtQuick 2.6 diff --git a/usr/share/sailfish-share/plugin/AppShareMethodPlugin.qml b/usr/share/sailfish-share/plugin/AppShareMethodPlugin.qml new file mode 100644 index 00000000..7ba5dec2 --- /dev/null +++ b/usr/share/sailfish-share/plugin/AppShareMethodPlugin.qml @@ -0,0 +1,113 @@ +/**************************************************************************************** +** Copyright (c) 2021 Open Mobile Platform LLC. +** Copyright (c) 2023 Jolla Ltd. +** +** All rights reserved. +** +** This file is part of Sailfish Transfer Engine component package. +** +** You may use this file under the terms of BSD license as follows: +** +** Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are met: +** +** 1. Redistributions of source code must retain the above copyright notice, this +** list of conditions and the following disclaimer. +** +** 2. Redistributions in binary form must reproduce the above copyright notice, +** this list of conditions and the following disclaimer in the documentation +** and/or other materials provided with the distribution. +** +** 3. Neither the name of the copyright holder nor the names of its +** contributors may be used to endorse or promote products derived from +** this software without specific prior written permission. +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +** AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +** IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +** DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +** FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +** DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +** SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +** CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +** OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +** +****************************************************************************************/ + +import QtQuick 2.6 +import Sailfish.Silica 1.0 +import Nemo.DBus 2.0 +import Sailfish.Share.AppShare 1.0 + +Item { + property var shareAction + width: parent.width + height: busy.running ? busy.height : errorLabel.height + Theme.paddingLarge + + InfoLabel { + id: errorLabel + + property string error + property int fileCount + + text: { + // User should not see , include it just for completeness + var name = shareAction ? shareAction.selectedTransferMethodInfo.displayName : "" + if (error === "") { + return "" + } else if (error === "org.freedesktop.DBus.Error.InvalidArgs") { + if (fileCount == 1) { + //: The target application (%1) was given one file that it didn't understand + //% "The file can not be shared to %1" + return qsTrId("sailfishshare-la-error_invalid_args_single_file").arg(name) + } else { + //: The target application (%1) was given multiple files and it didn't understand some of them + //% "The files can not be shared to %1" + return qsTrId("sailfishshare-la-error_invalid_args_multiple_files").arg(name) + } + } else { + //: Something went wrong while sharing to the target application (%1) + //% "Failed to share to %1" + return qsTrId("sailfishshare-la-general_error").arg(name) + } + } + } + + BusyIndicator { + id: busy + + anchors.horizontalCenter: parent.horizontalCenter + height: Theme.itemSizeLarge + running: errorLabel.error === "" + } + + ShareMethodInfo { + id: info + + readonly property bool ready: service !== "" && path !== "" && iface !== "" + + methodId: shareAction.selectedTransferMethodInfo.methodId + + onReadyChanged: { + if (ready) { + var config = shareAction.toConfiguration() + errorLabel.fileCount = config["resources"].length + app.call("share", [config], function() { + shareAction.done() + }, function(error, message) { + errorLabel.error = error + console.warn("Failed to share:", error, "with message:", message) + }) + } + } + } + + DBusInterface { + id: app + + service: info.service + path: info.path + iface: info.iface + } +} diff --git a/usr/share/sailfish-utilities/ActionList.qml b/usr/share/sailfish-utilities/ActionList.qml new file mode 100644 index 00000000..0ec44595 --- /dev/null +++ b/usr/share/sailfish-utilities/ActionList.qml @@ -0,0 +1,49 @@ +/** + * @file ActionList.qml + * @brief List of available actions + * @copyright (C) 2014 Jolla Ltd. + * @par License: LGPL 2.1 http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 + +Column { + + width: parent.width + + signal done(string name) + signal error(string name, string error) + + PageHeader { + //% "Utilities" + title: qsTrId("sailfish-tools-utilities") + } + + ListModel { + id: plugins + } + Component.onCompleted: { + var justLoad = function(name) { + var info = { name: name, path: "plugins/" + name + ".qml" } + plugins.append(info) + } + var names = [ "RestartNetwork", "RestartKeyboard", "RestartUI", + "RestartFingerprint", "RestartAudio", "RestartBluetooth", + "CleanPackageCache", "CleanTracker" + ] + for (var i = 0; i < names.length; ++i) + justLoad(names[i]) + } + Column { + width: parent.width + spacing: Theme.paddingLarge + Repeater { + model: plugins + Loader { + source: path + width: parent.width + } + } + } +} diff --git a/usr/share/sailfish-utilities/MainPage.qml b/usr/share/sailfish-utilities/MainPage.qml new file mode 100644 index 00000000..131af5ad --- /dev/null +++ b/usr/share/sailfish-utilities/MainPage.qml @@ -0,0 +1,102 @@ +/** + * @file MainPage.qml + * @copyright (C) 2014 Jolla Ltd. + * @par License: LGPL 2.1 http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import com.jolla.settings.system 1.0 +import Nemo.Notifications 1.0 +import org.nemomobile.devicelock 1.0 +import Nemo.DBus 2.0 + +Page { + id: mainPage + + property bool inProgress: false + + backNavigation: !inProgress + + DeviceLockQuery { + id: deviceLockQuery + } + + DeviceLockSettings { + id: deviceLockSettings + } + + DBusInterface { + id: dsmeDbus + bus: DBus.SystemBus + service: "com.nokia.dsme" + path: "/com/nokia/dsme/request" + iface: "com.nokia.dsme.request" + } + + Timer { + id: rebootTimer + interval: 1000 + onTriggered: dsmeDbus.call("req_reboot", []) + } + + function reboot() { + rebootTimer.start() + } + + function requestSecurityCode(on_ok) { + deviceLockQuery.authenticate(deviceLockSettings.authorization, + function(authenticationToken) { + console.log("Security code is ok or not used") + pageStack.pop(mainPage) + on_ok() + }, function () { + pageStack.pop(mainPage) + }) + } + + function actionIsDone(category, message) { + console.log("Notify", message); + //% "Sailfish Utilities" + notification.previewBody = qsTrId("sailfish-utilities-me-name"); + notification.previewSummary = message; + notification.close(); + notification.publish(); + } + + SilicaFlickable { + id: mainView + anchors.fill: parent + contentHeight: actionList.height + Theme.paddingLarge + + VerticalScrollDecorator { flickable: mainView } + + ActionList { + id: actionList + + opacity: mainPage.inProgress ? 0.0 : 1.0 + Behavior on opacity { FadeAnimation {} } + onDone: { + //% "%1: OK" + var message = qsTrId("sailfish-utilities-me-notification-ok").arg(name); + mainPage.actionIsDone("info", message); + } + onError: { + console.log(error); + //% "%1: failed" + var message = qsTrId("sailfish-utilities-me-notification-err").arg(name); + mainPage.actionIsDone("error", message) + } + } + } + Notification { + id: notification + icon: "icon-m-health" + } + + BusyIndicator { + anchors.centerIn: parent + size: BusyIndicatorSize.Large + running: mainPage.inProgress + } +} diff --git a/usr/share/sailfish-utilities/plugins/CleanPackageCache.qml b/usr/share/sailfish-utilities/plugins/CleanPackageCache.qml new file mode 100644 index 00000000..4861e46e --- /dev/null +++ b/usr/share/sailfish-utilities/plugins/CleanPackageCache.qml @@ -0,0 +1,23 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Sailfish.Utilities 1.0 + +ActionItem { + //% "Package cache" + title: qsTrId("sailfish-tools-he-packace_cache") + //% "Clean" + actionName: qsTrId("sailfish-tools-bt-clean") + //: Text surrounded by %1 and %2 becomes a hyperlink: %1 is replaced by and %2 by . + //% "Package cache cleaning can be tried if there are " + //% "problems with store, e.g. 'Critical problem with the app registry' error. " + //% "More information can be found " + //% "%1here%2." + description: qsTrId("sailfish-utilities-me-clean-pkg-cache-desc-url") + .arg("") + .arg("") + requiresReboot: true + + function action(on_reply, on_error) { + UtilTools.cleanRpmDb(on_reply, on_error) + } +} diff --git a/usr/share/sailfish-utilities/plugins/CleanTracker.qml b/usr/share/sailfish-utilities/plugins/CleanTracker.qml new file mode 100644 index 00000000..e4456064 --- /dev/null +++ b/usr/share/sailfish-utilities/plugins/CleanTracker.qml @@ -0,0 +1,24 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Sailfish.Utilities 1.0 + +ActionItem { + //% "Tracker database" + title: qsTrId("sailfish-tools-he-tracker_database") + //% "Clean" + actionName: qsTrId("sailfish-tools-bt-clean") + //: Text surrounded by %1 and %2 becomes a hyperlink: %1 is replaced by and %2 by . + //% "Tracker dabatabase cleaning can help in cases with " + //% "missing images, audio files etc. Processes using tracker " + //% "will be closed, tracker reindexing will be started. " + //% "More information can be found " + //% "%1here%2." + description: qsTrId("sailfish-utilities-me-clean-tracker-db-desc-url") + .arg("") + .arg("") + deviceLockRequired: false + + function action(on_reply, on_error) { + UtilTools.cleanTrackerDb(on_reply, on_error) + } +} diff --git a/usr/share/sailfish-utilities/plugins/RestartAudio.qml b/usr/share/sailfish-utilities/plugins/RestartAudio.qml new file mode 100644 index 00000000..1de7615c --- /dev/null +++ b/usr/share/sailfish-utilities/plugins/RestartAudio.qml @@ -0,0 +1,26 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Sailfish.Utilities 1.0 +import Nemo.DBus 2.0 + +ActionItem { + //% "Audio" + title: qsTrId("sailfish-tools-he-audio") + //% "Restart" + actionName: qsTrId("sailfish-tools-bt-restart") + deviceLockRequired: false + //% "Restart Audio subsystem, which may help if you lose audio." + description: qsTrId("sailfish-utilities-me-restart-audio-desc") + + DBusInterface { + id: service + bus: DBus.SessionBus + service: 'org.freedesktop.systemd1' + path: '/org/freedesktop/systemd1/unit/pulseaudio_2eservice' + iface: 'org.freedesktop.systemd1.Unit' + } + + function action(on_reply, on_error) { + service.call("Restart", ["replace"], on_reply, on_error) + } +} diff --git a/usr/share/sailfish-utilities/plugins/RestartBluetooth.qml b/usr/share/sailfish-utilities/plugins/RestartBluetooth.qml new file mode 100644 index 00000000..a375a58f --- /dev/null +++ b/usr/share/sailfish-utilities/plugins/RestartBluetooth.qml @@ -0,0 +1,19 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Sailfish.Utilities 1.0 + +ActionItem { + //% "Bluetooth" + title: qsTrId("sailfish-tools-he-bluetooth") + //% "Restart" + actionName: qsTrId("sailfish-tools-bt-restart") + deviceLockRequired: false + //% "Restart Bluetooth subsystem, which may help if you're " + //% "having trouble scanning or connecting to a Bluetooth " + //% "device which worked previously." + description: qsTrId("sailfish-utilities-me-restart_bluetooth_desc") + + function action(on_reply, on_error) { + UtilTools.restartBluetooth(on_reply, on_error) + } +} diff --git a/usr/share/sailfish-utilities/plugins/RestartFingerprint.qml b/usr/share/sailfish-utilities/plugins/RestartFingerprint.qml new file mode 100644 index 00000000..de1fa1a5 --- /dev/null +++ b/usr/share/sailfish-utilities/plugins/RestartFingerprint.qml @@ -0,0 +1,19 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Sailfish.Utilities 1.0 + +ActionItem { + //% "Fingerprint" + title: qsTrId("sailfish-tools-he-fingerprint") + //% "Restart" + actionName: qsTrId("sailfish-tools-bt-fingerprint_restart") + deviceLockRequired: false + //% "Restart fingerprint service. In some circumstancces this can " + //% "resolve issues where valid fingerprints are no longer being " + //% "recognised." + description: qsTrId("sailfish-utilities-restart-fingerprint_desc") + + function action(on_reply, on_error) { + UtilTools.restartFingerprint(on_reply, on_error) + } +} diff --git a/usr/share/sailfish-utilities/plugins/RestartKeyboard.qml b/usr/share/sailfish-utilities/plugins/RestartKeyboard.qml new file mode 100644 index 00000000..3a7ff36a --- /dev/null +++ b/usr/share/sailfish-utilities/plugins/RestartKeyboard.qml @@ -0,0 +1,27 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Sailfish.Utilities 1.0 +import Nemo.DBus 2.0 + +ActionItem { + //% "Keyboard" + title: qsTrId("sailfish-tools-he-keyboard") + //% "Restart" + actionName: qsTrId("sailfish-tools-bt-restart") + deviceLockRequired: false + //% "Restart the keyboard if it becomes unresponsive, keys are missing, " + //% "or if it behaves otherwise incorrectly." + description: qsTrId("sailfish-utilities-me-restart-keyboard-desc") + + DBusInterface { + id: service + bus: DBus.SessionBus + service: 'org.freedesktop.systemd1' + path: '/org/freedesktop/systemd1/unit/maliit_2dserver_2eservice' + iface: 'org.freedesktop.systemd1.Unit' + } + + function action(on_reply, on_error) { + service.call("Restart", ["replace"], on_reply, on_error) + } +} diff --git a/usr/share/sailfish-utilities/plugins/RestartNetwork.qml b/usr/share/sailfish-utilities/plugins/RestartNetwork.qml new file mode 100644 index 00000000..d8f5cebd --- /dev/null +++ b/usr/share/sailfish-utilities/plugins/RestartNetwork.qml @@ -0,0 +1,18 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Sailfish.Utilities 1.0 + +ActionItem { + //% "Network" + title: qsTrId("sailfish-tools-he-network") + //% "Restart" + actionName: qsTrId("sailfish-tools-bt-restart") + deviceLockRequired: false + //% "Restart network subsystem if anything wrong happened with " + //% "connectivity (WLAN, mobile data)." + description: qsTrId("sailfish-utilities-me-restart-network-desc") + + function action(on_reply, on_error) { + UtilTools.restartNetwork(on_reply, on_error) + } +} diff --git a/usr/share/sailfish-utilities/plugins/RestartUI.qml b/usr/share/sailfish-utilities/plugins/RestartUI.qml new file mode 100644 index 00000000..267a7cd3 --- /dev/null +++ b/usr/share/sailfish-utilities/plugins/RestartUI.qml @@ -0,0 +1,19 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Sailfish.Utilities 1.0 + +ActionItem { + //% "Home Screen" + title: qsTrId("sailfish-tools-he-home_screen") + //% "Restart + actionName: qsTrId("sailfish-tools-bt-restart") + deviceLockRequired: false + //% "Restart Home Screen, closing all runnning applications. " + //% "It'd be helpful if some application is hanged or can't be started, or " + //% "user experience any issues with keyboard, clipboard, home screen etc." + description: qsTrId("sailfish-utilities-restart-ui-desc") + + function action(on_reply, on_error) { + UtilTools.restartLipstick(on_reply, on_error) + } +} diff --git a/usr/share/sailfish-vpn/l2tp/import.qml b/usr/share/sailfish-vpn/l2tp/import.qml index 95cba5eb..98fa6af8 100644 --- a/usr/share/sailfish-vpn/l2tp/import.qml +++ b/usr/share/sailfish-vpn/l2tp/import.qml @@ -1,11 +1,11 @@ import QtQuick 2.0 import Sailfish.Settings.Networking.Vpn 1.0 -import org.nemomobile.systemsettings 1.0 as SystemSettings +import Nemo.Connectivity 1.0 as Connectivity QtObject { property string mimeType: 'application/x-l2tp' property string vpnType: 'l2tp' function parseFile(fileName) { - return SystemSettings.SettingsVpnModel.processProvisioningFile(fileName, "l2tp") + return Connectivity.SettingsVpnModel.processProvisioningFile(fileName, "l2tp") } } diff --git a/usr/share/sailfish-vpn/openconnect/import.qml b/usr/share/sailfish-vpn/openconnect/import.qml index 440e18d6..3bbdc501 100644 --- a/usr/share/sailfish-vpn/openconnect/import.qml +++ b/usr/share/sailfish-vpn/openconnect/import.qml @@ -1,10 +1,10 @@ import QtQuick 2.0 import Sailfish.Settings.Networking.Vpn 1.0 -import org.nemomobile.systemsettings 1.0 as SystemSettings +import Nemo.Connectivity 1.0 as Connectivity QtObject { property string mimeType: 'application/x-openconnect' function parseFile(fileName) { - return SystemSettings.SettingsVpnModel.processProvisioningFile(fileName, "openconnect") + return Connectivity.SettingsVpnModel.processProvisioningFile(fileName, "openconnect") } } diff --git a/usr/share/sailfish-vpn/openfortivpn/advanced.qml b/usr/share/sailfish-vpn/openfortivpn/advanced.qml new file mode 100644 index 00000000..d034ceb7 --- /dev/null +++ b/usr/share/sailfish-vpn/openfortivpn/advanced.qml @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2020 Jolla Ltd. + * Copyright (c) 2020 Open Mobile Platform LLC. + * + * License: Proprietary + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import org.nemomobile.systemsettings 1.0 +import Sailfish.Settings.Networking 1.0 +import Sailfish.Settings.Networking.Vpn 1.0 + +Column { + function setProperties(providerProperties) { + openfortivpnPort.text = getProperty('openfortivpn.Port') + // This option is for PPPD + openfortivpnNoIPv6.checked = getProperty('PPPD.NoIPv6') === 'true' + openfortivpnAllowSelfSignedCert.checked = getProperty('openfortivpn.AllowSelfSignedCert') === 'true' + openfortivpnTrustedCert.text = getProperty('openfortivpn.TrustedCert') + } + + function updateProperties(providerProperties) { + updateProvider('openfortivpn.Port', openfortivpnPort.filteredText) + updateProvider('PPPD.NoIPv6', openfortivpnNoIPv6.checked.toString()) + updateProvider('openfortivpn.AllowSelfSignedCert', openfortivpnAllowSelfSignedCert.checked ? 'true' : 'false') + updateProvider('openfortivpn.TrustedCert', openfortivpnTrustedCert.text) + } + + width: parent.width + + SectionHeader { + //: Settings pertaining to the communication channel + //% "Communications" + text: qsTrId("settings_network-he-vpn_openfortivpn_communications") + } + + ConfigIntField { + id: openfortivpnPort + intUpperLimit: 65535 + + //% "Port" + label: qsTrId("settings_network-la-vpn_openfortivpn_port") + //% "Port number must be a value between 1 and 65535" + description: errorHighlight ? qsTrId("settings_network_la-vpn_openfortivpn_port_error") : "" + + nextFocusItem: openfortivpnTrustedCert + } + + TextSwitch { + id: openfortivpnNoIPv6 + + //% "Disable IPv6 (enables IPv6 data leak protection)" + text: qsTrId("settings_network-la-vpn_pppd_noipv6") + } + + SectionHeader { + //% "Server" + text: qsTrId("settings_network-la-vpn_openfortivpn_server") + } + + TextSwitch { + id: openfortivpnAllowSelfSignedCert + + //% "Allow self signed certificate" + text: qsTrId("settings_network-la-vpn_openfortivpn_allow_self_signed_certificate") + } + + ConfigTextField { + id: openfortivpnTrustedCert + + //% "Trusted certificate fingerprint" + label: qsTrId("settings_network-la-vpn_openfortivpn_trusted_cert_fingerprint") + } +} diff --git a/usr/share/sailfish-vpn/openfortivpn/details.qml b/usr/share/sailfish-vpn/openfortivpn/details.qml new file mode 100644 index 00000000..bb9dcb5a --- /dev/null +++ b/usr/share/sailfish-vpn/openfortivpn/details.qml @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2020 Jolla Ltd. + * Copyright (c) 2020 Open Mobile Platform LLC. + * + * License: Proprietary + */ + + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Sailfish.Settings.Networking.Vpn 1.0 +import Sailfish.Settings.Networking 1.0 + +VpnPlatformDetailsPage { + // The VPN plugin for Fortinet VPN is called a OpenFortiVPN, so the VPN name should be used here. + //% "Fortinet" + subtitle: qsTrId("settings_network-me-vpn_type_openfortivpn") +} diff --git a/usr/share/sailfish-vpn/openfortivpn/edit.qml b/usr/share/sailfish-vpn/openfortivpn/edit.qml new file mode 100644 index 00000000..2eeb3a74 --- /dev/null +++ b/usr/share/sailfish-vpn/openfortivpn/edit.qml @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2020 Jolla Ltd. + * Copyright (c) 2020 Open Mobile Platform LLC. + * + * License: Proprietary + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import org.nemomobile.systemsettings 1.0 +import Sailfish.Settings.Networking 1.0 +import Sailfish.Settings.Networking.Vpn 1.0 + +VpnPlatformEditDialog { + //% "Add new openfortivpn connection" + newTitle: qsTrId("settings_network-he-vpn_add_new_openfortivpn") + //% "Edit openfortivpn connection" + editTitle: qsTrId("settings_network-he-vpn_edit_openfortivpn") + //% "Openfortivpn set up is ready!" + importTitle: qsTrId("settings_network-he-vpn_import_openfortivpn_success") + + Binding on subtitle { + when: newConnection && importPath + //% "Settings have been imported. You can change the settings now or later after saving the connection. If username and password are required, they will be requested after turning on the connection." + value: qsTrId("settings_network-he-vpn_import_openfortivpn_message") + } + + vpnType: "openfortivpn" + + onAccepted: saveConnection() + Component.onCompleted: init() +} diff --git a/usr/share/sailfish-vpn/openfortivpn/import.qml b/usr/share/sailfish-vpn/openfortivpn/import.qml new file mode 100644 index 00000000..db15a1a0 --- /dev/null +++ b/usr/share/sailfish-vpn/openfortivpn/import.qml @@ -0,0 +1,11 @@ +import QtQuick 2.0 +import Sailfish.Settings.Networking.Vpn 1.0 +import Nemo.Connectivity 1.0 as Connectivity + +QtObject { + property string mimeType: 'application/x-openfortivpn' + property string vpnType: 'openfortivpn' + function parseFile(fileName) { + return Connectivity.SettingsVpnModel.processProvisioningFile(fileName, "openfortivpn") + } +} diff --git a/usr/share/sailfish-vpn/openfortivpn/importdialog.qml b/usr/share/sailfish-vpn/openfortivpn/importdialog.qml new file mode 100644 index 00000000..8a755db5 --- /dev/null +++ b/usr/share/sailfish-vpn/openfortivpn/importdialog.qml @@ -0,0 +1,21 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import org.nemomobile.systemsettings 1.0 +import Sailfish.Settings.Networking 1.0 +import Sailfish.Settings.Networking.Vpn 1.0 + +VpnImportDialog { + //% "Import Fortinet config file" + title: qsTrId("settings_network-he-vpn_import_openfortivpn") + //% "Import of Fortinet config file failed" + failTitle: qsTrId("settings_network-he-vpn_import_openfortivpn_failed") + + //% "Use either forticlient .conn or openfortivpn config file. " + //% "Importing a file makes the set up process easier by filling out many options automatically." + //% "

Choose 'Skip' to set up openfortivpn manually." + message: qsTrId("settings_network-he-vpn_import_openfortivpn_desc") + //% "Choose 'Try again' to choose another file, or choose 'Skip' to set up openfortivpn manually." + failMessage: qsTrId("settings_network-he-vpn_import_openfortivpn_failed_desc") + + nameFilters: [ '*.conn', '*.conf' ] +} diff --git a/usr/share/sailfish-vpn/openfortivpn/listitem.qml b/usr/share/sailfish-vpn/openfortivpn/listitem.qml new file mode 100644 index 00000000..e0ab339d --- /dev/null +++ b/usr/share/sailfish-vpn/openfortivpn/listitem.qml @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2020 Jolla Ltd. + * Copyright (c) 2020 Open Mobile Platform LLC. + * + * License: Proprietary + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Sailfish.Settings.Networking.Vpn 1.0 +import Sailfish.Settings.Networking 1.0 + +VpnTypeItem { + canImport: true + onClicked: { + pageStack.animatorPush("OfvFileSettingsDialog.qml", { mainPage: _mainPage }) + } + + //% "Fortinet" + name: qsTrId("settings_network-me-vpn_type_openfortivpn") + + //% "An open implementation of Fortinet's proprietary PPP+SSL VPN solution" + description: qsTrId("settings_network-la-vpn_type_openfortivpn") +} diff --git a/usr/share/sailfish-vpn/openvpn/advanced.qml b/usr/share/sailfish-vpn/openvpn/advanced.qml index 0c103363..cc06ab13 100644 --- a/usr/share/sailfish-vpn/openvpn/advanced.qml +++ b/usr/share/sailfish-vpn/openvpn/advanced.qml @@ -17,6 +17,8 @@ Column { openVpnNSCertType.setValue(getProperty('OpenVPN.NSCertType')) openVpnRemoteCertTLS.setValue(getProperty('OpenVPN.RemoteCertTls')) openVpnCipher.text = getProperty('OpenVPN.Cipher') + openVpnDataCiphers.text = getProperty('OpenVPN.DataCiphers') + openVpnDataCiphersFallback.text = getProperty('OpenVPN.DataCiphersFallback') openVpnAuth.text = getProperty('OpenVPN.Auth') openVpnMTU.text = getProperty('OpenVPN.MTU') openVpnDeviceType.setValue(getProperty('OpenVPN.DeviceType')) @@ -39,6 +41,8 @@ Column { updateProvider('OpenVPN.NSCertType', openVpnNSCertType.selection) updateProvider('OpenVPN.RemoteCertTls', openVpnRemoteCertTLS.selection) updateProvider('OpenVPN.Cipher', openVpnCipher.text) + updateProvider('OpenVPN.DataCiphers', openVpnDataCiphers.text) + updateProvider('OpenVPN.DataCiphersFallback', openVpnDataCiphersFallback.text) updateProvider('OpenVPN.Auth', openVpnAuth.text) updateProvider('OpenVPN.MTU', openVpnMTU.filteredText) updateProvider('OpenVPN.DeviceType', openVpnDeviceType.selection) @@ -196,6 +200,22 @@ Column { //% "Cipher algorithm" label: qsTrId("settings_network-la-vpn_openvpn_cipher") + nextFocusItem: openVpnDataCiphers + } + + ConfigTextField { + id: openVpnDataCiphers + + //% "Available cipher algorithms to use" + label: qsTrId("settings_network-la-vpn_openvpn_data_ciphers") + nextFocusItem: openVpnDataCiphersFallback + } + + ConfigTextField { + id: openVpnDataCiphersFallback + + //% "Fallback cipher algorithm" + label: qsTrId("settings_network-la-vpn_openvpn_data_ciphers_fallback") nextFocusItem: openVpnAuth } diff --git a/usr/share/sailfish-vpn/openvpn/import.qml b/usr/share/sailfish-vpn/openvpn/import.qml index 8e1f4412..bdc9938a 100644 --- a/usr/share/sailfish-vpn/openvpn/import.qml +++ b/usr/share/sailfish-vpn/openvpn/import.qml @@ -1,11 +1,11 @@ import QtQuick 2.0 import Sailfish.Settings.Networking.Vpn 1.0 -import org.nemomobile.systemsettings 1.0 as SystemSettings +import Nemo.Connectivity 1.0 as Connectivity QtObject { property string mimeType: 'application/x-openvpn' property string vpnType: 'openvpn' function parseFile(fileName) { - return SystemSettings.SettingsVpnModel.processProvisioningFile(fileName, "openvpn") + return Connectivity.SettingsVpnModel.processProvisioningFile(fileName, "openvpn") } } diff --git a/usr/share/sailfish-vpn/pptp/import.qml b/usr/share/sailfish-vpn/pptp/import.qml index ba730e10..7172df2c 100644 --- a/usr/share/sailfish-vpn/pptp/import.qml +++ b/usr/share/sailfish-vpn/pptp/import.qml @@ -1,11 +1,11 @@ import QtQuick 2.0 import Sailfish.Settings.Networking.Vpn 1.0 -import org.nemomobile.systemsettings 1.0 as SystemSettings +import Nemo.Connectivity 1.0 as Connectivity QtObject { property string mimeType: 'application/x-pptp' property string vpnType: 'pptp' function parseFile(fileName) { - return SystemSettings.SettingsVpnModel.processProvisioningFile(fileName, "pptp") + return Connectivity.SettingsVpnModel.processProvisioningFile(fileName, "pptp") } } diff --git a/usr/share/sailfish-vpn/vpnc/import.qml b/usr/share/sailfish-vpn/vpnc/import.qml index 7caabeac..85fb296e 100644 --- a/usr/share/sailfish-vpn/vpnc/import.qml +++ b/usr/share/sailfish-vpn/vpnc/import.qml @@ -1,10 +1,10 @@ import QtQuick 2.0 import Sailfish.Settings.Networking.Vpn 1.0 -import org.nemomobile.systemsettings 1.0 as SystemSettings +import Nemo.Connectivity 1.0 as Connectivity QtObject { property string mimeType: 'application/x-vpnc' function parseFile(fileName) { - return SystemSettings.SettingsVpnModel.processProvisioningFile(fileName, "vpnc") + return Connectivity.SettingsVpnModel.processProvisioningFile(fileName, "vpnc") } } diff --git a/usr/share/sailfish-weather/cover/CurrentWeatherCover.qml b/usr/share/sailfish-weather/cover/CurrentWeatherCover.qml new file mode 100644 index 00000000..e9d0fe2e --- /dev/null +++ b/usr/share/sailfish-weather/cover/CurrentWeatherCover.qml @@ -0,0 +1,47 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Sailfish.Weather 1.0 + +Item { + WeatherCoverItem { + x: Theme.paddingLarge + width: parent.width - 2*x + topPadding: Theme.paddingLarge + text: (weather.status === Weather.Error || weather.status === Weather.Unauthorized) ? weather.city : TemperatureConverter.format(weather.temperature) + " " + weather.city + description: { + if (weather.status === Weather.Error) { + //% "Loading failed" + return qsTrId("weather-la-loading_failed") + } else if (weather.status === Weather.Unauthorized) { + //% "Invalid authentication credentials" + return qsTrId("weather-la-unauthorized") + } + + return weather.description + } + } + WeatherImage { + id: weatherImage + + height: width + width: parent.width - Theme.paddingLarge + sourceSize.width: width + sourceSize.height: width + weatherType: weather ? weather.weatherType : "" + anchors { + centerIn: parent + verticalCenterOffset: Theme.paddingSmall + } + } + Image { + scale: 0.5 + opacity: 0.5 + anchors { + bottom: parent.bottom + bottomMargin: Math.round(Theme.paddingSmall/2) + horizontalCenter: parent.horizontalCenter + } + source: "image://theme/graphic-foreca-small" + } + +} diff --git a/usr/share/sailfish-weather/cover/WeatherCover.qml b/usr/share/sailfish-weather/cover/WeatherCover.qml new file mode 100644 index 00000000..82d3b4c3 --- /dev/null +++ b/usr/share/sailfish-weather/cover/WeatherCover.qml @@ -0,0 +1,98 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Sailfish.Weather 1.0 + +CoverBackground { + id: cover + + property QtObject weather: savedWeathersModel.currentWeather + + property bool current: true + property bool ready: loaded && !error && !unauthorized + property bool loaded: weather + property bool error: loaded && savedWeathersModel.currentWeather.status == Weather.Error + property bool unauthorized: loaded && savedWeathersModel.currentWeather.status == Weather.Unauthorized + + function reload() { + if (current) { + if (savedWeathersModel.currentWeather && currentWeatherModel.updateAllowed()) { + currentWeatherModel.reload() + } + } else if (savedWeathersModel.count > 1) { + weatherApplication.reloadAllIfAllowed() + } + } + + onStatusChanged: if (status == Cover.Active) reload() + onCurrentChanged: reload() + + CoverPlaceholder { + visible: !ready + icon.source: "image://theme/graphic-foreca-large" + text: { + if (!loaded) { + //% "Select location to check weather" + return qsTrId("weather-la-select_location_to_check_weather") + } else if (error) { + //% "Unable to connect, try again" + return qsTrId("weather-la-unable_to_connect_try_again") + } else if (unauthorized) { + //% "Invalid authentication credentials" + return qsTrId("weather-la-unauthorized") + } + + return "" + } + } + Loader { + active: ready + opacity: ready && current ? 1.0 : 0.0 + source: "CurrentWeatherCover.qml" + Behavior on opacity { FadeAnimation {} } + anchors.fill: parent + } + Loader { + active: ready && savedWeathersModel.count > 0 + opacity: ready && !current ? 1.0 : 0.0 + source: "WeatherListCover.qml" + Behavior on opacity { FadeAnimation {} } + anchors.fill: parent + } + + CoverActionList { + enabled: !loaded + CoverAction { + iconSource: "image://theme/icon-cover-search" + onTriggered: { + var alreadyOpen = pageStack.currentPage && pageStack.currentPage.objectName === "LocationSearchPage" + if (!alreadyOpen) { + pageStack.push(Qt.resolvedUrl("../pages/LocationSearchPage.qml"), undefined, PageStackAction.Immediate) + } + weatherApplication.activate() + } + } + } + CoverActionList { + enabled: error + CoverAction { + iconSource: "image://theme/icon-cover-sync" + onTriggered: { + weatherApplication.reloadAll() + } + } + } + CoverActionList { + enabled: ready && savedWeathersModel.count > 0 + CoverAction { + iconSource: current ? "image://theme/icon-cover-previous" + : "image://theme/icon-cover-next" + onTriggered: { + current = !current + } + } + } + Connections { + target: savedWeathersModel + onCountChanged: if (savedWeathersModel.count === 0) current = true + } +} diff --git a/usr/share/sailfish-weather/cover/WeatherCoverItem.qml b/usr/share/sailfish-weather/cover/WeatherCoverItem.qml new file mode 100644 index 00000000..5f87a838 --- /dev/null +++ b/usr/share/sailfish-weather/cover/WeatherCoverItem.qml @@ -0,0 +1,25 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 + +Column { + property alias text: primaryLabel.text + property alias description: secondaryLabel.text + property alias topPadding: topPaddingItem.height + + Item { + id: topPaddingItem + width: parent.width + } + Label { + id: primaryLabel + width: parent.width + truncationMode: TruncationMode.Fade + } + Label { + id: secondaryLabel + width: parent.width + truncationMode: TruncationMode.Fade + font.pixelSize: Theme.fontSizeExtraSmall + color: Theme.secondaryColor + } +} diff --git a/usr/share/sailfish-weather/cover/WeatherListCover.qml b/usr/share/sailfish-weather/cover/WeatherListCover.qml new file mode 100644 index 00000000..d0d9e094 --- /dev/null +++ b/usr/share/sailfish-weather/cover/WeatherListCover.qml @@ -0,0 +1,84 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Sailfish.Weather 1.0 + +Item { + id: root + + property int visibleItemCount: 4 + property int maximumHeight: parent.height - Theme.itemSizeSmall/2 + property int itemHeight: Math.round(maximumHeight / visibleItemCount) + + PathView { + id: view + + property int rollIndex + property real rollOffset + + x: Theme.paddingLarge + model: savedWeathersModel + width: parent.width - 2*x + pathItemCount: count > 4 ? 5 : Math.min(visibleItemCount, count) + height: Math.min(visibleItemCount, count)/visibleItemCount*maximumHeight + offset: rollIndex + rollOffset + delegate: WeatherCoverItem { + property bool aboutToSlideIn: view.rollOffset === 0 && model.index === (view.count - view.rollIndex) % view.count + + width: view.width + visible: view.count <= 4 || !aboutToSlideIn + topPadding: Theme.paddingLarge + Theme.paddingMedium + text: (model.status === Weather.Error || model.status === Weather.Unauthorized) ? model.city : TemperatureConverter.format(model.temperature) + " " + model.city + description: { + if (model.status === Weather.Error) { + //% "Loading failed" + return qsTrId("weather-la-loading_failed") + } else if (model.status === Weather.Unauthorized) { + //% "Invalid authentication credentials" + return qsTrId("weather-la-unauthorized") + } + + return model.description + } + } + path: Path { + startX: view.width/2; startY: view.count > 4 ? -itemHeight/2 : itemHeight/2 + PathLine { x: view.width/2; y: view.height + (view.count > 4 ? itemHeight/2 : itemHeight/2) } + } + Binding { + when: view.count <= 4 + target: view + property: "offset" + value: 0 + } + SequentialAnimation on rollOffset { + id: rollAnimation + running: cover.status === Cover.Active && view.visible && view.count > 4 + loops: Animation.Infinite + NumberAnimation { + from: 0 + to: 1 + duration: 1000 + easing.type: Easing.InOutQuad + } + ScriptAction { + script: { + view.rollIndex = view.rollIndex + 1 + view.rollOffset = 0 + if (view.rollIndex >= view.count) { + view.rollIndex = 0 + } + } + } + PauseAnimation { duration: 3000 } + } + } + OpacityRampEffect { + enabled: view.count > 3 + sourceItem: root + parent: root.parent + direction: OpacityRamp.TopToBottom + slope: 3 + offset: 1 - 1 / slope + } +} + diff --git a/usr/share/sailfish-weather/model/ApplicationWeatherModel.qml b/usr/share/sailfish-weather/model/ApplicationWeatherModel.qml new file mode 100644 index 00000000..326e7d11 --- /dev/null +++ b/usr/share/sailfish-weather/model/ApplicationWeatherModel.qml @@ -0,0 +1,19 @@ +import QtQuick 2.1 +import Sailfish.Silica 1.0 +import Sailfish.Weather 1.0 + +WeatherModel { + id: model + + active: Qt.application.active + property Connections reloadOnUsersRequest: Connections { + target: weatherApplication + onReload: { + if (locationId === model.locationId) { + model.reload() + } + } + onReloadAll: model.reload(true) + onReloadAllIfAllowed: if (model.updateAllowed()) model.reload() + } +} diff --git a/usr/share/sailfish-weather/pages/LocationSearchPage.qml b/usr/share/sailfish-weather/pages/LocationSearchPage.qml new file mode 100644 index 00000000..9fee2edb --- /dev/null +++ b/usr/share/sailfish-weather/pages/LocationSearchPage.qml @@ -0,0 +1,166 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Sailfish.Weather 1.0 + +Page { + id: page + + property bool error: locationsModel.status === Weather.Error + property bool unauthorized: locationsModel.status === Weather.Unauthorized + property bool loading: locationsModel.status === Weather.Loading || loadingTimer.running + objectName: "LocationSearchPage" + + Timer { id: loadingTimer; interval: 600 } + + LocationsModel { + id: locationsModel + onStatusChanged: if (status === Weather.Loading) loadingTimer.restart() + onFilterChanged: delayedFilter.restart() + } + + SilicaListView { + id: locationListView + currentIndex: -1 + anchors.fill: parent + model: locationsModel + header: Column { + width: parent.width + PageHeader { + //% "New location" + title: qsTrId("weather-la-new_location") + } + SearchField { + id: searchField + + //% "Search locations" + placeholderText: qsTrId("weather-la-search_locations") + onFocusChanged: if (focus) forceActiveFocus() + width: parent.width + EnterKey.iconSource: "image://theme/icon-m-enter-close" + EnterKey.onClicked: focus = false + + Binding { + target: locationsModel + property: "filter" + value: searchField.text.toLowerCase().trim() + } + Binding { + target: searchField + property: "focus" + value: true + when: page.status == PageStatus.Active && locationListView.atYBeginning + } + } + } + BusyIndicator { + running: !error && loading && locationsModel.filter.length > 0 && locationsModel.count === 0 + anchors.horizontalCenter: parent.horizontalCenter + y: placeHolder.y + Math.round(height/2) + parent: placeHolder.parent + size: BusyIndicatorSize.Large + } + ViewPlaceholder { + id: placeHolder + text: { + if (error) { + //% "Loading failed" + return qsTrId("weather-la-loading_failed") + } else if (unauthorized) { + //% "Invalid authentication credentials" + return qsTrId("weather-la-unauthorized") + } else if (locationsModel.filter.length === 0) { + //: Placeholder displayed when user hasn't yet typed a search string + //% "Search and select new location" + return qsTrId("weather-la-search_and_select_location") + } else if (!loading && !delayedFilter.running && locationListView.count == 0) { + if (locationsModel.filter.length < 3) { + //% "Could not find the location. Type at least three characters to perform a partial word search." + return qsTrId("weather-la-search_three_characters_required") + } else { + //% "Sorry, we couldn't find anything" + return qsTrId("weather-la-could_not_find_anything") + } + } + return "" + } + + // Suppress error label flicker when filter has changed but model loading state hasn't yet had time to update + Timer { + id: delayedFilter + interval: 1 + } + + enabled: error || (locationListView.count == 0 && !loading) || locationsModel.filter.length < 1 + + y: locationListView.originY + Math.round(parent.height/14) + + (locationListView.headerItem ? locationListView.headerItem.height : 0) + Button { + //% "Try again" + text: error ? qsTrId("weather-la-try_again") + //% "Save current" + : qsTrId("weather-bt-save_current") + visible: error + onClicked: locationsModel.reload() + anchors { + top: parent.bottom + topMargin: Theme.paddingMedium + horizontalCenter: parent.horizontalCenter + } + } + } + delegate: BackgroundItem { + id: searchResultItem + height: Theme.itemSizeMedium + onClicked: { + var location = { + "locationId": model.id, + "city": model.name, + "state": "", + "country": model.country, + "adminArea": model.adminArea, + "adminArea2": model.adminArea2, + } + if (!savedWeathersModel.currentWeather + || savedWeathersModel.currentWeather.status === Weather.Error) { + savedWeathersModel.setCurrentWeather(location) + } else { + savedWeathersModel.addLocation(location) + } + + pageStack.pop() + } + ListView.onAdd: AddAnimation { target: searchResultItem; from: 0; to: 1 } + ListView.onRemove: FadeAnimation { target: searchResultItem; from: 1; to: 0 } + Column { + anchors { + left: parent.left + right: parent.right + rightMargin: Theme.horizontalPageMargin - Theme.paddingMedium + leftMargin: Theme.itemSizeSmall + Theme.horizontalPageMargin - Theme.paddingMedium + verticalCenter: parent.verticalCenter + } + Label { + width: parent.width + textFormat: Text.StyledText + text: Theme.highlightText(model.name, locationsModel.filter, Theme.highlightColor) + color: highlighted ? Theme.highlightColor : Theme.primaryColor + truncationMode: TruncationMode.Fade + } + Label { + // Order of location country string, "Country", "Admin Area", "Admin Area2" + // e.g. "United States, Nevada, Clark" + readonly property string countryString: model.country + + (model.adminArea ? (", " + model.adminArea) : "") + + (model.adminArea2 ? (", " + model.adminArea2) : "") + width: parent.width + textFormat: Text.StyledText + text: Theme.highlightText(countryString, locationsModel.filter, Theme.highlightColor) + color: highlighted ? Theme.secondaryHighlightColor : Theme.secondaryColor + font.pixelSize: Theme.fontSizeSmall + truncationMode: TruncationMode.Fade + } + } + } + VerticalScrollDecorator {} + } +} diff --git a/usr/share/sailfish-weather/pages/MainPage.qml b/usr/share/sailfish-weather/pages/MainPage.qml new file mode 100644 index 00000000..702a49bd --- /dev/null +++ b/usr/share/sailfish-weather/pages/MainPage.qml @@ -0,0 +1,275 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Sailfish.Weather 1.0 +import Nemo.DBus 2.0 + +Page { + SilicaListView { + id: weatherListView + PullDownMenu { + visible: savedWeathersModel.currentWeather.status !== Weather.Unauthorized + MenuItem { + //% "New location" + text: qsTrId("weather-me-new_location") + onClicked: pageStack.animatorPush(Qt.resolvedUrl("LocationSearchPage.qml")) + } + MenuItem { + //% "Update" + text: qsTrId("weather-me-update") + onClicked: reloadTimer.restart() + enabled: savedWeathersModel.currentWeather || savedWeathersModel.count > 0 + Timer { + id: reloadTimer + interval: 500 + onTriggered: weatherApplication.reloadAll() + } + } + } + anchors.fill: parent + header: Column { + width: parent.width + spacing: Theme.paddingLarge + WeatherHeader { + opacity: currentWeatherAvailable ? 1.0 : 0.0 + weather: savedWeathersModel.currentWeather + onClicked: { + pageStack.animatorPush("WeatherPage.qml", {"weather": weather, "weatherModel": currentWeatherModel, "current": true }) + } + } + + Label { + visible: !placeholder.enabled && currentWeatherAvailable && currentWeatherModel.status === Weather.Unauthorized + x: Theme.horizontalPageMargin + width: parent.width - 2*x + horizontalAlignment: Text.AlignHCenter + wrapMode: Text.Wrap + font { + pixelSize: Theme.fontSizeLarge + family: Theme.fontFamilyHeading + } + + color: palette.highlightColor + opacity: 0.6 + + //% "Invalid authentication credentials" + text: qsTrId("weather-la-unauthorized") + } + + Item { + width: parent.width + height: Theme.paddingLarge + } + } + PlaceholderItem { + id: placeholder + flickable: weatherListView + parent: weatherListView.contentItem + y: weatherListView.originY + (currentWeatherAvailable ? Math.round(parent.height/12) + weatherListView.headerItem.height + : Math.round(Screen.height/4)) + enabled: !currentWeatherAvailable || (savedWeathersModel.count === 0 && counter.active) + error: savedWeathersModel.currentWeather && savedWeathersModel.currentWeather.status === Weather.Error + unauthorized: savedWeathersModel.currentWeather && savedWeathersModel.currentWeather.status === Weather.Unauthorized + empty: !savedWeathersModel.currentWeather || savedWeathersModel.count == 0 + text: { + if (error) { + //% "Loading failed" + return qsTrId("weather-la-loading_failed") + } else if (unauthorized) { + //% "Invalid authentication credentials" + return qsTrId("weather-la-unauthorized") + } else if (empty) { + if (currentWeatherAvailable) { + if (counter.active) { + //% "Pull down to add another weather location" + return qsTrId("weather-la-pull_down_to_add_another_location") + } else { + return "" + } + } else { + //% "Pull down to select your location" + return qsTrId("weather-la-pull_down_to_select_your_location") + } + } else { + //% "Loading" + return qsTrId("weather-la-loading") + } + } + onReload: weatherApplication.reload(savedWeathersModel.currentWeather.locationId) + + // Only show pull down to add another location hint twice on app startup + FirstTimeUseCounter { + id: counter + limit: 2 + key: "/sailfish/weather/pull_down_to_add_another_location_hint_count" + property bool showLocationHint: active && currentWeatherAvailable + onShowLocationHintChanged: if (showLocationHint) counter.increase() + } + } + model: savedWeathersModel + delegate: ListItem { + id: savedWeatherItem + + function remove() { + savedWeathersModel.remove(locationId) + } + ListView.onAdd: AddAnimation { target: savedWeatherItem } + ListView.onRemove: animateRemoval() + menu: contextMenuComponent + contentHeight: Math.max(Theme.itemSizeMedium, labelColumn.implicitHeight + 2 * Theme.paddingMedium) + onClicked: { + pageStack.animatorPush("WeatherPage.qml", {"weather": savedWeathersModel.get(model.locationId), + "weatherModel": weatherModels[model.locationId] }) + } + + Image { + id: icon + x: Theme.horizontalPageMargin + anchors.verticalCenter: labelColumn.verticalCenter + visible: model.status !== Weather.Loading + width: Theme.iconSizeMedium + height: Theme.iconSizeMedium + source: !!model.weatherType + && model.weatherType.length > 0 ? "image://theme/icon-m-weather-" + model.weatherType + + (highlighted ? "?" + Theme.highlightColor : "") + : "" + } + BusyIndicator { + running: model.status === Weather.Loading + anchors.centerIn: icon + } + Column { + id: labelColumn + + y: Theme.paddingMedium + height: cityLabel.height + descriptionLabel.lineHeight + anchors { + left: icon.right + right: temperatureLabel.left + leftMargin: Theme.paddingMedium + rightMargin: Theme.paddingSmall + } + Label { + id: cityLabel + width: parent.width + color: highlighted ? Theme.highlightColor : Theme.primaryColor + text: model.city + ", " + model.country + (model.adminArea ? (", " + model.adminArea) : "") + truncationMode: TruncationMode.Fade + } + Label { + id: descriptionLabel + + property real lineHeight: height/lineCount + width: parent.width + color: highlighted ? Theme.secondaryHighlightColor : Theme.secondaryColor + text: !model.populated && model.status === Weather.Error ? + //% "Loading current conditions failed" + qsTrId("weather-la-loading_current_conditions_failed") + : + model.description + font.pixelSize: Theme.fontSizeSmall + elide: Text.ElideRight + wrapMode: Text.Wrap + } + } + Label { + id: temperatureLabel + text: TemperatureConverter.format(model.temperature) + color: highlighted ? Theme.secondaryHighlightColor : Theme.secondaryColor + font.pixelSize: Theme.fontSizeHuge + anchors { + verticalCenter: labelColumn.verticalCenter + right: parent.right + rightMargin: Theme.horizontalPageMargin + } + width: visible ? implicitWidth : 0 + visible: model.populated + } + Component { + id: contextMenuComponent + ContextMenu { + property bool moveItemsWhenClosed + property bool setCurrentWhenClosed + property bool menuOpen: height > 0 + + onMenuOpenChanged: { + if (!menuOpen) { + if (moveItemsWhenClosed) { + savedWeathersModel.moveToTop(model.index) + moveItemsWhenClosed = false + } + if (setCurrentWhenClosed) { + var current = savedWeathersModel.currentWeather + if (!current || current.locationId !== model.locationId) { + var weather = { + "locationId": model.locationId, + "city": model.city, + "state": model.state, + "adminArea": model.adminArea, + "adminArea2": model.adminArea2, + "station": model.station, + "country": model.country, + "temperature": model.temperature, + "feelsLikeTemperature": model.feelsLikeTemperature, + "weatherType": model.weatherType, + "description": model.description, + "timestamp": model.timestamp, + "populated": model.populated + } + savedWeathersModel.setCurrentWeather(weather) + + } + setCurrentWhenClosed = false + } + } + } + + MenuItem { + //% "Remove" + text: qsTrId("weather-me-remove") + onClicked: remove() + } + MenuItem { + //% "Set as current" + text: qsTrId("weather-me-set_as_current") + visible: model.populated + onClicked: setCurrentWhenClosed = true + } + MenuItem { + //% "Move to top" + text: qsTrId("weather-me-move_to_top") + visible: model.index !== 0 + onClicked: moveItemsWhenClosed = true + } + } + } + } + footer: Item { + width: parent.width + height: provider.height + } + ProviderDisclaimer { + id: provider + y: weatherListView.originY - weatherListView.contentY - height + Math.max(Screen.height, weatherListView.contentHeight) + weather: savedWeathersModel.currentWeather + } + VerticalScrollDecorator {} + } + + DBusAdaptor { + service: "org.sailfishos.weather" + path: "/org/sailfishos/weather" + iface: "org.sailfishos.weather" + xml: " \n" + + " \n" + + " \n" + + signal newLocation + + onNewLocation: { + var alreadyOpen = pageStack.currentPage && pageStack.currentPage.objectName === "LocationSearchPage" + if (!alreadyOpen) + pageStack.push(Qt.resolvedUrl("LocationSearchPage.qml"), undefined, PageStackAction.Immediate) + weatherApplication.activate() + } + } +} diff --git a/usr/share/sailfish-weather/pages/WeatherPage.qml b/usr/share/sailfish-weather/pages/WeatherPage.qml new file mode 100644 index 00000000..1488b159 --- /dev/null +++ b/usr/share/sailfish-weather/pages/WeatherPage.qml @@ -0,0 +1,5 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Sailfish.Weather 1.0 as Weather + +Weather.WeatherPage {} \ No newline at end of file diff --git a/usr/share/sailfish-weather/weather.qml b/usr/share/sailfish-weather/weather.qml new file mode 100644 index 00000000..25b64a58 --- /dev/null +++ b/usr/share/sailfish-weather/weather.qml @@ -0,0 +1,54 @@ +import QtQuick 2.1 +import Sailfish.Silica 1.0 +import Sailfish.Weather 1.0 +import "cover" +import "model" +import "pages" + +ApplicationWindow { + id: weatherApplication + + property var weatherModels + property bool currentWeatherAvailable: savedWeathersModel.currentWeather + && savedWeathersModel.currentWeather.populated + + initialPage: Component { MainPage {} } + cover: Component { WeatherCover {} } + allowedOrientations: Screen.sizeCategory > Screen.Medium + ? defaultAllowedOrientations + : defaultAllowedOrientations & Orientation.PortraitMask + _defaultPageOrientations: Orientation.All + + signal reload(int locationId) + signal reloadAll() + signal reloadAllIfAllowed() + + Connections { + target: Qt.application + onActiveChanged: { + if (!Qt.application.active) { + savedWeathersModel.save() + } + } + } + ApplicationWeatherModel { + id: currentWeatherModel + + savedWeathers: savedWeathersModel + weather: savedWeathersModel.currentWeather + } + Instantiator { + asynchronous: true + onObjectAdded: { + var models = weatherModels ? weatherModels : {} + models[object.locationId] = object + weatherModels = models + } + + model: SavedWeathersModel { id: savedWeathersModel } + ApplicationWeatherModel { + savedWeathers: savedWeathersModel + weather: model + } + } +} diff --git a/usr/share/sp-smaps-visualize/expander.js b/usr/share/sp-smaps-visualize/expander.js new file mode 100644 index 00000000..55a0b4de --- /dev/null +++ b/usr/share/sp-smaps-visualize/expander.js @@ -0,0 +1,116 @@ + +// --------------------- Variables -------------------------- +// Default values for image names of the images/text to expand and collapse the hidden text. +// ---------------------------------------------------------- +var expandImage = "expand.gif"; +var collapseImage = "collapse.gif"; +var defaultExpandText = "Show More..."; +var defaultCollapseText = "Show Less...."; + +// ---------------- setDefaultExpanderImages(...) ------------------ +// Call this method to change the names of the images used for the "expand" and "collapse" buttons +// The parameters should be the actual image URLS for the images, which may be relative or absolute +// ---------------------------------------------------------- +function setDefaultExpanderImages(expandImgName, collapseImgName) { + expandImage = expandImgName; + collapseImage = collapseImgName; +} + +// ---------------- setDefaultExpanderText(...) ------------------ +// Call this method to change the strings used for the "expand" and "collapse" links +// expandText = "Expand" or "Show More" or "Reveal Answer" etc. +// collapseText = "Hide" or "Show Less" or "Hide Answer" etc. +// ---------------------------------------------------------- +function setDefaultExpanderText(expandText, collapseText) { + defaultExpandText = expandText; + defaultCollapseText = collapseText; +} + +// ------------------- toggleBlock(...) --------------------- +// Method to Collapse/Expand a block of text, using default image names +// hiddenDivId - the ID of div or span to show/hide +// expander - pass in a reference to the image tag for the expender +// usually this will just be "this" (without any quotes) +// ---------------------------------------------------------- +//function toggleBlockImage (hiddenDivId, expander) { +// alert(collapseImage); +// toggleBlockImage(hiddenDivId, expander, expandImage, collapseImage); +//} + +// ------------------- toggleBlockImage(...) --------------------- +// Method to Collapse/Expand a block of text, using custom image names +// hiddenDivId - the ID of div or span to show/hide +// expander - pass in a reference to the image tag for the expender +// usually this will just be "this" (without any quotes) +// expandImageName & collapseImageName are optional parameters +// ---------------------------------------------------------- +function toggleBlockImage (hiddenDivId, expander, expandImageName, collapseImageName) { + if (document.getElementById) { + if (document.getElementById(hiddenDivId).style.display == "none") { + document.getElementById(hiddenDivId).style.display = ""; + expander.src = (collapseImageName)?collapseImageName:collapseImage; + } else { + document.getElementById(hiddenDivId).style.display = "none"; + expander.src = (expandImageName)?expandImageName:expandImage; + } + } +} + + +// ----------------- toggleBlockText(...) ------------------- +// Method to show or hide a paragraph or other block or span of text. +// Use this version for text links to show/hide, and custom link text +// Parameters: +// hiddenDivId - the ID attribute of div to show or hide +// expander - pass in a reference to the image tag for the expender +// usually this will just be "this" (without any quotes) +// expandText - the text to show when the block of text is hidden (to show the text) OPTIONAL +// collapseText - the text to show when the block of text is showing (to hide the text) OPTIONAL +// ---------------------------------------------------------- +function toggleBlockText (hiddenDivId, expander, expandText, collapseText) { + if (document.getElementById) { + if (document.getElementById(hiddenDivId).style.display == "none") { + document.getElementById(hiddenDivId).style.display = ""; + expander.innerHTML = collapseText?collapseText:defaultCollapseText; + } else { + document.getElementById(hiddenDivId).style.display = "none"; + expander.innerHTML = expandText?expandText:defaultExpandText; + } + } +} + +// ---------------------------------------------------------- +// another different way to toggle how much is shown...scrollbars +// collapseHeight - eg: "200px" +// ---------------------------------------------------------- +function toggleOverflowImage (hiddenDivId, expander, expandImageName, collapseImageName, collapseHeight) { + if (document.getElementById) { + if (document.getElementById(hiddenDivId).style.overflow == "scroll") { + document.getElementById(hiddenDivId).style.overflow = "auto"; + document.getElementById(hiddenDivId).style.height = ""; + expander.src = (collapseImageName)?collapseImageName:collapseImage; + } else { + document.getElementById(hiddenDivId).style.overflow = "scroll"; + document.getElementById(hiddenDivId).style.height = collapseHeight; + expander.src = (expandImageName)?expandImageName:expandImage; + } + } +} + +// ---------------------------------------------------------- +// another different way to toggle how much is shown...scrollbars +// collapseHeight - eg: "200px" +// ---------------------------------------------------------- +function toggleOverflowText (hiddenDivId, expander, expandText, collapseText, collapseHeight) { + if (document.getElementById) { + if (document.getElementById(hiddenDivId).style.overflow == "scroll") { + document.getElementById(hiddenDivId).style.overflow = "auto"; + document.getElementById(hiddenDivId).style.height = ""; + expander.innerHTML = collapseText?collapseText:defaultHideText; + } else { + document.getElementById(hiddenDivId).style.overflow = "scroll"; + document.getElementById(hiddenDivId).style.height = collapseHeight; + expander.innerHTML = expandText?expandText:defaultExpandText; + } + } +} \ No newline at end of file diff --git a/usr/share/sp-smaps-visualize/jquery.metadata.js b/usr/share/sp-smaps-visualize/jquery.metadata.js new file mode 100644 index 00000000..9da403fd --- /dev/null +++ b/usr/share/sp-smaps-visualize/jquery.metadata.js @@ -0,0 +1,148 @@ +/* + * Metadata - jQuery plugin for parsing metadata from elements + * + * Copyright (c) 2006 John Resig, Yehuda Katz, J�örn Zaefferer, Paul McLanahan + * + * Dual licensed under the MIT and GPL licenses: + * http://www.opensource.org/licenses/mit-license.php + * http://www.gnu.org/licenses/gpl.html + * + * Revision: $Id: jquery.metadata.js 3640 2007-10-11 18:34:38Z pmclanahan $ + * + */ + +/** + * Sets the type of metadata to use. Metadata is encoded in JSON, and each property + * in the JSON will become a property of the element itself. + * + * There are four supported types of metadata storage: + * + * attr: Inside an attribute. The name parameter indicates *which* attribute. + * + * class: Inside the class attribute, wrapped in curly braces: { } + * + * elem: Inside a child element (e.g. a script tag). The + * name parameter indicates *which* element. + * html5: Values are stored in data-* attributes. + * + * The metadata for an element is loaded the first time the element is accessed via jQuery. + * + * As a result, you can define the metadata type, use $(expr) to load the metadata into the elements + * matched by expr, then redefine the metadata type and run another $(expr) for other elements. + * + * @name $.metadata.setType + * + * @example

This is a p

+ * @before $.metadata.setType("class") + * @after $("#one").metadata().item_id == 1; $("#one").metadata().item_label == "Label" + * @desc Reads metadata from the class attribute + * + * @example

This is a p

+ * @before $.metadata.setType("attr", "data") + * @after $("#one").metadata().item_id == 1; $("#one").metadata().item_label == "Label" + * @desc Reads metadata from a "data" attribute + * + * @example

This is a p

+ * @before $.metadata.setType("elem", "script") + * @after $("#one").metadata().item_id == 1; $("#one").metadata().item_label == "Label" + * @desc Reads metadata from a nested script element + * + * @example

This is a p

+ * @before $.metadata.setType("html5") + * @after $("#one").metadata().item_id == 1; $("#one").metadata().item_label == "Label" + * @desc Reads metadata from a series of data-* attributes + * + * @param String type The encoding type + * @param String name The name of the attribute to be used to get metadata (optional) + * @cat Plugins/Metadata + * @descr Sets the type of encoding to be used when loading metadata for the first time + * @type undefined + * @see metadata() + */ + +(function($) { + +$.extend({ + metadata : { + defaults : { + type: 'class', + name: 'metadata', + cre: /({.*})/, + single: 'metadata' + }, + setType: function( type, name ){ + this.defaults.type = type; + this.defaults.name = name; + }, + get: function( elem, opts ){ + var settings = $.extend({},this.defaults,opts); + // check for empty string in single property + if ( !settings.single.length ) settings.single = 'metadata'; + + var data = $.data(elem, settings.single); + // returned cached data if it already exists + if ( data ) return data; + + data = "{}"; + + var getData = function(data) { + if(typeof data != "string") return data; + + if( data.indexOf('{') < 0 ) { + data = eval("(" + data + ")"); + } + } + + var getObject = function(data) { + if(typeof data != "string") return data; + + data = eval("(" + data + ")"); + return data; + } + + if ( settings.type == "html5" ) { + var object = {}; + $( elem.attributes ).each(function() { + var name = this.nodeName; + if(name.match(/^data-/)) name = name.replace(/^data-/, ''); + else return true; + object[name] = getObject(this.nodeValue); + }); + } else { + if ( settings.type == "class" ) { + var m = settings.cre.exec( elem.className ); + if ( m ) + data = m[1]; + } else if ( settings.type == "elem" ) { + if( !elem.getElementsByTagName ) return; + var e = elem.getElementsByTagName(settings.name); + if ( e.length ) + data = $.trim(e[0].innerHTML); + } else if ( elem.getAttribute != undefined ) { + var attr = elem.getAttribute( settings.name ); + if ( attr ) + data = attr; + } + object = getObject(data.indexOf("{") < 0 ? "{" + data + "}" : data); + } + + $.data( elem, settings.single, object ); + return object; + } + } +}); + +/** + * Returns the metadata object for the first member of the jQuery object. + * + * @name metadata + * @descr Returns element's metadata object + * @param Object opts An object contianing settings to override the defaults + * @type jQuery + * @cat Plugins/Metadata + */ +$.fn.metadata = function( opts ){ + return $.metadata.get( this[0], opts ); +}; + +})(jQuery); \ No newline at end of file diff --git a/usr/share/sp-smaps-visualize/jquery.min.js b/usr/share/sp-smaps-visualize/jquery.min.js new file mode 100644 index 00000000..b2ac1747 --- /dev/null +++ b/usr/share/sp-smaps-visualize/jquery.min.js @@ -0,0 +1,18 @@ +/*! + * jQuery JavaScript Library v1.6.1 + * http://jquery.com/ + * + * Copyright 2011, John Resig + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * Includes Sizzle.js + * http://sizzlejs.com/ + * Copyright 2011, The Dojo Foundation + * Released under the MIT, BSD, and GPL Licenses. + * + * Date: Thu May 12 15:04:36 2011 -0400 + */ +(function(a,b){function cy(a){return f.isWindow(a)?a:a.nodeType===9?a.defaultView||a.parentWindow:!1}function cv(a){if(!cj[a]){var b=f("<"+a+">").appendTo("body"),d=b.css("display");b.remove();if(d==="none"||d===""){ck||(ck=c.createElement("iframe"),ck.frameBorder=ck.width=ck.height=0),c.body.appendChild(ck);if(!cl||!ck.createElement)cl=(ck.contentWindow||ck.contentDocument).document,cl.write("");b=cl.createElement(a),cl.body.appendChild(b),d=f.css(b,"display"),c.body.removeChild(ck)}cj[a]=d}return cj[a]}function cu(a,b){var c={};f.each(cp.concat.apply([],cp.slice(0,b)),function(){c[this]=a});return c}function ct(){cq=b}function cs(){setTimeout(ct,0);return cq=f.now()}function ci(){try{return new a.ActiveXObject("Microsoft.XMLHTTP")}catch(b){}}function ch(){try{return new a.XMLHttpRequest}catch(b){}}function cb(a,c){a.dataFilter&&(c=a.dataFilter(c,a.dataType));var d=a.dataTypes,e={},g,h,i=d.length,j,k=d[0],l,m,n,o,p;for(g=1;g=0===c})}function W(a){return!a||!a.parentNode||a.parentNode.nodeType===11}function O(a,b){return(a&&a!=="*"?a+".":"")+b.replace(A,"`").replace(B,"&")}function N(a){var b,c,d,e,g,h,i,j,k,l,m,n,o,p=[],q=[],r=f._data(this,"events");if(!(a.liveFired===this||!r||!r.live||a.target.disabled||a.button&&a.type==="click")){a.namespace&&(n=new RegExp("(^|\\.)"+a.namespace.split(".").join("\\.(?:.*\\.)?")+"(\\.|$)")),a.liveFired=this;var s=r.live.slice(0);for(i=0;ic)break;a.currentTarget=e.elem,a.data=e.handleObj.data,a.handleObj=e.handleObj,o=e.handleObj.origHandler.apply(e.elem,arguments);if(o===!1||a.isPropagationStopped()){c=e.level,o===!1&&(b=!1);if(a.isImmediatePropagationStopped())break}}return b}}function L(a,c,d){var e=f.extend({},d[0]);e.type=a,e.originalEvent={},e.liveFired=b,f.event.handle.call(c,e),e.isDefaultPrevented()&&d[0].preventDefault()}function F(){return!0}function E(){return!1}function m(a,c,d){var e=c+"defer",g=c+"queue",h=c+"mark",i=f.data(a,e,b,!0);i&&(d==="queue"||!f.data(a,g,b,!0))&&(d==="mark"||!f.data(a,h,b,!0))&&setTimeout(function(){!f.data(a,g,b,!0)&&!f.data(a,h,b,!0)&&(f.removeData(a,e,!0),i.resolve())},0)}function l(a){for(var b in a)if(b!=="toJSON")return!1;return!0}function k(a,c,d){if(d===b&&a.nodeType===1){var e="data-"+c.replace(j,"$1-$2").toLowerCase();d=a.getAttribute(e);if(typeof d=="string"){try{d=d==="true"?!0:d==="false"?!1:d==="null"?null:f.isNaN(d)?i.test(d)?f.parseJSON(d):d:parseFloat(d)}catch(g){}f.data(a,c,d)}else d=b}return d}var c=a.document,d=a.navigator,e=a.location,f=function(){function H(){if(!e.isReady){try{c.documentElement.doScroll("left")}catch(a){setTimeout(H,1);return}e.ready()}}var e=function(a,b){return new e.fn.init(a,b,h)},f=a.jQuery,g=a.$,h,i=/^(?:[^<]*(<[\w\W]+>)[^>]*$|#([\w\-]*)$)/,j=/\S/,k=/^\s+/,l=/\s+$/,m=/\d/,n=/^<(\w+)\s*\/?>(?:<\/\1>)?$/,o=/^[\],:{}\s]*$/,p=/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g,q=/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,r=/(?:^|:|,)(?:\s*\[)+/g,s=/(webkit)[ \/]([\w.]+)/,t=/(opera)(?:.*version)?[ \/]([\w.]+)/,u=/(msie) ([\w.]+)/,v=/(mozilla)(?:.*? rv:([\w.]+))?/,w=d.userAgent,x,y,z,A=Object.prototype.toString,B=Object.prototype.hasOwnProperty,C=Array.prototype.push,D=Array.prototype.slice,E=String.prototype.trim,F=Array.prototype.indexOf,G={};e.fn=e.prototype={constructor:e,init:function(a,d,f){var g,h,j,k;if(!a)return this;if(a.nodeType){this.context=this[0]=a,this.length=1;return this}if(a==="body"&&!d&&c.body){this.context=c,this[0]=c.body,this.selector=a,this.length=1;return this}if(typeof a=="string"){a.charAt(0)!=="<"||a.charAt(a.length-1)!==">"||a.length<3?g=i.exec(a):g=[null,a,null];if(g&&(g[1]||!d)){if(g[1]){d=d instanceof e?d[0]:d,k=d?d.ownerDocument||d:c,j=n.exec(a),j?e.isPlainObject(d)?(a=[c.createElement(j[1])],e.fn.attr.call(a,d,!0)):a=[k.createElement(j[1])]:(j=e.buildFragment([g[1]],[k]),a=(j.cacheable?e.clone(j.fragment):j.fragment).childNodes);return e.merge(this,a)}h=c.getElementById(g[2]);if(h&&h.parentNode){if(h.id!==g[2])return f.find(a);this.length=1,this[0]=h}this.context=c,this.selector=a;return this}return!d||d.jquery?(d||f).find(a):this.constructor(d).find(a)}if(e.isFunction(a))return f.ready(a);a.selector!==b&&(this.selector=a.selector,this.context=a.context);return e.makeArray(a,this)},selector:"",jquery:"1.6.1",length:0,size:function(){return this.length},toArray:function(){return D.call(this,0)},get:function(a){return a==null?this.toArray():a<0?this[this.length+a]:this[a]},pushStack:function(a,b,c){var d=this.constructor();e.isArray(a)?C.apply(d,a):e.merge(d,a),d.prevObject=this,d.context=this.context,b==="find"?d.selector=this.selector+(this.selector?" ":"")+c:b&&(d.selector=this.selector+"."+b+"("+c+")");return d},each:function(a,b){return e.each(this,a,b)},ready:function(a){e.bindReady(),y.done(a);return this},eq:function(a){return a===-1?this.slice(a):this.slice(a,+a+1)},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},slice:function(){return this.pushStack(D.apply(this,arguments),"slice",D.call(arguments).join(","))},map:function(a){return this.pushStack(e.map(this,function(b,c){return a.call(b,c,b)}))},end:function(){return this.prevObject||this.constructor(null)},push:C,sort:[].sort,splice:[].splice},e.fn.init.prototype=e.fn,e.extend=e.fn.extend=function(){var a,c,d,f,g,h,i=arguments[0]||{},j=1,k=arguments.length,l=!1;typeof i=="boolean"&&(l=i,i=arguments[1]||{},j=2),typeof i!="object"&&!e.isFunction(i)&&(i={}),k===j&&(i=this,--j);for(;j0)return;y.resolveWith(c,[e]),e.fn.trigger&&e(c).trigger("ready").unbind("ready")}},bindReady:function(){if(!y){y=e._Deferred();if(c.readyState==="complete")return setTimeout(e.ready,1);if(c.addEventListener)c.addEventListener("DOMContentLoaded",z,!1),a.addEventListener("load",e.ready,!1);else if(c.attachEvent){c.attachEvent("onreadystatechange",z),a.attachEvent("onload",e.ready);var b=!1;try{b=a.frameElement==null}catch(d){}c.documentElement.doScroll&&b&&H()}}},isFunction:function(a){return e.type(a)==="function"},isArray:Array.isArray||function(a){return e.type(a)==="array"},isWindow:function(a){return a&&typeof a=="object"&&"setInterval"in a},isNaN:function(a){return a==null||!m.test(a)||isNaN(a)},type:function(a){return a==null?String(a):G[A.call(a)]||"object"},isPlainObject:function(a){if(!a||e.type(a)!=="object"||a.nodeType||e.isWindow(a))return!1;if(a.constructor&&!B.call(a,"constructor")&&!B.call(a.constructor.prototype,"isPrototypeOf"))return!1;var c;for(c in a);return c===b||B.call(a,c)},isEmptyObject:function(a){for(var b in a)return!1;return!0},error:function(a){throw a},parseJSON:function(b){if(typeof b!="string"||!b)return null;b=e.trim(b);if(a.JSON&&a.JSON.parse)return a.JSON.parse(b);if(o.test(b.replace(p,"@").replace(q,"]").replace(r,"")))return(new Function("return "+b))();e.error("Invalid JSON: "+b)},parseXML:function(b,c,d){a.DOMParser?(d=new DOMParser,c=d.parseFromString(b,"text/xml")):(c=new ActiveXObject("Microsoft.XMLDOM"),c.async="false",c.loadXML(b)),d=c.documentElement,(!d||!d.nodeName||d.nodeName==="parsererror")&&e.error("Invalid XML: "+b);return c},noop:function(){},globalEval:function(b){b&&j.test(b)&&(a.execScript||function(b){a.eval.call(a,b)})(b)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toUpperCase()===b.toUpperCase()},each:function(a,c,d){var f,g=0,h=a.length,i=h===b||e.isFunction(a);if(d){if(i){for(f in a)if(c.apply(a[f],d)===!1)break}else for(;g0&&a[0]&&a[j-1]||j===0||e.isArray(a));if(k)for(;i1?h.call(arguments,0):c,--e||g.resolveWith(g,h.call(b,0))}}var b=arguments,c=0,d=b.length,e=d,g=d<=1&&a&&f.isFunction(a.promise)?a:f.Deferred();if(d>1){for(;c
a",d=a.getElementsByTagName("*"),e=a.getElementsByTagName("a")[0];if(!d||!d.length||!e)return{};f=c.createElement("select"),g=f.appendChild(c.createElement("option")),h=a.getElementsByTagName("input")[0],j={leadingWhitespace:a.firstChild.nodeType===3,tbody:!a.getElementsByTagName("tbody").length,htmlSerialize:!!a.getElementsByTagName("link").length,style:/top/.test(e.getAttribute("style")),hrefNormalized:e.getAttribute("href")==="/a",opacity:/^0.55$/.test(e.style.opacity),cssFloat:!!e.style.cssFloat,checkOn:h.value==="on",optSelected:g.selected,getSetAttribute:a.className!=="t",submitBubbles:!0,changeBubbles:!0,focusinBubbles:!1,deleteExpando:!0,noCloneEvent:!0,inlineBlockNeedsLayout:!1,shrinkWrapBlocks:!1,reliableMarginRight:!0},h.checked=!0,j.noCloneChecked=h.cloneNode(!0).checked,f.disabled=!0,j.optDisabled=!g.disabled;try{delete a.test}catch(s){j.deleteExpando=!1}!a.addEventListener&&a.attachEvent&&a.fireEvent&&(a.attachEvent("onclick",function b(){j.noCloneEvent=!1,a.detachEvent("onclick",b)}),a.cloneNode(!0).fireEvent("onclick")),h=c.createElement("input"),h.value="t",h.setAttribute("type","radio"),j.radioValue=h.value==="t",h.setAttribute("checked","checked"),a.appendChild(h),k=c.createDocumentFragment(),k.appendChild(a.firstChild),j.checkClone=k.cloneNode(!0).cloneNode(!0).lastChild.checked,a.innerHTML="",a.style.width=a.style.paddingLeft="1px",l=c.createElement("body"),m={visibility:"hidden",width:0,height:0,border:0,margin:0,background:"none"};for(q in m)l.style[q]=m[q];l.appendChild(a),b.insertBefore(l,b.firstChild),j.appendChecked=h.checked,j.boxModel=a.offsetWidth===2,"zoom"in a.style&&(a.style.display="inline",a.style.zoom=1,j.inlineBlockNeedsLayout=a.offsetWidth===2,a.style.display="",a.innerHTML="
",j.shrinkWrapBlocks=a.offsetWidth!==2),a.innerHTML="
t
",n=a.getElementsByTagName("td"),r=n[0].offsetHeight===0,n[0].style.display="",n[1].style.display="none",j.reliableHiddenOffsets=r&&n[0].offsetHeight===0,a.innerHTML="",c.defaultView&&c.defaultView.getComputedStyle&&(i=c.createElement("div"),i.style.width="0",i.style.marginRight="0",a.appendChild(i),j.reliableMarginRight=(parseInt((c.defaultView.getComputedStyle(i,null)||{marginRight:0}).marginRight,10)||0)===0),l.innerHTML="",b.removeChild(l);if(a.attachEvent)for(q in{submit:1,change:1,focusin:1})p="on"+q,r=p in a,r||(a.setAttribute(p,"return;"),r=typeof a[p]=="function"),j[q+"Bubbles"]=r;return j}(),f.boxModel=f.support.boxModel;var i=/^(?:\{.*\}|\[.*\])$/,j=/([a-z])([A-Z])/g;f.extend({cache:{},uuid:0,expando:"jQuery"+(f.fn.jquery+Math.random()).replace(/\D/g,""),noData:{embed:!0,object:"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000",applet:!0},hasData:function(a){a=a.nodeType?f.cache[a[f.expando]]:a[f.expando];return!!a&&!l(a)},data:function(a,c,d,e){if(!!f.acceptData(a)){var g=f.expando,h=typeof c=="string",i,j=a.nodeType,k=j?f.cache:a,l=j?a[f.expando]:a[f.expando]&&f.expando;if((!l||e&&l&&!k[l][g])&&h&&d===b)return;l||(j?a[f.expando]=l=++f.uuid:l=f.expando),k[l]||(k[l]={},j||(k[l].toJSON=f.noop));if(typeof c=="object"||typeof c=="function")e?k[l][g]=f.extend(k[l][g],c):k[l]=f.extend(k[l],c);i=k[l],e&&(i[g]||(i[g]={}),i=i[g]),d!==b&&(i[f.camelCase(c)]=d);if(c==="events"&&!i[c])return i[g]&&i[g].events;return h?i[f.camelCase(c)]:i}},removeData:function(b,c,d){if(!!f.acceptData(b)){var e=f.expando,g=b.nodeType,h=g?f.cache:b,i=g?b[f.expando]:f.expando;if(!h[i])return;if(c){var j=d?h[i][e]:h[i];if(j){delete j[c];if(!l(j))return}}if(d){delete h[i][e];if(!l(h[i]))return}var k=h[i][e];f.support.deleteExpando||h!=a?delete h[i]:h[i]=null,k?(h[i]={},g||(h[i].toJSON=f.noop),h[i][e]=k):g&&(f.support.deleteExpando?delete b[f.expando]:b.removeAttribute?b.removeAttribute(f.expando):b[f.expando]=null)}},_data:function(a,b,c){return f.data(a,b,c,!0)},acceptData:function(a){if(a.nodeName){var b=f.noData[a.nodeName.toLowerCase()];if(b)return b!==!0&&a.getAttribute("classid")===b}return!0}}),f.fn.extend({data:function(a,c){var d=null;if(typeof a=="undefined"){if(this.length){d=f.data(this[0]);if(this[0].nodeType===1){var e=this[0].attributes,g;for(var h=0,i=e.length;h-1)return!0;return!1},val:function(a){var c,d,e=this[0];if(!arguments.length){if(e){c=f.valHooks[e.nodeName.toLowerCase()]||f.valHooks[e.type];if(c&&"get"in c&&(d=c.get(e,"value"))!==b)return d;return(e.value||"").replace(p,"")}return b}var g=f.isFunction(a);return this.each(function(d){var e=f(this),h;if(this.nodeType===1){g?h=a.call(this,d,e.val()):h=a,h==null?h="":typeof h=="number"?h+="":f.isArray(h)&&(h=f.map(h,function(a){return a==null?"":a+""})),c=f.valHooks[this.nodeName.toLowerCase()]||f.valHooks[this.type];if(!c||!("set"in c)||c.set(this,h,"value")===b)this.value=h}})}}),f.extend({valHooks:{option:{get:function(a){var b=a.attributes.value;return!b||b.specified?a.value:a.text}},select:{get:function(a){var b,c=a.selectedIndex,d=[],e=a.options,g=a.type==="select-one";if(c<0)return null;for(var h=g?c:0,i=g?c+1:e.length;h=0}),c.length||(a.selectedIndex=-1);return c}}},attrFn:{val:!0,css:!0,html:!0,text:!0,data:!0,width:!0,height:!0,offset:!0},attrFix:{tabindex:"tabIndex"},attr:function(a,c,d,e){var g=a.nodeType;if(!a||g===3||g===8||g===2)return b;if(e&&c in f.attrFn)return f(a)[c](d);if(!("getAttribute"in a))return f.prop(a,c,d);var h,i,j=g!==1||!f.isXMLDoc(a);c=j&&f.attrFix[c]||c,i=f.attrHooks[c],i||(!t.test(c)||typeof d!="boolean"&&d!==b&&d.toLowerCase()!==c.toLowerCase()?v&&(f.nodeName(a,"form")||u.test(c))&&(i=v):i=w);if(d!==b){if(d===null){f.removeAttr(a,c);return b}if(i&&"set"in i&&j&&(h=i.set(a,d,c))!==b)return h;a.setAttribute(c,""+d);return d}if(i&&"get"in i&&j)return i.get(a,c);h=a.getAttribute(c);return h===null?b:h},removeAttr:function(a,b){var c;a.nodeType===1&&(b=f.attrFix[b]||b,f.support.getSetAttribute?a.removeAttribute(b):(f.attr(a,b,""),a.removeAttributeNode(a.getAttributeNode(b))),t.test(b)&&(c=f.propFix[b]||b)in a&&(a[c]=!1))},attrHooks:{type:{set:function(a,b){if(q.test(a.nodeName)&&a.parentNode)f.error("type property can't be changed");else if(!f.support.radioValue&&b==="radio"&&f.nodeName(a,"input")){var c=a.value;a.setAttribute("type",b),c&&(a.value=c);return b}}},tabIndex:{get:function(a){var c=a.getAttributeNode("tabIndex");return c&&c.specified?parseInt(c.value,10):r.test(a.nodeName)||s.test(a.nodeName)&&a.href?0:b}}},propFix:{tabindex:"tabIndex",readonly:"readOnly","for":"htmlFor","class":"className",maxlength:"maxLength",cellspacing:"cellSpacing",cellpadding:"cellPadding",rowspan:"rowSpan",colspan:"colSpan",usemap:"useMap",frameborder:"frameBorder",contenteditable:"contentEditable"},prop:function(a,c,d){var e=a.nodeType;if(!a||e===3||e===8||e===2)return b;var g,h,i=e!==1||!f.isXMLDoc(a);c=i&&f.propFix[c]||c,h=f.propHooks[c];return d!==b?h&&"set"in h&&(g=h.set(a,d,c))!==b?g:a[c]=d:h&&"get"in h&&(g=h.get(a,c))!==b?g:a[c]},propHooks:{}}),w={get:function(a,c){return a[f.propFix[c]||c]?c.toLowerCase():b},set:function(a,b,c){var d;b===!1?f.removeAttr(a,c):(d=f.propFix[c]||c,d in a&&(a[d]=b),a.setAttribute(c,c.toLowerCase()));return c}},f.attrHooks.value={get:function(a,b){if(v&&f.nodeName(a,"button"))return v.get(a,b);return a.value},set:function(a,b,c){if(v&&f.nodeName(a,"button"))return v.set(a,b,c);a.value=b}},f.support.getSetAttribute||(f.attrFix=f.propFix,v=f.attrHooks.name=f.valHooks.button={get:function(a,c){var d;d=a.getAttributeNode(c);return d&&d.nodeValue!==""?d.nodeValue:b},set:function(a,b,c){var d=a.getAttributeNode(c);if(d){d.nodeValue=b;return b}}},f.each(["width","height"],function(a,b){f.attrHooks[b]=f.extend(f.attrHooks[b],{set:function(a,c){if(c===""){a.setAttribute(b,"auto");return c}}})})),f.support.hrefNormalized||f.each(["href","src","width","height"],function(a,c){f.attrHooks[c]=f.extend(f.attrHooks[c],{get:function(a){var d=a.getAttribute(c,2);return d===null?b:d}})}),f.support.style||(f.attrHooks.style={get:function(a){return a.style.cssText.toLowerCase()||b},set:function(a,b){return a.style.cssText=""+b}}),f.support.optSelected||(f.propHooks.selected=f.extend(f.propHooks.selected,{get:function(a){var b=a.parentNode;b&&(b.selectedIndex,b.parentNode&&b.parentNode.selectedIndex)}})),f.support.checkOn||f.each(["radio","checkbox"],function(){f.valHooks[this]={get:function(a){return a.getAttribute("value")===null?"on":a.value}}}),f.each(["radio","checkbox"],function(){f.valHooks[this]=f.extend(f.valHooks[this],{set:function(a,b){if(f.isArray(b))return a.checked=f.inArray(f(a).val(),b)>=0}})});var x=Object.prototype.hasOwnProperty,y=/\.(.*)$/,z=/^(?:textarea|input|select)$/i,A=/\./g,B=/ /g,C=/[^\w\s.|`]/g,D=function(a){return a.replace(C,"\\$&")};f.event={add:function(a,c,d,e){if(a.nodeType!==3&&a.nodeType!==8){if(d===!1)d=E;else if(!d)return;var g,h;d.handler&&(g=d,d=g.handler),d.guid||(d.guid=f.guid++);var i=f._data(a);if(!i)return;var j=i.events,k=i.handle;j||(i.events=j={}),k||(i.handle=k=function(a){return typeof f!="undefined"&&(!a||f.event.triggered!==a.type)?f.event.handle.apply(k.elem,arguments):b}),k.elem=a,c=c.split(" ");var l,m=0,n;while(l=c[m++]){h=g?f.extend({},g):{handler:d,data:e},l.indexOf(".")>-1?(n=l.split("."),l=n.shift(),h.namespace=n.slice(0).sort().join(".")):(n=[],h.namespace=""),h.type=l,h.guid||(h.guid=d.guid);var o=j[l],p=f.event.special[l]||{};if(!o){o=j[l]=[];if(!p.setup||p.setup.call(a,e,n,k)===!1)a.addEventListener?a.addEventListener(l,k,!1):a.attachEvent&&a.attachEvent("on"+l,k)}p.add&&(p.add.call(a,h),h.handler.guid||(h.handler.guid=d.guid)),o.push(h),f.event.global[l]=!0}a=null}},global:{},remove:function(a,c,d,e){if(a.nodeType!==3&&a.nodeType!==8){d===!1&&(d=E);var g,h,i,j,k=0,l,m,n,o,p,q,r,s=f.hasData(a)&&f._data(a),t=s&&s.events;if(!s||!t)return;c&&c.type&&(d=c.handler,c=c.type);if(!c||typeof c=="string"&&c.charAt(0)==="."){c=c||"";for(h in t)f.event.remove(a,h+c);return}c=c.split(" ");while(h=c[k++]){r=h,q=null,l=h.indexOf(".")<0,m=[],l||(m=h.split("."),h=m.shift(),n=new RegExp("(^|\\.)"+f.map(m.slice(0).sort(),D).join("\\.(?:.*\\.)?")+"(\\.|$)")),p=t[h];if(!p)continue;if(!d){for(j=0;j=0&&(h=h.slice(0,-1),j=!0),h.indexOf(".")>=0&&(i=h.split("."),h=i.shift(),i.sort());if(!!e&&!f.event.customEvent[h]||!!f.event.global[h]){c=typeof c=="object"?c[f.expando]?c:new f.Event(h,c):new f.Event(h),c.type=h,c.exclusive=j,c.namespace=i.join("."),c.namespace_re=new RegExp("(^|\\.)"+i.join("\\.(?:.*\\.)?")+"(\\.|$)");if(g||!e)c.preventDefault(),c.stopPropagation();if(!e){f.each(f.cache,function(){var a=f.expando,b=this[a];b&&b.events&&b.events[h]&&f.event.trigger(c,d,b.handle.elem +)});return}if(e.nodeType===3||e.nodeType===8)return;c.result=b,c.target=e,d=d?f.makeArray(d):[],d.unshift(c);var k=e,l=h.indexOf(":")<0?"on"+h:"";do{var m=f._data(k,"handle");c.currentTarget=k,m&&m.apply(k,d),l&&f.acceptData(k)&&k[l]&&k[l].apply(k,d)===!1&&(c.result=!1,c.preventDefault()),k=k.parentNode||k.ownerDocument||k===c.target.ownerDocument&&a}while(k&&!c.isPropagationStopped());if(!c.isDefaultPrevented()){var n,o=f.event.special[h]||{};if((!o._default||o._default.call(e.ownerDocument,c)===!1)&&(h!=="click"||!f.nodeName(e,"a"))&&f.acceptData(e)){try{l&&e[h]&&(n=e[l],n&&(e[l]=null),f.event.triggered=h,e[h]())}catch(p){}n&&(e[l]=n),f.event.triggered=b}}return c.result}},handle:function(c){c=f.event.fix(c||a.event);var d=((f._data(this,"events")||{})[c.type]||[]).slice(0),e=!c.exclusive&&!c.namespace,g=Array.prototype.slice.call(arguments,0);g[0]=c,c.currentTarget=this;for(var h=0,i=d.length;h-1?f.map(a.options,function(a){return a.selected}).join("-"):"":f.nodeName(a,"select")&&(c=a.selectedIndex);return c},K=function(c){var d=c.target,e,g;if(!!z.test(d.nodeName)&&!d.readOnly){e=f._data(d,"_change_data"),g=J(d),(c.type!=="focusout"||d.type!=="radio")&&f._data(d,"_change_data",g);if(e===b||g===e)return;if(e!=null||g)c.type="change",c.liveFired=b,f.event.trigger(c,arguments[1],d)}};f.event.special.change={filters:{focusout:K,beforedeactivate:K,click:function(a){var b=a.target,c=f.nodeName(b,"input")?b.type:"";(c==="radio"||c==="checkbox"||f.nodeName(b,"select"))&&K.call(this,a)},keydown:function(a){var b=a.target,c=f.nodeName(b,"input")?b.type:"";(a.keyCode===13&&!f.nodeName(b,"textarea")||a.keyCode===32&&(c==="checkbox"||c==="radio")||c==="select-multiple")&&K.call(this,a)},beforeactivate:function(a){var b=a.target;f._data(b,"_change_data",J(b))}},setup:function(a,b){if(this.type==="file")return!1;for(var c in I)f.event.add(this,c+".specialChange",I[c]);return z.test(this.nodeName)},teardown:function(a){f.event.remove(this,".specialChange");return z.test(this.nodeName)}},I=f.event.special.change.filters,I.focus=I.beforeactivate}f.support.focusinBubbles||f.each({focus:"focusin",blur:"focusout"},function(a,b){function e(a){var c=f.event.fix(a);c.type=b,c.originalEvent={},f.event.trigger(c,null,c.target),c.isDefaultPrevented()&&a.preventDefault()}var d=0;f.event.special[b]={setup:function(){d++===0&&c.addEventListener(a,e,!0)},teardown:function(){--d===0&&c.removeEventListener(a,e,!0)}}}),f.each(["bind","one"],function(a,c){f.fn[c]=function(a,d,e){var g;if(typeof a=="object"){for(var h in a)this[c](h,d,a[h],e);return this}if(arguments.length===2||d===!1)e=d,d=b;c==="one"?(g=function(a){f(this).unbind(a,g);return e.apply(this,arguments)},g.guid=e.guid||f.guid++):g=e;if(a==="unload"&&c!=="one")this.one(a,d,e);else for(var i=0,j=this.length;i0?this.bind(b,a,c):this.trigger(b)},f.attrFn&&(f.attrFn[b]=!0)}),function(){function u(a,b,c,d,e,f){for(var g=0,h=d.length;g0){j=i;break}}i=i[a]}d[g]=j}}}function t(a,b,c,d,e,f){for(var g=0,h=d.length;g+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g,d=0,e=Object.prototype.toString,g=!1,h=!0,i=/\\/g,j=/\W/;[0,0].sort(function(){h=!1;return 0});var k=function(b,d,f,g){f=f||[],d=d||c;var h=d;if(d.nodeType!==1&&d.nodeType!==9)return[];if(!b||typeof b!="string")return f;var i,j,n,o,q,r,s,t,u=!0,w=k.isXML(d),x=[],y=b;do{a.exec(""),i=a.exec(y);if(i){y=i[3],x.push(i[1]);if(i[2]){o=i[3];break}}}while(i);if(x.length>1&&m.exec(b))if(x.length===2&&l.relative[x[0]])j=v(x[0]+x[1],d);else{j=l.relative[x[0]]?[d]:k(x.shift(),d);while(x.length)b=x.shift(),l.relative[b]&&(b+=x.shift()),j=v(b,j)}else{!g&&x.length>1&&d.nodeType===9&&!w&&l.match.ID.test(x[0])&&!l.match.ID.test(x[x.length-1])&&(q=k.find(x.shift(),d,w),d=q.expr?k.filter(q.expr,q.set)[0]:q.set[0]);if(d){q=g?{expr:x.pop(),set:p(g)}:k.find(x.pop(),x.length===1&&(x[0]==="~"||x[0]==="+")&&d.parentNode?d.parentNode:d,w),j=q.expr?k.filter(q.expr,q.set):q.set,x.length>0?n=p(j):u=!1;while(x.length)r=x.pop(),s=r,l.relative[r]?s=x.pop():r="",s==null&&(s=d),l.relative[r](n,s,w)}else n=x=[]}n||(n=j),n||k.error(r||b);if(e.call(n)==="[object Array]")if(!u)f.push.apply(f,n);else if(d&&d.nodeType===1)for(t=0;n[t]!=null;t++)n[t]&&(n[t]===!0||n[t].nodeType===1&&k.contains(d,n[t]))&&f.push(j[t]);else for(t=0;n[t]!=null;t++)n[t]&&n[t].nodeType===1&&f.push(j[t]);else p(n,f);o&&(k(o,h,f,g),k.uniqueSort(f));return f};k.uniqueSort=function(a){if(r){g=h,a.sort(r);if(g)for(var b=1;b0},k.find=function(a,b,c){var d;if(!a)return[];for(var e=0,f=l.order.length;e":function(a,b){var c,d=typeof b=="string",e=0,f=a.length;if(d&&!j.test(b)){b=b.toLowerCase();for(;e=0)?c||d.push(h):c&&(b[g]=!1));return!1},ID:function(a){return a[1].replace(i,"")},TAG:function(a,b){return a[1].replace(i,"").toLowerCase()},CHILD:function(a){if(a[1]==="nth"){a[2]||k.error(a[0]),a[2]=a[2].replace(/^\+|\s*/g,"");var b=/(-?)(\d*)(?:n([+\-]?\d*))?/.exec(a[2]==="even"&&"2n"||a[2]==="odd"&&"2n+1"||!/\D/.test(a[2])&&"0n+"+a[2]||a[2]);a[2]=b[1]+(b[2]||1)-0,a[3]=b[3]-0}else a[2]&&k.error(a[0]);a[0]=d++;return a},ATTR:function(a,b,c,d,e,f){var g=a[1]=a[1].replace(i,"");!f&&l.attrMap[g]&&(a[1]=l.attrMap[g]),a[4]=(a[4]||a[5]||"").replace(i,""),a[2]==="~="&&(a[4]=" "+a[4]+" ");return a},PSEUDO:function(b,c,d,e,f){if(b[1]==="not")if((a.exec(b[3])||"").length>1||/^\w/.test(b[3]))b[3]=k(b[3],null,null,c);else{var g=k.filter(b[3],c,d,!0^f);d||e.push.apply(e,g);return!1}else if(l.match.POS.test(b[0])||l.match.CHILD.test(b[0]))return!0;return b},POS:function(a){a.unshift(!0);return a}},filters:{enabled:function(a){return a.disabled===!1&&a.type!=="hidden"},disabled:function(a){return a.disabled===!0},checked:function(a){return a.checked===!0},selected:function(a){a.parentNode&&a.parentNode.selectedIndex;return a.selected===!0},parent:function(a){return!!a.firstChild},empty:function(a){return!a.firstChild},has:function(a,b,c){return!!k(c[3],a).length},header:function(a){return/h\d/i.test(a.nodeName)},text:function(a){var b=a.getAttribute("type"),c=a.type;return a.nodeName.toLowerCase()==="input"&&"text"===c&&(b===c||b===null)},radio:function(a){return a.nodeName.toLowerCase()==="input"&&"radio"===a.type},checkbox:function(a){return a.nodeName.toLowerCase()==="input"&&"checkbox"===a.type},file:function(a){return a.nodeName.toLowerCase()==="input"&&"file"===a.type},password:function(a){return a.nodeName.toLowerCase()==="input"&&"password"===a.type},submit:function(a){var b=a.nodeName.toLowerCase();return(b==="input"||b==="button")&&"submit"===a.type},image:function(a){return a.nodeName.toLowerCase()==="input"&&"image"===a.type},reset:function(a){var b=a.nodeName.toLowerCase();return(b==="input"||b==="button")&&"reset"===a.type},button:function(a){var b=a.nodeName.toLowerCase();return b==="input"&&"button"===a.type||b==="button"},input:function(a){return/input|select|textarea|button/i.test(a.nodeName)},focus:function(a){return a===a.ownerDocument.activeElement}},setFilters:{first:function(a,b){return b===0},last:function(a,b,c,d){return b===d.length-1},even:function(a,b){return b%2===0},odd:function(a,b){return b%2===1},lt:function(a,b,c){return bc[3]-0},nth:function(a,b,c){return c[3]-0===b},eq:function(a,b,c){return c[3]-0===b}},filter:{PSEUDO:function(a,b,c,d){var e=b[1],f=l.filters[e];if(f)return f(a,c,b,d);if(e==="contains")return(a.textContent||a.innerText||k.getText([a])||"").indexOf(b[3])>=0;if(e==="not"){var g=b[3];for(var h=0,i=g.length;h=0}},ID:function(a,b){return a.nodeType===1&&a.getAttribute("id")===b},TAG:function(a,b){return b==="*"&&a.nodeType===1||a.nodeName.toLowerCase()===b},CLASS:function(a,b){return(" "+(a.className||a.getAttribute("class"))+" ").indexOf(b)>-1},ATTR:function(a,b){var c=b[1],d=l.attrHandle[c]?l.attrHandle[c](a):a[c]!=null?a[c]:a.getAttribute(c),e=d+"",f=b[2],g=b[4];return d==null?f==="!=":f==="="?e===g:f==="*="?e.indexOf(g)>=0:f==="~="?(" "+e+" ").indexOf(g)>=0:g?f==="!="?e!==g:f==="^="?e.indexOf(g)===0:f==="$="?e.substr(e.length-g.length)===g:f==="|="?e===g||e.substr(0,g.length+1)===g+"-":!1:e&&d!==!1},POS:function(a,b,c,d){var e=b[2],f=l.setFilters[e];if(f)return f(a,c,b,d)}}},m=l.match.POS,n=function(a,b){return"\\"+(b-0+1)};for(var o in l.match)l.match[o]=new RegExp(l.match[o].source+/(?![^\[]*\])(?![^\(]*\))/.source),l.leftMatch[o]=new RegExp(/(^(?:.|\r|\n)*?)/.source+l.match[o].source.replace(/\\(\d+)/g,n));var p=function(a,b){a=Array.prototype.slice.call(a,0);if(b){b.push.apply(b,a);return b}return a};try{Array.prototype.slice.call(c.documentElement.childNodes,0)[0].nodeType}catch(q){p=function(a,b){var c=0,d=b||[];if(e.call(a)==="[object Array]")Array.prototype.push.apply(d,a);else if(typeof a.length=="number")for(var f=a.length;c",e.insertBefore(a,e.firstChild),c.getElementById(d)&&(l.find.ID=function(a,c,d){if(typeof c.getElementById!="undefined"&&!d){var e=c.getElementById(a[1]);return e?e.id===a[1]||typeof e.getAttributeNode!="undefined"&&e.getAttributeNode("id").nodeValue===a[1]?[e]:b:[]}},l.filter.ID=function(a,b){var c=typeof a.getAttributeNode!="undefined"&&a.getAttributeNode("id");return a.nodeType===1&&c&&c.nodeValue===b}),e.removeChild(a),e=a=null}(),function(){var a=c.createElement("div");a.appendChild(c.createComment("")),a.getElementsByTagName("*").length>0&&(l.find.TAG=function(a,b){var c=b.getElementsByTagName(a[1]);if(a[1]==="*"){var d=[];for(var e=0;c[e];e++)c[e].nodeType===1&&d.push(c[e]);c=d}return c}),a.innerHTML="",a.firstChild&&typeof a.firstChild.getAttribute!="undefined"&&a.firstChild.getAttribute("href")!=="#"&&(l.attrHandle.href=function(a){return a.getAttribute("href",2)}),a=null}(),c.querySelectorAll&&function(){var a=k,b=c.createElement("div"),d="__sizzle__";b.innerHTML="

";if(!b.querySelectorAll||b.querySelectorAll(".TEST").length!==0){k=function(b,e,f,g){e=e||c;if(!g&&!k.isXML(e)){var h=/^(\w+$)|^\.([\w\-]+$)|^#([\w\-]+$)/.exec(b);if(h&&(e.nodeType===1||e.nodeType===9)){if(h[1])return p(e.getElementsByTagName(b),f);if(h[2]&&l.find.CLASS&&e.getElementsByClassName)return p(e.getElementsByClassName(h[2]),f)}if(e.nodeType===9){if(b==="body"&&e.body)return p([e.body],f);if(h&&h[3]){var i=e.getElementById(h[3]);if(!i||!i.parentNode)return p([],f);if(i.id===h[3])return p([i],f)}try{return p(e.querySelectorAll(b),f)}catch(j){}}else if(e.nodeType===1&&e.nodeName.toLowerCase()!=="object"){var m=e,n=e.getAttribute("id"),o=n||d,q=e.parentNode,r=/^\s*[+~]/.test(b);n?o=o.replace(/'/g,"\\$&"):e.setAttribute("id",o),r&&q&&(e=e.parentNode);try{if(!r||q)return p(e.querySelectorAll("[id='"+o+"'] "+b),f)}catch(s){}finally{n||m.removeAttribute("id")}}}return a(b,e,f,g)};for(var e in a)k[e]=a[e];b=null}}(),function(){var a=c.documentElement,b=a.matchesSelector||a.mozMatchesSelector||a.webkitMatchesSelector||a.msMatchesSelector;if(b){var d=!b.call(c.createElement("div"),"div"),e=!1;try{b.call(c.documentElement,"[test!='']:sizzle")}catch(f){e=!0}k.matchesSelector=function(a,c){c=c.replace(/\=\s*([^'"\]]*)\s*\]/g,"='$1']");if(!k.isXML(a))try{if(e||!l.match.PSEUDO.test(c)&&!/!=/.test(c)){var f=b.call(a,c);if(f||!d||a.document&&a.document.nodeType!==11)return f}}catch(g){}return k(c,null,null,[a]).length>0}}}(),function(){var a=c.createElement("div");a.innerHTML="
";if(!!a.getElementsByClassName&&a.getElementsByClassName("e").length!==0){a.lastChild.className="e";if(a.getElementsByClassName("e").length===1)return;l.order.splice(1,0,"CLASS"),l.find.CLASS=function(a,b,c){if(typeof b.getElementsByClassName!="undefined"&&!c)return b.getElementsByClassName(a[1])},a=null}}(),c.documentElement.contains?k.contains=function(a,b){return a!==b&&(a.contains?a.contains(b):!0)}:c.documentElement.compareDocumentPosition?k.contains=function(a,b){return!!(a.compareDocumentPosition(b)&16)}:k.contains=function(){return!1},k.isXML=function(a){var b=(a?a.ownerDocument||a:0).documentElement;return b?b.nodeName!=="HTML":!1};var v=function(a,b){var c,d=[],e="",f=b.nodeType?[b]:b;while(c=l.match.PSEUDO.exec(a))e+=c[0],a=a.replace(l.match.PSEUDO,"");a=l.relative[a]?a+"*":a;for(var g=0,h=f.length;g0)for(h=g;h0:this.filter(a).length>0)},closest:function(a,b){var c=[],d,e,g=this[0];if(f.isArray(a)){var h,i,j={},k=1;if(g&&a.length){for(d=0,e=a.length;d-1:f(g).is(h))&&c.push({selector:i,elem:g,level:k});g=g.parentNode,k++}}return c}var l=U.test(a)||typeof a!="string"?f(a,b||this.context):0;for(d=0,e=this.length;d-1:f.find.matchesSelector(g,a)){c.push(g);break}g=g.parentNode;if(!g||!g.ownerDocument||g===b||g.nodeType===11)break}}c=c.length>1?f.unique(c):c;return this.pushStack(c,"closest",a)},index:function(a){if(!a||typeof a=="string")return f.inArray(this[0],a?f(a):this.parent().children());return f.inArray(a.jquery?a[0]:a,this)},add:function(a,b){var c=typeof a=="string"?f(a,b):f.makeArray(a&&a.nodeType?[a]:a),d=f.merge(this.get(),c);return this.pushStack(W(c[0])||W(d[0])?d:f.unique(d))},andSelf:function(){return this.add(this.prevObject)}}),f.each({parent:function(a){var b=a.parentNode;return b&&b.nodeType!==11?b:null},parents:function(a){return f.dir(a,"parentNode")},parentsUntil:function(a,b,c){return f.dir(a,"parentNode",c)},next:function(a){return f.nth(a,2,"nextSibling")},prev:function(a){return f.nth(a,2,"previousSibling")},nextAll:function(a){return f.dir(a,"nextSibling")},prevAll:function(a){return f.dir(a,"previousSibling")},nextUntil:function(a,b,c){return f.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return f.dir(a,"previousSibling",c)},siblings:function(a){return f.sibling(a.parentNode.firstChild,a)},children:function(a){return f.sibling(a.firstChild)},contents:function(a){return f.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:f.makeArray(a.childNodes)}},function(a,b){f.fn[a]=function(c,d){var e=f.map(this,b,c),g=T.call(arguments);P.test(a)||(d=c),d&&typeof d=="string"&&(e=f.filter(d,e)),e=this.length>1&&!V[a]?f.unique(e):e,(this.length>1||R.test(d))&&Q.test(a)&&(e=e.reverse());return this.pushStack(e,a,g.join(","))}}),f.extend({filter:function(a,b,c){c&&(a=":not("+a+")");return b.length===1?f.find.matchesSelector(b[0],a)?[b[0]]:[]:f.find.matches(a,b)},dir:function(a,c,d){var e=[],g=a[c];while(g&&g.nodeType!==9&&(d===b||g.nodeType!==1||!f(g).is(d)))g.nodeType===1&&e.push(g),g=g[c];return e},nth:function(a,b,c,d){b=b||1;var e=0;for(;a;a=a[c])if(a.nodeType===1&&++e===b)break;return a},sibling:function(a,b){var c=[];for(;a;a=a.nextSibling)a.nodeType===1&&a!==b&&c.push(a);return c}});var Y=/ jQuery\d+="(?:\d+|null)"/g,Z=/^\s+/,$=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/ig,_=/<([\w:]+)/,ba=/",""],legend:[1,"
","
"],thead:[1,"","
"],tr:[2,"","
"],td:[3,"","
"],col:[2,"","
"],area:[1,"",""],_default:[0,"",""]};bg.optgroup=bg.option,bg.tbody=bg.tfoot=bg.colgroup=bg.caption=bg.thead,bg.th=bg.td,f.support.htmlSerialize||(bg._default=[1,"div
","
"]),f.fn.extend({text:function(a){if(f.isFunction(a))return this.each(function(b){var c=f(this);c.text(a.call(this,b,c.text()))});if(typeof a!="object"&&a!==b)return this.empty().append((this[0]&&this[0].ownerDocument||c).createTextNode(a));return f.text(this)},wrapAll:function(a){if(f.isFunction(a))return this.each(function(b){f(this).wrapAll(a.call(this,b))});if(this[0]){var b=f(a,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstChild&&a.firstChild.nodeType===1)a=a.firstChild;return a}).append(this)}return this},wrapInner:function(a){if(f.isFunction(a))return this.each(function(b){f(this).wrapInner(a.call(this,b))});return this.each(function(){var b=f(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){return this.each(function(){f(this).wrapAll(a)})},unwrap:function(){return this.parent().each(function(){f.nodeName(this,"body")||f(this).replaceWith(this.childNodes)}).end()},append:function(){return this.domManip(arguments,!0,function(a){this.nodeType===1&&this.appendChild(a)})},prepend:function(){return this.domManip(arguments,!0,function(a){this.nodeType===1&&this.insertBefore(a,this.firstChild)})},before:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this)});if(arguments.length){var a=f(arguments[0]);a.push.apply(a,this.toArray());return this.pushStack(a,"before",arguments)}},after:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this.nextSibling)});if(arguments.length){var a=this.pushStack(this,"after",arguments);a.push.apply(a,f(arguments[0]).toArray());return a}},remove:function(a,b){for(var c=0,d;(d=this[c])!=null;c++)if(!a||f.filter(a,[d]).length)!b&&d.nodeType===1&&(f.cleanData(d.getElementsByTagName("*")),f.cleanData([d])),d.parentNode&&d.parentNode.removeChild(d);return this},empty:function(){for(var a=0,b;(b=this[a])!=null;a++){b.nodeType===1&&f.cleanData(b.getElementsByTagName("*"));while(b.firstChild)b.removeChild(b.firstChild)}return this},clone:function(a,b){a=a==null?!1:a,b=b==null?a:b;return this.map(function(){return f.clone(this,a,b)})},html:function(a){if(a===b)return this[0]&&this[0].nodeType===1?this[0].innerHTML.replace(Y,""):null;if(typeof a=="string"&&!bc.test(a)&&(f.support.leadingWhitespace||!Z.test(a))&&!bg[(_.exec(a)||["",""])[1].toLowerCase()]){a=a.replace($,"<$1>");try{for(var c=0,d=this.length;c1&&l0?this.clone(!0):this).get();f(e[h])[b](j),d=d.concat(j)}return this.pushStack(d,a,e.selector)}}),f.extend({clone:function(a,b,c){var d=a.cloneNode(!0),e,g,h;if((!f.support.noCloneEvent||!f.support.noCloneChecked)&&(a.nodeType===1||a.nodeType===11)&&!f.isXMLDoc(a)){bj(a,d),e=bk(a),g=bk(d);for(h=0;e[h];++h)bj(e[h],g[h])}if(b){bi(a,d);if(c){e=bk(a),g=bk(d);for(h=0;e[h];++h)bi(e[h],g[h])}}return d},clean:function(a,b,d,e){var g;b=b||c,typeof b.createElement=="undefined"&&(b=b.ownerDocument|| +b[0]&&b[0].ownerDocument||c);var h=[],i;for(var j=0,k;(k=a[j])!=null;j++){typeof k=="number"&&(k+="");if(!k)continue;if(typeof k=="string")if(!bb.test(k))k=b.createTextNode(k);else{k=k.replace($,"<$1>");var l=(_.exec(k)||["",""])[1].toLowerCase(),m=bg[l]||bg._default,n=m[0],o=b.createElement("div");o.innerHTML=m[1]+k+m[2];while(n--)o=o.lastChild;if(!f.support.tbody){var p=ba.test(k),q=l==="table"&&!p?o.firstChild&&o.firstChild.childNodes:m[1]===""&&!p?o.childNodes:[];for(i=q.length-1;i>=0;--i)f.nodeName(q[i],"tbody")&&!q[i].childNodes.length&&q[i].parentNode.removeChild(q[i])}!f.support.leadingWhitespace&&Z.test(k)&&o.insertBefore(b.createTextNode(Z.exec(k)[0]),o.firstChild),k=o.childNodes}var r;if(!f.support.appendChecked)if(k[0]&&typeof (r=k.length)=="number")for(i=0;i=0)return b+"px"}}}),f.support.opacity||(f.cssHooks.opacity={get:function(a,b){return bp.test((b&&a.currentStyle?a.currentStyle.filter:a.style.filter)||"")?parseFloat(RegExp.$1)/100+"":b?"1":""},set:function(a,b){var c=a.style,d=a.currentStyle;c.zoom=1;var e=f.isNaN(b)?"":"alpha(opacity="+b*100+")",g=d&&d.filter||c.filter||"";c.filter=bo.test(g)?g.replace(bo,e):g+" "+e}}),f(function(){f.support.reliableMarginRight||(f.cssHooks.marginRight={get:function(a,b){var c;f.swap(a,{display:"inline-block"},function(){b?c=bz(a,"margin-right","marginRight"):c=a.style.marginRight});return c}})}),c.defaultView&&c.defaultView.getComputedStyle&&(bA=function(a,c){var d,e,g;c=c.replace(br,"-$1").toLowerCase();if(!(e=a.ownerDocument.defaultView))return b;if(g=e.getComputedStyle(a,null))d=g.getPropertyValue(c),d===""&&!f.contains(a.ownerDocument.documentElement,a)&&(d=f.style(a,c));return d}),c.documentElement.currentStyle&&(bB=function(a,b){var c,d=a.currentStyle&&a.currentStyle[b],e=a.runtimeStyle&&a.runtimeStyle[b],f=a.style;!bs.test(d)&&bt.test(d)&&(c=f.left,e&&(a.runtimeStyle.left=a.currentStyle.left),f.left=b==="fontSize"?"1em":d||0,d=f.pixelLeft+"px",f.left=c,e&&(a.runtimeStyle.left=e));return d===""?"auto":d}),bz=bA||bB,f.expr&&f.expr.filters&&(f.expr.filters.hidden=function(a){var b=a.offsetWidth,c=a.offsetHeight;return b===0&&c===0||!f.support.reliableHiddenOffsets&&(a.style.display||f.css(a,"display"))==="none"},f.expr.filters.visible=function(a){return!f.expr.filters.hidden(a)});var bE=/%20/g,bF=/\[\]$/,bG=/\r?\n/g,bH=/#.*$/,bI=/^(.*?):[ \t]*([^\r\n]*)\r?$/mg,bJ=/^(?:color|date|datetime|email|hidden|month|number|password|range|search|tel|text|time|url|week)$/i,bK=/^(?:about|app|app\-storage|.+\-extension|file|widget):$/,bL=/^(?:GET|HEAD)$/,bM=/^\/\//,bN=/\?/,bO=/)<[^<]*)*<\/script>/gi,bP=/^(?:select|textarea)/i,bQ=/\s+/,bR=/([?&])_=[^&]*/,bS=/^([\w\+\.\-]+:)(?:\/\/([^\/?#:]*)(?::(\d+))?)?/,bT=f.fn.load,bU={},bV={},bW,bX;try{bW=e.href}catch(bY){bW=c.createElement("a"),bW.href="",bW=bW.href}bX=bS.exec(bW.toLowerCase())||[],f.fn.extend({load:function(a,c,d){if(typeof a!="string"&&bT)return bT.apply(this,arguments);if(!this.length)return this;var e=a.indexOf(" ");if(e>=0){var g=a.slice(e,a.length);a=a.slice(0,e)}var h="GET";c&&(f.isFunction(c)?(d=c,c=b):typeof c=="object"&&(c=f.param(c,f.ajaxSettings.traditional),h="POST"));var i=this;f.ajax({url:a,type:h,dataType:"html",data:c,complete:function(a,b,c){c=a.responseText,a.isResolved()&&(a.done(function(a){c=a}),i.html(g?f("
").append(c.replace(bO,"")).find(g):c)),d&&i.each(d,[c,b,a])}});return this},serialize:function(){return f.param(this.serializeArray())},serializeArray:function(){return this.map(function(){return this.elements?f.makeArray(this.elements):this}).filter(function(){return this.name&&!this.disabled&&(this.checked||bP.test(this.nodeName)||bJ.test(this.type))}).map(function(a,b){var c=f(this).val();return c==null?null:f.isArray(c)?f.map(c,function(a,c){return{name:b.name,value:a.replace(bG,"\r\n")}}):{name:b.name,value:c.replace(bG,"\r\n")}}).get()}}),f.each("ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split(" "),function(a,b){f.fn[b]=function(a){return this.bind(b,a)}}),f.each(["get","post"],function(a,c){f[c]=function(a,d,e,g){f.isFunction(d)&&(g=g||e,e=d,d=b);return f.ajax({type:c,url:a,data:d,success:e,dataType:g})}}),f.extend({getScript:function(a,c){return f.get(a,b,c,"script")},getJSON:function(a,b,c){return f.get(a,b,c,"json")},ajaxSetup:function(a,b){b?f.extend(!0,a,f.ajaxSettings,b):(b=a,a=f.extend(!0,f.ajaxSettings,b));for(var c in{context:1,url:1})c in b?a[c]=b[c]:c in f.ajaxSettings&&(a[c]=f.ajaxSettings[c]);return a},ajaxSettings:{url:bW,isLocal:bK.test(bX[1]),global:!0,type:"GET",contentType:"application/x-www-form-urlencoded",processData:!0,async:!0,accepts:{xml:"application/xml, text/xml",html:"text/html",text:"text/plain",json:"application/json, text/javascript","*":"*/*"},contents:{xml:/xml/,html:/html/,json:/json/},responseFields:{xml:"responseXML",text:"responseText"},converters:{"* text":a.String,"text html":!0,"text json":f.parseJSON,"text xml":f.parseXML}},ajaxPrefilter:bZ(bU),ajaxTransport:bZ(bV),ajax:function(a,c){function w(a,c,l,m){if(s!==2){s=2,q&&clearTimeout(q),p=b,n=m||"",v.readyState=a?4:0;var o,r,u,w=l?ca(d,v,l):b,x,y;if(a>=200&&a<300||a===304){if(d.ifModified){if(x=v.getResponseHeader("Last-Modified"))f.lastModified[k]=x;if(y=v.getResponseHeader("Etag"))f.etag[k]=y}if(a===304)c="notmodified",o=!0;else try{r=cb(d,w),c="success",o=!0}catch(z){c="parsererror",u=z}}else{u=c;if(!c||a)c="error",a<0&&(a=0)}v.status=a,v.statusText=c,o?h.resolveWith(e,[r,c,v]):h.rejectWith(e,[v,c,u]),v.statusCode(j),j=b,t&&g.trigger("ajax"+(o?"Success":"Error"),[v,d,o?r:u]),i.resolveWith(e,[v,c]),t&&(g.trigger("ajaxComplete",[v,d]),--f.active||f.event.trigger("ajaxStop"))}}typeof a=="object"&&(c=a,a=b),c=c||{};var d=f.ajaxSetup({},c),e=d.context||d,g=e!==d&&(e.nodeType||e instanceof f)?f(e):f.event,h=f.Deferred(),i=f._Deferred(),j=d.statusCode||{},k,l={},m={},n,o,p,q,r,s=0,t,u,v={readyState:0,setRequestHeader:function(a,b){if(!s){var c=a.toLowerCase();a=m[c]=m[c]||a,l[a]=b}return this},getAllResponseHeaders:function(){return s===2?n:null},getResponseHeader:function(a){var c;if(s===2){if(!o){o={};while(c=bI.exec(n))o[c[1].toLowerCase()]=c[2]}c=o[a.toLowerCase()]}return c===b?null:c},overrideMimeType:function(a){s||(d.mimeType=a);return this},abort:function(a){a=a||"abort",p&&p.abort(a),w(0,a);return this}};h.promise(v),v.success=v.done,v.error=v.fail,v.complete=i.done,v.statusCode=function(a){if(a){var b;if(s<2)for(b in a)j[b]=[j[b],a[b]];else b=a[v.status],v.then(b,b)}return this},d.url=((a||d.url)+"").replace(bH,"").replace(bM,bX[1]+"//"),d.dataTypes=f.trim(d.dataType||"*").toLowerCase().split(bQ),d.crossDomain==null&&(r=bS.exec(d.url.toLowerCase()),d.crossDomain=!(!r||r[1]==bX[1]&&r[2]==bX[2]&&(r[3]||(r[1]==="http:"?80:443))==(bX[3]||(bX[1]==="http:"?80:443)))),d.data&&d.processData&&typeof d.data!="string"&&(d.data=f.param(d.data,d.traditional)),b$(bU,d,c,v);if(s===2)return!1;t=d.global,d.type=d.type.toUpperCase(),d.hasContent=!bL.test(d.type),t&&f.active++===0&&f.event.trigger("ajaxStart");if(!d.hasContent){d.data&&(d.url+=(bN.test(d.url)?"&":"?")+d.data),k=d.url;if(d.cache===!1){var x=f.now(),y=d.url.replace(bR,"$1_="+x);d.url=y+(y===d.url?(bN.test(d.url)?"&":"?")+"_="+x:"")}}(d.data&&d.hasContent&&d.contentType!==!1||c.contentType)&&v.setRequestHeader("Content-Type",d.contentType),d.ifModified&&(k=k||d.url,f.lastModified[k]&&v.setRequestHeader("If-Modified-Since",f.lastModified[k]),f.etag[k]&&v.setRequestHeader("If-None-Match",f.etag[k])),v.setRequestHeader("Accept",d.dataTypes[0]&&d.accepts[d.dataTypes[0]]?d.accepts[d.dataTypes[0]]+(d.dataTypes[0]!=="*"?", */*; q=0.01":""):d.accepts["*"]);for(u in d.headers)v.setRequestHeader(u,d.headers[u]);if(d.beforeSend&&(d.beforeSend.call(e,v,d)===!1||s===2)){v.abort();return!1}for(u in{success:1,error:1,complete:1})v[u](d[u]);p=b$(bV,d,c,v);if(!p)w(-1,"No Transport");else{v.readyState=1,t&&g.trigger("ajaxSend",[v,d]),d.async&&d.timeout>0&&(q=setTimeout(function(){v.abort("timeout")},d.timeout));try{s=1,p.send(l,w)}catch(z){status<2?w(-1,z):f.error(z)}}return v},param:function(a,c){var d=[],e=function(a,b){b=f.isFunction(b)?b():b,d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(b)};c===b&&(c=f.ajaxSettings.traditional);if(f.isArray(a)||a.jquery&&!f.isPlainObject(a))f.each(a,function(){e(this.name,this.value)});else for(var g in a)b_(g,a[g],c,e);return d.join("&").replace(bE,"+")}}),f.extend({active:0,lastModified:{},etag:{}});var cc=f.now(),cd=/(\=)\?(&|$)|\?\?/i;f.ajaxSetup({jsonp:"callback",jsonpCallback:function(){return f.expando+"_"+cc++}}),f.ajaxPrefilter("json jsonp",function(b,c,d){var e=b.contentType==="application/x-www-form-urlencoded"&&typeof b.data=="string";if(b.dataTypes[0]==="jsonp"||b.jsonp!==!1&&(cd.test(b.url)||e&&cd.test(b.data))){var g,h=b.jsonpCallback=f.isFunction(b.jsonpCallback)?b.jsonpCallback():b.jsonpCallback,i=a[h],j=b.url,k=b.data,l="$1"+h+"$2";b.jsonp!==!1&&(j=j.replace(cd,l),b.url===j&&(e&&(k=k.replace(cd,l)),b.data===k&&(j+=(/\?/.test(j)?"&":"?")+b.jsonp+"="+h))),b.url=j,b.data=k,a[h]=function(a){g=[a]},d.always(function(){a[h]=i,g&&f.isFunction(i)&&a[h](g[0])}),b.converters["script json"]=function(){g||f.error(h+" was not called");return g[0]},b.dataTypes[0]="json";return"script"}}),f.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/javascript|ecmascript/},converters:{"text script":function(a){f.globalEval(a);return a}}}),f.ajaxPrefilter("script",function(a){a.cache===b&&(a.cache=!1),a.crossDomain&&(a.type="GET",a.global=!1)}),f.ajaxTransport("script",function(a){if(a.crossDomain){var d,e=c.head||c.getElementsByTagName("head")[0]||c.documentElement;return{send:function(f,g){d=c.createElement("script"),d.async="async",a.scriptCharset&&(d.charset=a.scriptCharset),d.src=a.url,d.onload=d.onreadystatechange=function(a,c){if(c||!d.readyState||/loaded|complete/.test(d.readyState))d.onload=d.onreadystatechange=null,e&&d.parentNode&&e.removeChild(d),d=b,c||g(200,"success")},e.insertBefore(d,e.firstChild)},abort:function(){d&&d.onload(0,1)}}}});var ce=a.ActiveXObject?function(){for(var a in cg)cg[a](0,1)}:!1,cf=0,cg;f.ajaxSettings.xhr=a.ActiveXObject?function(){return!this.isLocal&&ch()||ci()}:ch,function(a){f.extend(f.support,{ajax:!!a,cors:!!a&&"withCredentials"in a})}(f.ajaxSettings.xhr()),f.support.ajax&&f.ajaxTransport(function(c){if(!c.crossDomain||f.support.cors){var d;return{send:function(e,g){var h=c.xhr(),i,j;c.username?h.open(c.type,c.url,c.async,c.username,c.password):h.open(c.type,c.url,c.async);if(c.xhrFields)for(j in c.xhrFields)h[j]=c.xhrFields[j];c.mimeType&&h.overrideMimeType&&h.overrideMimeType(c.mimeType),!c.crossDomain&&!e["X-Requested-With"]&&(e["X-Requested-With"]="XMLHttpRequest");try{for(j in e)h.setRequestHeader(j,e[j])}catch(k){}h.send(c.hasContent&&c.data||null),d=function(a,e){var j,k,l,m,n;try{if(d&&(e||h.readyState===4)){d=b,i&&(h.onreadystatechange=f.noop,ce&&delete cg[i]);if(e)h.readyState!==4&&h.abort();else{j=h.status,l=h.getAllResponseHeaders(),m={},n=h.responseXML,n&&n.documentElement&&(m.xml=n),m.text=h.responseText;try{k=h.statusText}catch(o){k=""}!j&&c.isLocal&&!c.crossDomain?j=m.text?200:404:j===1223&&(j=204)}}}catch(p){e||g(-1,p)}m&&g(j,k,m,l)},!c.async||h.readyState===4?d():(i=++cf,ce&&(cg||(cg={},f(a).unload(ce)),cg[i]=d),h.onreadystatechange=d)},abort:function(){d&&d(0,1)}}}});var cj={},ck,cl,cm=/^(?:toggle|show|hide)$/,cn=/^([+\-]=)?([\d+.\-]+)([a-z%]*)$/i,co,cp=[["height","marginTop","marginBottom","paddingTop","paddingBottom"],["width","marginLeft","marginRight","paddingLeft","paddingRight"],["opacity"]],cq,cr=a.webkitRequestAnimationFrame||a.mozRequestAnimationFrame||a.oRequestAnimationFrame;f.fn.extend({show:function(a,b,c){var d,e;if(a||a===0)return this.animate(cu("show",3),a,b,c);for(var g=0,h=this.length;g=e.duration+this.startTime){this.now=this.end,this.pos=this.state=1,this.update(),e.animatedProperties[this.prop]=!0;for(g in e.animatedProperties)e.animatedProperties[g]!==!0&&(c=!1);if(c){e.overflow!=null&&!f.support.shrinkWrapBlocks&&f.each(["","X","Y"],function(a,b){d.style["overflow"+b]=e.overflow[a]}),e.hide&&f(d).hide();if(e.hide||e.show)for(var i in e.animatedProperties)f.style(d,i,e.orig[i]);e.complete.call(d)}return!1}e.duration==Infinity?this.now=b:(h=b-this.startTime,this.state=h/e.duration,this.pos=f.easing[e.animatedProperties[this.prop]](this.state,h,0,1,e.duration),this.now=this.start+(this.end-this.start)*this.pos),this.update();return!0}},f.extend(f.fx,{tick:function(){for(var a=f.timers,b=0;b
";f.extend(b.style,{position:"absolute",top:0,left:0,margin:0,border:0,width:"1px",height:"1px",visibility:"hidden"}),b.innerHTML=j,a.insertBefore(b,a.firstChild),d=b.firstChild,e=d.firstChild,h=d.nextSibling.firstChild.firstChild,this.doesNotAddBorder=e.offsetTop!==5,this.doesAddBorderForTableAndCells=h.offsetTop===5,e.style.position="fixed",e.style.top="20px",this.supportsFixedPosition=e.offsetTop===20||e.offsetTop===15,e.style.position=e.style.top="",d.style.overflow="hidden",d.style.position="relative",this.subtractsBorderForOverflowNotVisible=e.offsetTop===-5,this.doesNotIncludeMarginInBodyOffset=a.offsetTop!==i,a.removeChild(b),f.offset.initialize=f.noop},bodyOffset:function(a){var b=a.offsetTop,c=a.offsetLeft;f.offset.initialize(),f.offset.doesNotIncludeMarginInBodyOffset&&(b+=parseFloat(f.css(a,"marginTop"))||0,c+=parseFloat(f.css(a,"marginLeft"))||0);return{top:b,left:c}},setOffset:function(a,b,c){var d=f.css(a,"position");d==="static"&&(a.style.position="relative");var e=f(a),g=e.offset(),h=f.css(a,"top"),i=f.css(a,"left"),j=(d==="absolute"||d==="fixed")&&f.inArray("auto",[h,i])>-1,k={},l={},m,n;j?(l=e.position(),m=l.top,n=l.left):(m=parseFloat(h)||0,n=parseFloat(i)||0),f.isFunction(b)&&(b=b.call(a,c,g)),b.top!=null&&(k.top=b.top-g.top+m),b.left!=null&&(k.left=b.left-g.left+n),"using"in b?b.using.call(a,k):e.css(k)}},f.fn.extend({position:function(){if(!this[0])return null;var a=this[0],b=this.offsetParent(),c=this.offset(),d=cx.test(b[0].nodeName)?{top:0,left:0}:b.offset();c.top-=parseFloat(f.css(a,"marginTop"))||0,c.left-=parseFloat(f.css(a,"marginLeft"))||0,d.top+=parseFloat(f.css(b[0],"borderTopWidth"))||0,d.left+=parseFloat(f.css(b[0],"borderLeftWidth"))||0;return{top:c.top-d.top,left:c.left-d.left}},offsetParent:function(){return this.map(function(){var a=this.offsetParent||c.body;while(a&&!cx.test(a.nodeName)&&f.css(a,"position")==="static")a=a.offsetParent;return a})}}),f.each(["Left","Top"],function(a,c){var d="scroll"+c;f.fn[d]=function(c){var e,g;if(c===b){e=this[0];if(!e)return null;g=cy(e);return g?"pageXOffset"in g?g[a?"pageYOffset":"pageXOffset"]:f.support.boxModel&&g.document.documentElement[d]||g.document.body[d]:e[d]}return this.each(function(){g=cy(this),g?g.scrollTo(a?f(g).scrollLeft():c,a?c:f(g).scrollTop()):this[d]=c})}}),f.each(["Height","Width"],function(a,c){var d=c.toLowerCase();f.fn["inner"+c]=function(){return this[0]?parseFloat(f.css(this[0],d,"padding")):null},f.fn["outer"+c]=function(a){return this[0]?parseFloat(f.css(this[0],d,a?"margin":"border")):null},f.fn[d]=function(a){var e=this[0];if(!e)return a==null?null:this;if(f.isFunction(a))return this.each(function(b){var c=f(this);c[d](a.call(this,b,c[d]()))});if(f.isWindow(e)){var g=e.document.documentElement["client"+c];return e.document.compatMode==="CSS1Compat"&&g||e.document.body["client"+c]||g}if(e.nodeType===9)return Math.max(e.documentElement["client"+c],e.body["scroll"+c],e.documentElement["scroll"+c],e.body["offset"+c],e.documentElement["offset"+c]);if(a===b){var h=f.css(e,d),i=parseFloat(h);return f.isNaN(i)?h:i}return this.css(d,typeof a=="string"?a:a+"px")}}),a.jQuery=a.$=f})(window); \ No newline at end of file diff --git a/usr/share/sp-smaps-visualize/jquery.tablesorter.js b/usr/share/sp-smaps-visualize/jquery.tablesorter.js new file mode 100644 index 00000000..fc218880 --- /dev/null +++ b/usr/share/sp-smaps-visualize/jquery.tablesorter.js @@ -0,0 +1,1031 @@ +/* + * + * TableSorter 2.0 - Client-side table sorting with ease! + * Version 2.0.5b + * @requires jQuery v1.2.3 + * + * Copyright (c) 2007 Christian Bach + * Examples and docs at: http://tablesorter.com + * Dual licensed under the MIT and GPL licenses: + * http://www.opensource.org/licenses/mit-license.php + * http://www.gnu.org/licenses/gpl.html + * + */ +/** + * + * @description Create a sortable table with multi-column sorting capabilitys + * + * @example $('table').tablesorter(); + * @desc Create a simple tablesorter interface. + * + * @example $('table').tablesorter({ sortList:[[0,0],[1,0]] }); + * @desc Create a tablesorter interface and sort on the first and secound column column headers. + * + * @example $('table').tablesorter({ headers: { 0: { sorter: false}, 1: {sorter: false} } }); + * + * @desc Create a tablesorter interface and disableing the first and second column headers. + * + * + * @example $('table').tablesorter({ headers: { 0: {sorter:"integer"}, 1: {sorter:"currency"} } }); + * + * @desc Create a tablesorter interface and set a column parser for the first + * and second column. + * + * + * @param Object + * settings An object literal containing key/value pairs to provide + * optional settings. + * + * + * @option String cssHeader (optional) A string of the class name to be appended + * to sortable tr elements in the thead of the table. Default value: + * "header" + * + * @option String cssAsc (optional) A string of the class name to be appended to + * sortable tr elements in the thead on a ascending sort. Default value: + * "headerSortUp" + * + * @option String cssDesc (optional) A string of the class name to be appended + * to sortable tr elements in the thead on a descending sort. Default + * value: "headerSortDown" + * + * @option String sortInitialOrder (optional) A string of the inital sorting + * order can be asc or desc. Default value: "asc" + * + * @option String sortMultisortKey (optional) A string of the multi-column sort + * key. Default value: "shiftKey" + * + * @option String textExtraction (optional) A string of the text-extraction + * method to use. For complex html structures inside td cell set this + * option to "complex", on large tables the complex option can be slow. + * Default value: "simple" + * + * @option Object headers (optional) An array containing the forces sorting + * rules. This option let's you specify a default sorting rule. Default + * value: null + * + * @option Array sortList (optional) An array containing the forces sorting + * rules. This option let's you specify a default sorting rule. Default + * value: null + * + * @option Array sortForce (optional) An array containing forced sorting rules. + * This option let's you specify a default sorting rule, which is + * prepended to user-selected rules. Default value: null + * + * @option Boolean sortLocaleCompare (optional) Boolean flag indicating whatever + * to use String.localeCampare method or not. Default set to true. + * + * + * @option Array sortAppend (optional) An array containing forced sorting rules. + * This option let's you specify a default sorting rule, which is + * appended to user-selected rules. Default value: null + * + * @option Boolean widthFixed (optional) Boolean flag indicating if tablesorter + * should apply fixed widths to the table columns. This is usefull when + * using the pager companion plugin. This options requires the dimension + * jquery plugin. Default value: false + * + * @option Boolean cancelSelection (optional) Boolean flag indicating if + * tablesorter should cancel selection of the table headers text. + * Default value: true + * + * @option Boolean debug (optional) Boolean flag indicating if tablesorter + * should display debuging information usefull for development. + * + * @type jQuery + * + * @name tablesorter + * + * @cat Plugins/Tablesorter + * + * @author Christian Bach/christian.bach@polyester.se + */ + +(function ($) { + $.extend({ + tablesorter: new + function () { + + var parsers = [], + widgets = []; + + this.defaults = { + cssHeader: "header", + cssAsc: "headerSortUp", + cssDesc: "headerSortDown", + cssChildRow: "expand-child", + sortInitialOrder: "asc", + sortMultiSortKey: "shiftKey", + sortForce: null, + sortAppend: null, + sortLocaleCompare: true, + textExtraction: "simple", + parsers: {}, widgets: [], + widgetZebra: { + css: ["even", "odd"] + }, headers: {}, widthFixed: false, + cancelSelection: true, + sortList: [], + headerList: [], + dateFormat: "us", + decimal: '/\.|\,/g', + onRenderHeader: null, + selectorHeaders: 'thead th', + debug: false + }; + + /* debuging utils */ + + function benchmark(s, d) { + log(s + "," + (new Date().getTime() - d.getTime()) + "ms"); + } + + this.benchmark = benchmark; + + function log(s) { + if (typeof console != "undefined" && typeof console.debug != "undefined") { + console.log(s); + } else { + alert(s); + } + } + + /* parsers utils */ + + function buildParserCache(table, $headers) { + + if (table.config.debug) { + var parsersDebug = ""; + } + + if (table.tBodies.length == 0) return; // In the case of empty tables + var rows = table.tBodies[0].rows; + + if (rows[0]) { + + var list = [], + cells = rows[0].cells, + l = cells.length; + + for (var i = 0; i < l; i++) { + + var p = false; + + if ($.metadata && ($($headers[i]).metadata() && $($headers[i]).metadata().sorter)) { + + p = getParserById($($headers[i]).metadata().sorter); + + } else if ((table.config.headers[i] && table.config.headers[i].sorter)) { + + p = getParserById(table.config.headers[i].sorter); + } + if (!p) { + + p = detectParserForColumn(table, rows, -1, i); + } + + if (table.config.debug) { + parsersDebug += "column:" + i + " parser:" + p.id + "\n"; + } + + list.push(p); + } + } + + if (table.config.debug) { + log(parsersDebug); + } + + return list; + }; + + function detectParserForColumn(table, rows, rowIndex, cellIndex) { + var l = parsers.length, + node = false, + nodeValue = false, + keepLooking = true; + while (nodeValue == '' && keepLooking) { + rowIndex++; + if (rows[rowIndex]) { + node = getNodeFromRowAndCellIndex(rows, rowIndex, cellIndex); + nodeValue = trimAndGetNodeText(table.config, node); + if (table.config.debug) { + log('Checking if value was empty on row:' + rowIndex); + } + } else { + keepLooking = false; + } + } + for (var i = 1; i < l; i++) { + if (parsers[i].is(nodeValue, table, node)) { + return parsers[i]; + } + } + // 0 is always the generic parser (text) + return parsers[0]; + } + + function getNodeFromRowAndCellIndex(rows, rowIndex, cellIndex) { + return rows[rowIndex].cells[cellIndex]; + } + + function trimAndGetNodeText(config, node) { + return $.trim(getElementText(config, node)); + } + + function getParserById(name) { + var l = parsers.length; + for (var i = 0; i < l; i++) { + if (parsers[i].id.toLowerCase() == name.toLowerCase()) { + return parsers[i]; + } + } + return false; + } + + /* utils */ + + function buildCache(table) { + + if (table.config.debug) { + var cacheTime = new Date(); + } + + var totalRows = (table.tBodies[0] && table.tBodies[0].rows.length) || 0, + totalCells = (table.tBodies[0].rows[0] && table.tBodies[0].rows[0].cells.length) || 0, + parsers = table.config.parsers, + cache = { + row: [], + normalized: [] + }; + + for (var i = 0; i < totalRows; ++i) { + + /** Add the table data to main data array */ + var c = $(table.tBodies[0].rows[i]), + cols = []; + + // if this is a child row, add it to the last row's children and + // continue to the next row + if (c.hasClass(table.config.cssChildRow)) { + cache.row[cache.row.length - 1] = cache.row[cache.row.length - 1].add(c); + // go to the next for loop + continue; + } + + cache.row.push(c); + + for (var j = 0; j < totalCells; ++j) { + cols.push(parsers[j].format(getElementText(table.config, c[0].cells[j]), table, c[0].cells[j])); + } + + cols.push(cache.normalized.length); // add position for rowCache + cache.normalized.push(cols); + cols = null; + }; + + if (table.config.debug) { + benchmark("Building cache for " + totalRows + " rows:", cacheTime); + } + + return cache; + }; + + function getElementText(config, node) { + + var text = ""; + + if (!node) return ""; + + if (!config.supportsTextContent) config.supportsTextContent = node.textContent || false; + + if (config.textExtraction == "simple") { + if (config.supportsTextContent) { + text = node.textContent; + } else { + if (node.childNodes[0] && node.childNodes[0].hasChildNodes()) { + text = node.childNodes[0].innerHTML; + } else { + text = node.innerHTML; + } + } + } else { + if (typeof(config.textExtraction) == "function") { + text = config.textExtraction(node); + } else { + text = $(node).text(); + } + } + return text; + } + + function appendToTable(table, cache) { + + if (table.config.debug) { + var appendTime = new Date() + } + + var c = cache, + r = c.row, + n = c.normalized, + totalRows = n.length, + checkCell = (n[0].length - 1), + tableBody = $(table.tBodies[0]), + rows = []; + + + for (var i = 0; i < totalRows; i++) { + var pos = n[i][checkCell]; + + rows.push(r[pos]); + + if (!table.config.appender) { + + //var o = ; + var l = r[pos].length; + for (var j = 0; j < l; j++) { + tableBody[0].appendChild(r[pos][j]); + } + + // + } + } + + + + if (table.config.appender) { + + table.config.appender(table, rows); + } + + rows = null; + + if (table.config.debug) { + benchmark("Rebuilt table:", appendTime); + } + + // apply table widgets + applyWidget(table); + + // trigger sortend + setTimeout(function () { + $(table).trigger("sortEnd"); + }, 0); + + }; + + function buildHeaders(table) { + + if (table.config.debug) { + var time = new Date(); + } + + var meta = ($.metadata) ? true : false; + + var header_index = computeTableHeaderCellIndexes(table); + + $tableHeaders = $(table.config.selectorHeaders, table).each(function (index) { + + this.column = header_index[this.parentNode.rowIndex + "-" + this.cellIndex]; + // this.column = index; + this.order = formatSortingOrder(table.config.sortInitialOrder); + + + this.count = this.order; + + if (checkHeaderMetadata(this) || checkHeaderOptions(table, index)) this.sortDisabled = true; + if (checkHeaderOptionsSortingLocked(table, index)) this.order = this.lockedOrder = checkHeaderOptionsSortingLocked(table, index); + + if (!this.sortDisabled) { + var $th = $(this).addClass(table.config.cssHeader); + if (table.config.onRenderHeader) table.config.onRenderHeader.apply($th); + } + + // add cell to headerList + table.config.headerList[index] = this; + }); + + if (table.config.debug) { + benchmark("Built headers:", time); + log($tableHeaders); + } + + return $tableHeaders; + + }; + + // from: + // http://www.javascripttoolbox.com/lib/table/examples.php + // http://www.javascripttoolbox.com/temp/table_cellindex.html + + + function computeTableHeaderCellIndexes(t) { + var matrix = []; + var lookup = {}; + var thead = t.getElementsByTagName('THEAD')[0]; + var trs = thead.getElementsByTagName('TR'); + + for (var i = 0; i < trs.length; i++) { + var cells = trs[i].cells; + for (var j = 0; j < cells.length; j++) { + var c = cells[j]; + + var rowIndex = c.parentNode.rowIndex; + var cellId = rowIndex + "-" + c.cellIndex; + var rowSpan = c.rowSpan || 1; + var colSpan = c.colSpan || 1 + var firstAvailCol; + if (typeof(matrix[rowIndex]) == "undefined") { + matrix[rowIndex] = []; + } + // Find first available column in the first row + for (var k = 0; k < matrix[rowIndex].length + 1; k++) { + if (typeof(matrix[rowIndex][k]) == "undefined") { + firstAvailCol = k; + break; + } + } + lookup[cellId] = firstAvailCol; + for (var k = rowIndex; k < rowIndex + rowSpan; k++) { + if (typeof(matrix[k]) == "undefined") { + matrix[k] = []; + } + var matrixrow = matrix[k]; + for (var l = firstAvailCol; l < firstAvailCol + colSpan; l++) { + matrixrow[l] = "x"; + } + } + } + } + return lookup; + } + + function checkCellColSpan(table, rows, row) { + var arr = [], + r = table.tHead.rows, + c = r[row].cells; + + for (var i = 0; i < c.length; i++) { + var cell = c[i]; + + if (cell.colSpan > 1) { + arr = arr.concat(checkCellColSpan(table, headerArr, row++)); + } else { + if (table.tHead.length == 1 || (cell.rowSpan > 1 || !r[row + 1])) { + arr.push(cell); + } + // headerArr[row] = (i+row); + } + } + return arr; + }; + + function checkHeaderMetadata(cell) { + if (($.metadata) && ($(cell).metadata().sorter === false)) { + return true; + }; + return false; + } + + function checkHeaderOptions(table, i) { + if ((table.config.headers[i]) && (table.config.headers[i].sorter === false)) { + return true; + }; + return false; + } + + function checkHeaderOptionsSortingLocked(table, i) { + if ((table.config.headers[i]) && (table.config.headers[i].lockedOrder)) return table.config.headers[i].lockedOrder; + return false; + } + + function applyWidget(table) { + var c = table.config.widgets; + var l = c.length; + for (var i = 0; i < l; i++) { + + getWidgetById(c[i]).format(table); + } + + } + + function getWidgetById(name) { + var l = widgets.length; + for (var i = 0; i < l; i++) { + if (widgets[i].id.toLowerCase() == name.toLowerCase()) { + return widgets[i]; + } + } + }; + + function formatSortingOrder(v) { + if (typeof(v) != "Number") { + return (v.toLowerCase() == "desc") ? 1 : 0; + } else { + return (v == 1) ? 1 : 0; + } + } + + function isValueInArray(v, a) { + var l = a.length; + for (var i = 0; i < l; i++) { + if (a[i][0] == v) { + return true; + } + } + return false; + } + + function setHeadersCss(table, $headers, list, css) { + // remove all header information + $headers.removeClass(css[0]).removeClass(css[1]); + + var h = []; + $headers.each(function (offset) { + if (!this.sortDisabled) { + h[this.column] = $(this); + } + }); + + var l = list.length; + for (var i = 0; i < l; i++) { + h[list[i][0]].addClass(css[list[i][1]]); + } + } + + function fixColumnWidth(table, $headers) { + var c = table.config; + if (c.widthFixed) { + var colgroup = $(''); + $("tr:first td", table.tBodies[0]).each(function () { + colgroup.append($('').css('width', $(this).width())); + }); + $(table).prepend(colgroup); + }; + } + + function updateHeaderSortCount(table, sortList) { + var c = table.config, + l = sortList.length; + for (var i = 0; i < l; i++) { + var s = sortList[i], + o = c.headerList[s[0]]; + o.count = s[1]; + o.count++; + } + } + + /* sorting methods */ + + function multisort(table, sortList, cache) { + + if (table.config.debug) { + var sortTime = new Date(); + } + + var dynamicExp = "var sortWrapper = function(a,b) {", + l = sortList.length; + + // TODO: inline functions. + for (var i = 0; i < l; i++) { + + var c = sortList[i][0]; + var order = sortList[i][1]; + // var s = (getCachedSortType(table.config.parsers,c) == "text") ? + // ((order == 0) ? "sortText" : "sortTextDesc") : ((order == 0) ? + // "sortNumeric" : "sortNumericDesc"); + // var s = (table.config.parsers[c].type == "text") ? ((order == 0) + // ? makeSortText(c) : makeSortTextDesc(c)) : ((order == 0) ? + // makeSortNumeric(c) : makeSortNumericDesc(c)); + var s = (table.config.parsers[c].type == "text") ? ((order == 0) ? makeSortFunction("text", "asc", c) : makeSortFunction("text", "desc", c)) : ((order == 0) ? makeSortFunction("numeric", "asc", c) : makeSortFunction("numeric", "desc", c)); + var e = "e" + i; + + dynamicExp += "var " + e + " = " + s; // + "(a[" + c + "],b[" + c + // + "]); "; + dynamicExp += "if(" + e + ") { return " + e + "; } "; + dynamicExp += "else { "; + + } + + // if value is the same keep orignal order + var orgOrderCol = cache.normalized[0].length - 1; + dynamicExp += "return a[" + orgOrderCol + "]-b[" + orgOrderCol + "];"; + + for (var i = 0; i < l; i++) { + dynamicExp += "}; "; + } + + dynamicExp += "return 0; "; + dynamicExp += "}; "; + + if (table.config.debug) { + benchmark("Evaling expression:" + dynamicExp, new Date()); + } + + eval(dynamicExp); + + cache.normalized.sort(sortWrapper); + + if (table.config.debug) { + benchmark("Sorting on " + sortList.toString() + " and dir " + order + " time:", sortTime); + } + + return cache; + }; + + function makeSortFunction(type, direction, index) { + var a = "a[" + index + "]", + b = "b[" + index + "]"; + if (type == 'text' && direction == 'asc') { + return "(" + a + " == " + b + " ? 0 : (" + a + " === null ? Number.POSITIVE_INFINITY : (" + b + " === null ? Number.NEGATIVE_INFINITY : (" + a + " < " + b + ") ? -1 : 1 )));"; + } else if (type == 'text' && direction == 'desc') { + return "(" + a + " == " + b + " ? 0 : (" + a + " === null ? Number.POSITIVE_INFINITY : (" + b + " === null ? Number.NEGATIVE_INFINITY : (" + b + " < " + a + ") ? -1 : 1 )));"; + } else if (type == 'numeric' && direction == 'desc') { + return "(" + a + " === null && " + b + " === null) ? 0 :(" + a + " === null ? Number.POSITIVE_INFINITY : (" + b + " === null ? Number.NEGATIVE_INFINITY : " + a + " - " + b + "));"; + } else if (type == 'numeric' && direction == 'asc') { + return "(" + a + " === null && " + b + " === null) ? 0 :(" + a + " === null ? Number.POSITIVE_INFINITY : (" + b + " === null ? Number.NEGATIVE_INFINITY : " + b + " - " + a + "));"; + } + }; + + function makeSortText(i) { + return "((a[" + i + "] < b[" + i + "]) ? -1 : ((a[" + i + "] > b[" + i + "]) ? 1 : 0));"; + }; + + function makeSortTextDesc(i) { + return "((b[" + i + "] < a[" + i + "]) ? -1 : ((b[" + i + "] > a[" + i + "]) ? 1 : 0));"; + }; + + function makeSortNumeric(i) { + return "a[" + i + "]-b[" + i + "];"; + }; + + function makeSortNumericDesc(i) { + return "b[" + i + "]-a[" + i + "];"; + }; + + function sortText(a, b) { + if (table.config.sortLocaleCompare) return a.localeCompare(b); + return ((a < b) ? -1 : ((a > b) ? 1 : 0)); + }; + + function sortTextDesc(a, b) { + if (table.config.sortLocaleCompare) return b.localeCompare(a); + return ((b < a) ? -1 : ((b > a) ? 1 : 0)); + }; + + function sortNumeric(a, b) { + return a - b; + }; + + function sortNumericDesc(a, b) { + return b - a; + }; + + function getCachedSortType(parsers, i) { + return parsers[i].type; + }; /* public methods */ + this.construct = function (settings) { + return this.each(function () { + // if no thead or tbody quit. + if (!this.tHead || !this.tBodies) return; + // declare + var $this, $document, $headers, cache, config, shiftDown = 0, + sortOrder; + // new blank config object + this.config = {}; + // merge and extend. + config = $.extend(this.config, $.tablesorter.defaults, settings); + // store common expression for speed + $this = $(this); + // save the settings where they read + $.data(this, "tablesorter", config); + // build headers + $headers = buildHeaders(this); + // try to auto detect column type, and store in tables config + this.config.parsers = buildParserCache(this, $headers); + // build the cache for the tbody cells + cache = buildCache(this); + // get the css class names, could be done else where. + var sortCSS = [config.cssDesc, config.cssAsc]; + // fixate columns if the users supplies the fixedWidth option + fixColumnWidth(this); + // apply event handling to headers + // this is to big, perhaps break it out? + $headers.click( + + function (e) { + var totalRows = ($this[0].tBodies[0] && $this[0].tBodies[0].rows.length) || 0; + if (!this.sortDisabled && totalRows > 0) { + // Only call sortStart if sorting is + // enabled. + $this.trigger("sortStart"); + // store exp, for speed + var $cell = $(this); + // get current column index + var i = this.column; + // get current column sort order + this.order = this.count++ % 2; + // always sort on the locked order. + if(this.lockedOrder) this.order = this.lockedOrder; + + // user only whants to sort on one + // column + if (!e[config.sortMultiSortKey]) { + // flush the sort list + config.sortList = []; + if (config.sortForce != null) { + var a = config.sortForce; + for (var j = 0; j < a.length; j++) { + if (a[j][0] != i) { + config.sortList.push(a[j]); + } + } + } + // add column to sort list + config.sortList.push([i, this.order]); + // multi column sorting + } else { + // the user has clicked on an all + // ready sortet column. + if (isValueInArray(i, config.sortList)) { + // revers the sorting direction + // for all tables. + for (var j = 0; j < config.sortList.length; j++) { + var s = config.sortList[j], + o = config.headerList[s[0]]; + if (s[0] == i) { + o.count = s[1]; + o.count++; + s[1] = o.count % 2; + } + } + } else { + // add column to sort list array + config.sortList.push([i, this.order]); + } + }; + setTimeout(function () { + // set css for headers + setHeadersCss($this[0], $headers, config.sortList, sortCSS); + appendToTable( + $this[0], multisort( + $this[0], config.sortList, cache) + ); + }, 1); + // stop normal event by returning false + return false; + } + // cancel selection + }).mousedown(function () { + if (config.cancelSelection) { + this.onselectstart = function () { + return false + }; + return false; + } + }); + // apply easy methods that trigger binded events + $this.bind("update", function () { + var me = this; + setTimeout(function () { + // rebuild parsers. + me.config.parsers = buildParserCache( + me, $headers); + // rebuild the cache map + cache = buildCache(me); + }, 1); + }).bind("updateCell", function (e, cell) { + var config = this.config; + // get position from the dom. + var pos = [(cell.parentNode.rowIndex - 1), cell.cellIndex]; + // update cache + cache.normalized[pos[0]][pos[1]] = config.parsers[pos[1]].format( + getElementText(config, cell), cell); + }).bind("sorton", function (e, list) { + $(this).trigger("sortStart"); + config.sortList = list; + // update and store the sortlist + var sortList = config.sortList; + // update header count index + updateHeaderSortCount(this, sortList); + // set css for headers + setHeadersCss(this, $headers, sortList, sortCSS); + // sort the table and append it to the dom + appendToTable(this, multisort(this, sortList, cache)); + }).bind("appendCache", function () { + appendToTable(this, cache); + }).bind("applyWidgetId", function (e, id) { + getWidgetById(id).format(this); + }).bind("applyWidgets", function () { + // apply widgets + applyWidget(this); + }); + if ($.metadata && ($(this).metadata() && $(this).metadata().sortlist)) { + config.sortList = $(this).metadata().sortlist; + } + // if user has supplied a sort list to constructor. + if (config.sortList.length > 0) { + $this.trigger("sorton", [config.sortList]); + } + // apply widgets + applyWidget(this); + }); + }; + this.addParser = function (parser) { + var l = parsers.length, + a = true; + for (var i = 0; i < l; i++) { + if (parsers[i].id.toLowerCase() == parser.id.toLowerCase()) { + a = false; + } + } + if (a) { + parsers.push(parser); + }; + }; + this.addWidget = function (widget) { + widgets.push(widget); + }; + this.formatFloat = function (s) { + var i = parseFloat(s); + return (isNaN(i)) ? 0 : i; + }; + this.formatInt = function (s) { + var i = parseInt(s); + return (isNaN(i)) ? 0 : i; + }; + this.isDigit = function (s, config) { + // replace all an wanted chars and match. + return /^[-+]?\d*$/.test($.trim(s.replace(/[,.']/g, ''))); + }; + this.clearTableBody = function (table) { + if ($.browser.msie) { + function empty() { + while (this.firstChild) + this.removeChild(this.firstChild); + } + empty.apply(table.tBodies[0]); + } else { + table.tBodies[0].innerHTML = ""; + } + }; + } + }); + + // extend plugin scope + $.fn.extend({ + tablesorter: $.tablesorter.construct + }); + + // make shortcut + var ts = $.tablesorter; + + // add default parsers + ts.addParser({ + id: "text", + is: function (s) { + return true; + }, format: function (s) { + return $.trim(s.toLocaleLowerCase()); + }, type: "text" + }); + + ts.addParser({ + id: "digit", + is: function (s, table) { + var c = table.config; + return $.tablesorter.isDigit(s, c); + }, format: function (s) { + return $.tablesorter.formatFloat(s); + }, type: "numeric" + }); + + ts.addParser({ + id: "currency", + is: function (s) { + return /^[£$€?.]/.test(s); + }, format: function (s) { + return $.tablesorter.formatFloat(s.replace(new RegExp(/[£$€]/g), "")); + }, type: "numeric" + }); + + ts.addParser({ + id: "ipAddress", + is: function (s) { + return /^\d{2,3}[\.]\d{2,3}[\.]\d{2,3}[\.]\d{2,3}$/.test(s); + }, format: function (s) { + var a = s.split("."), + r = "", + l = a.length; + for (var i = 0; i < l; i++) { + var item = a[i]; + if (item.length == 2) { + r += "0" + item; + } else { + r += item; + } + } + return $.tablesorter.formatFloat(r); + }, type: "numeric" + }); + + ts.addParser({ + id: "url", + is: function (s) { + return /^(https?|ftp|file):\/\/$/.test(s); + }, format: function (s) { + return jQuery.trim(s.replace(new RegExp(/(https?|ftp|file):\/\//), '')); + }, type: "text" + }); + + ts.addParser({ + id: "isoDate", + is: function (s) { + return /^\d{4}[\/-]\d{1,2}[\/-]\d{1,2}$/.test(s); + }, format: function (s) { + return $.tablesorter.formatFloat((s != "") ? new Date(s.replace( + new RegExp(/-/g), "/")).getTime() : "0"); + }, type: "numeric" + }); + + ts.addParser({ + id: "percent", + is: function (s) { + return /\%$/.test($.trim(s)); + }, format: function (s) { + return $.tablesorter.formatFloat(s.replace(new RegExp(/%/g), "")); + }, type: "numeric" + }); + + ts.addParser({ + id: "usLongDate", + is: function (s) { + return s.match(new RegExp(/^[A-Za-z]{3,10}\.? [0-9]{1,2}, ([0-9]{4}|'?[0-9]{2}) (([0-2]?[0-9]:[0-5][0-9])|([0-1]?[0-9]:[0-5][0-9]\s(AM|PM)))$/)); + }, format: function (s) { + return $.tablesorter.formatFloat(new Date(s).getTime()); + }, type: "numeric" + }); + + ts.addParser({ + id: "shortDate", + is: function (s) { + return /\d{1,2}[\/\-]\d{1,2}[\/\-]\d{2,4}/.test(s); + }, format: function (s, table) { + var c = table.config; + s = s.replace(/\-/g, "/"); + if (c.dateFormat == "us") { + // reformat the string in ISO format + s = s.replace(/(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{4})/, "$3/$1/$2"); + } else if (c.dateFormat == "uk") { + // reformat the string in ISO format + s = s.replace(/(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{4})/, "$3/$2/$1"); + } else if (c.dateFormat == "dd/mm/yy" || c.dateFormat == "dd-mm-yy") { + s = s.replace(/(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{2})/, "$1/$2/$3"); + } + return $.tablesorter.formatFloat(new Date(s).getTime()); + }, type: "numeric" + }); + ts.addParser({ + id: "time", + is: function (s) { + return /^(([0-2]?[0-9]:[0-5][0-9])|([0-1]?[0-9]:[0-5][0-9]\s(am|pm)))$/.test(s); + }, format: function (s) { + return $.tablesorter.formatFloat(new Date("2000/01/01 " + s).getTime()); + }, type: "numeric" + }); + ts.addParser({ + id: "metadata", + is: function (s) { + return false; + }, format: function (s, table, cell) { + var c = table.config, + p = (!c.parserMetadataName) ? 'sortValue' : c.parserMetadataName; + return $(cell).metadata()[p]; + }, type: "numeric" + }); + // add default widgets + ts.addWidget({ + id: "zebra", + format: function (table) { + if (table.config.debug) { + var time = new Date(); + } + var $tr, row = -1, + odd; + // loop through the visible rows + $("tr:visible", table.tBodies[0]).each(function (i) { + $tr = $(this); + // style children rows the same way the parent + // row was styled + if (!$tr.hasClass(table.config.cssChildRow)) row++; + odd = (row % 2 == 0); + $tr.removeClass( + table.config.widgetZebra.css[odd ? 0 : 1]).addClass( + table.config.widgetZebra.css[odd ? 1 : 0]) + }); + if (table.config.debug) { + $.tablesorter.benchmark("Applying Zebra widget", time); + } + } + }); +})(jQuery); diff --git a/usr/share/store-client/main.qml b/usr/share/store-client/main.qml index 9e68dc73..691ac87f 100644 --- a/usr/share/store-client/main.qml +++ b/usr/share/store-client/main.qml @@ -17,8 +17,6 @@ ApplicationWindow { property int appGridIconSize: Screen.sizeCategory > Screen.Medium ? Theme.iconSizeLauncher : Theme.iconSizeMedium property int maxAppGridColumns: 3 - property string _tohId - property var _tohDialog property string _packageName property bool _signInPending @@ -32,9 +30,6 @@ ApplicationWindow { signal uiClosed signal showApp(string packageName) signal openInstalled - signal otherHalfConnected(string id) - signal otherHalfDisconnected - signal otherHalfInstallationFailed(string reason) signal enterStore signal systemDialogCreated signal systemDialogDestroyed @@ -96,13 +91,6 @@ ApplicationWindow { PageStackAction.Immediate) } - function showOtherHalfNotification(id) { - if (!_tohDialog) { - _tohDialog = otherHalfNotification.createObject(win) - _tohDialog.installOtherHalf(id) - } - } - function pushSignIn() { if (pageStack.busy) { // we might notice that the credentials are lost in the middle of push/pop operation @@ -138,35 +126,11 @@ ApplicationWindow { return (idx === -1) ? version : version.substring(0, idx) } - function tohAccountCheck() { - if (jollaStore.accountState === AccountState.NoAccount) { - console.log("TOH couldn't be installed. No account.") - //: The second row for error notification shown when the Other Half installation failed - //: because Jolla account was not defined. - //% "Jolla account required" - otherHalfInstallationFailed(qsTrId("jolla-store-no-toh_failed_no_account")) - return false - } else if (jollaStore.accountState === AccountState.NeedsUpdate) { - console.log("TOH couldn't be installed. Account needs update.") - //: The second row for error notification shown when the Other Half installation failed - //: because Jolla account credentials have expired and need to be updated. - //% "Jolla account needs update" - otherHalfInstallationFailed(qsTrId("jolla-store-no-toh_failed_account_error")) - return false - } else { - return true - } - } - on_StoreAvailableChanged: { if (_storeAvailable) { if (_uiEntryPoint !== "") { showEntryPoint(_uiEntryPoint) } - if (_tohId !== "") { - showOtherHalfNotification(_tohId) - _tohId = "" - } } } @@ -189,45 +153,10 @@ ApplicationWindow { showEntryPoint("myapps") } - onOtherHalfConnected: { - if (tohAccountCheck()) { - tohTimeoutTimer.restart() - if (_storeAvailable) { - showOtherHalfNotification(id) - } else { - _tohId = id - jollaStore.tryConnect() - } - } - } - - onOtherHalfDisconnected: { - _tohId = "" - if (_tohDialog) { - _tohDialog.cancelOtherHalfInstallation() - } - } - onEnterStore: { showEntryPoint("store") } - Timer { - id: tohTimeoutTimer - interval: 60000 // Has to be shorter than INACTIVITY_TIMEOUT, see storeclient.cpp - onTriggered: { - if (_tohId !== "") { - // Clear the TOH id if we didn't manage to handle it by now - _tohId = "" - console.log("TOH couldn't be installed. Connection failed.") - //: The second row for error notification shown when the Other Half installation failed - //: because there was a connection error with Store server. - //% "Connection error" - otherHalfInstallationFailed(qsTrId("jolla-store-no-toh_failed_connection_error")) - } - } - } - AppGridItem { id: gridItemForSize width: Math.floor((_pageWidth -2 * appGridMargin - (appGridColumns - 1) * appGridSpacing) / appGridColumns) @@ -329,13 +258,6 @@ ApplicationWindow { } pushSignIn() } - - onAccountStateChanged: { - if (_tohId !== "" && !tohAccountCheck()) { - _tohId = "" - tohTimeoutTimer.stop() - } - } } Connections { @@ -369,20 +291,4 @@ ApplicationWindow { navigationState.openSearch(true) } } - - // Create OtherHalfNotification system dialog dynamically - // At this point this is the only way to prevent ghost - // covers to appear on home when e.g. closing the app. - // This dialog will be destroyed after it's been dismissed - // or the installation ends. - Component { - id: otherHalfNotification - OtherHalfNotification { - Component.onCompleted: win.systemDialogCreated() - Component.onDestruction: { - win.systemDialogDestroyed() - win._tohDialog = null - } - } - } } diff --git a/usr/share/store-client/pages/AppPage.qml b/usr/share/store-client/pages/AppPage.qml index 38401dbc..0afde69c 100644 --- a/usr/share/store-client/pages/AppPage.qml +++ b/usr/share/store-client/pages/AppPage.qml @@ -38,11 +38,13 @@ Page { contentHeight: column.height PullDownMenu { - visible: (uninstallMenuItem.canShow - || openMenuItem.canShow - || updateMenuItem.canShow - || installMenuItem.canShow) - && appData.packageName !== "" + enabled: uninstallMenuItem.canShow + || openMenuItem.canShow + || updateMenuItem.canShow + || installMenuItem.canShow + visible: appData.packageName !== "" && (enabled || busy) + busy: appData.state == ApplicationState.Installing + || appData.state == ApplicationState.Uninstalling MenuItem { id: uninstallMenuItem diff --git a/usr/share/store-client/pages/OtherHalfNotification.qml b/usr/share/store-client/pages/OtherHalfNotification.qml deleted file mode 100644 index 62b948f8..00000000 --- a/usr/share/store-client/pages/OtherHalfNotification.qml +++ /dev/null @@ -1,268 +0,0 @@ -import QtQuick 2.0 -import Sailfish.Silica 1.0 -import Sailfish.Lipstick 1.0 -import org.pycage.jollastore 1.0 -import Nemo.DBus 2.0 -import Sailfish.Policy 1.0 - -SystemDialog { - id: dialog - - property string _tohId - property string _packageName - property string _packageTitle - //: Store client fetching TOH information from the store - //% "Fetching TOH information" - property string _descriptionText: qsTrId("jolla-store-la-fetching_ambience_information") - - property string _uuid - property string _coverImage - property string _ambienceFilePath: "/usr/share/ambience/" + dialog._packageName + - "/" + dialog._packageName + ".ambience" - property bool _installing - property bool _waitForConnection - property bool _ready - property bool _shown - - // There's no good way to handle dialog dismiss. With monitoring - // 'visible' property + couple of other properties we can decide what's - // the right action to do. - // Cases to handle: - // - User taps area outside of it -> dismiss, destroy - // - User taps install -> dismiss it, but don't destroy - // - TOH is attached and detached -> dismiss, destroy - property bool _finished: !visible && !_installing && _ready && _shown - - function installOtherHalf(id) { - if (id ===_tohId) { - return - } - - _tohId = id - if (_tohId == "") { - console.log("Could not read TOH ID") - destroy(200) - return - } - - if (jollaStore.connectionState === JollaStore.Ready) { - jollaStore.activateTheOtherHalf(_tohId) - } else { - _waitForConnection = true - } - } - - function cancelOtherHalfInstallation() { - // This actually means that other half was removed while this SystemDialog - // is still visible i.e. user has attached and detached the TOH. - dialog.lower() - - // If TOH/ambience package is alrady being installed cancel the installation - // and dissmis the Ambience switcher in home - if (dialog._installing) { - dbusConnector.cancelledAmbience(dialog._ambienceFilePath) - // PackageKit does not support cancelling of ongoing transactions - } - destroy(200) - } - - // Copy-paste code from the ApplicationData - function install() - { - function closure(store, contentModel, appData) - { - return function(success) - { - if (success) { - // Notify switcher about ambience installation - dbusConnector.installedAmbience("/usr/share/ambience/" + appData._packageName, appData._tohId) - store.updateInstalledOn(appData._uuid) - } else { - // TODO: What to really do here. Error might be that there's no updatable candidate - // available or something else. - dbusConnector.cancelledAmbience(appData._ambienceFilePath) - } - // This notifies the dialog that it can be destroyed - appData._installing = false - } - } - - installedModel.addApplication(dialog._uuid) - jollaStore.postStoreInstalled(dialog._uuid) - packageHandler.install(dialog._packageName, - closure(jollaStore, installedModel, dialog)) - } - - function checkPackage() - { - // Get rid of fast double taps, which might cause two or more installations - if (_installing) { - return - } - - dialog._installing = true - dialog.lower() - dbusConnector.installingAmbience(dialog._ambienceFilePath, dialog._packageTitle, dialog._coverImage) - packageHandler.checkInstalled(dialog._packageName) - } - - function refreshApplicationData() - { - var data = jollaStore.applicationData(dialog._uuid) - if (data.hasOwnProperty("uuid")) { - // TODO data.title could be used here too to show the package title, but - // due SystemDialog bug it doesn't get updated. - dialog._descriptionText = data.summary - dialog._packageTitle = data.title - dialog.title = data.title - dialog._coverImage = data.cover !== "" ? data.cover : data.icon - dialog._ready = true - - if (!dialog.visible && !_installing) { - dialog.raise() - dialog.showFullScreen() - } - } - } - - //: Title for The Other Half system dialog - //% "The Other Half detected" - title: qsTrId("jolla-store-he-toh_title") - contentHeight: content.height - - onVisibleChanged: { - if (visible) { - mceDbus.displayOn() - } - } - - onActiveChanged: { - if (active) { - _shown = true - } - } - - on_FinishedChanged: { - if (_finished) { - destroy(200) - } - } - - PolicyValue { - id: policy - policyType: PolicyValue.ApplicationInstallationEnabled - } - - Column { - id: content - width: parent.width - - SystemDialogHeader { - id: header - - title: dialog.title - description: dialog._descriptionText - } - - SystemDialogIconButton { - width: header.width / 2 - anchors.horizontalCenter: parent.horizontalCenter - //: Install button in The Other Half system dialog - //% "Install" - text: qsTrId("jolla-store-bt-toh_install") - iconSource: (Screen.sizeCategory >= Screen.Large) ? "image://theme/icon-l-add" - : "image://theme/icon-m-add" - enabled: dialog._ready - onClicked: dialog.checkPackage() - visible: policy.value - } - Label { - height: implicitHeight + 2*Theme.paddingMedium - visible: !policy.value - x: Theme.horizontalPageMargin - width: parent.width - 2*Theme.horizontalPageMargin - font.pixelSize: Theme.fontSizeLarge - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - wrapMode: Text.Wrap - color: Theme.highlightColor - //% "Other Half installation prevented by Sailfish Device Manager" - text: qsTrId("jolla-store-other_half_installation_not_allowed") - } - } - - // Used for waking up the display - DBusInterface { - id: mceDbus - - function displayOn() - { - mceDbus.call("req_display_state_on", undefined) - } - - service: "com.nokia.mce" - path: "/com/nokia/mce/request" - iface: "com.nokia.mce.request" - bus: DBus.SystemBus - } - - Connections { - target: jollaStore - - onConnectionStateChanged: { - if (dialog._waitForConnection && jollaStore.connectionState === JollaStore.Ready) { - dialog._waitForConnection = false - jollaStore.activateTheOtherHalf(dialog._tohId) - } - } - - onTheOtherHalfActivationReceived: { - if (data.toh_id === dialog._tohId) { - dialog._uuid = data.package_uuid - dialog._packageName = data.package_name - refreshApplicationData() - } else { - dialog.cancelOtherHalfInstallation() - } - } - - onApplicationReceived: { - if (uuid === dialog._uuid) { - refreshApplicationData() - } - } - - onError: { - console.log("TOH Failure: ", details, "\n") - dialog.cancelOtherHalfInstallation() - } - } - - Connections { - target: packageHandler - - onCheckInstalledResult: { - if (dialog._packageName === packageName) { - if (isInstalled) { - // Package is already installed. Give some time for lipstick to - // setup everything properly - delayedInstallTimer.start() - } else { - // Install package because it's not installed yet. - dialog.install() - } - } - } - } - - // Delay animations for re-activating already installed ambience on purpose. - // Making animations to happen too fast, makes everything to look too busy. - Timer { - id: delayedInstallTimer - interval: 4500 - onTriggered: { - dbusConnector.installedAmbience("/usr/share/ambience/" + dialog._packageName, dialog._tohId) - dialog._installing = false - } - } -} diff --git a/usr/share/store-client/pages/ReviewItem.qml b/usr/share/store-client/pages/ReviewItem.qml index 0f0e33d2..6901c3db 100644 --- a/usr/share/store-client/pages/ReviewItem.qml +++ b/usr/share/store-client/pages/ReviewItem.qml @@ -97,7 +97,7 @@ ListItem { text: { var txt = authorName + " • " + reviewItem.version + " • " var timestamp = Format.formatDate(reviewItem.updatedOn, - Formatter.DurationElapsed) + Formatter.TimeElapsed) if (reviewItem.updatedOn > reviewItem.createdOn) { //: Timestamp label for edited comments. Takes the timestamp as a //: parameter (in format 'N minutes/hours/days ago'). diff --git a/usr/share/store-client/pages/SignInPage.qml b/usr/share/store-client/pages/SignInPage.qml index b7eca645..6dc8b4f7 100644 --- a/usr/share/store-client/pages/SignInPage.qml +++ b/usr/share/store-client/pages/SignInPage.qml @@ -77,25 +77,16 @@ Page { if (jollaStore.connectionState === JollaStore.Unauthorized) { switch (jollaStore.accountState) { case AccountState.NeedsUpdate: - //: View placeholder text shown when there's some problem with the account - //: that requires the user to go to settings and sign in again. - //% "Jolla account needs to be updated" - return qsTrId("jolla-store-li-account_needs_update") + return jollaStore.accountNeedsUpdateMessage() case AccountState.NoAccount: - //: View placeholder when there's no Jolla account created or the user has not yet signed in - //% "Jolla account needed" - return qsTrId("jolla-store-li-account_needed") + return jollaStore.accountNeededMessage(); case AccountState.NetworkError: - //: View placeholder when there's a network error during signing in - //% "Network connection failure. Try again later." - return qsTrId("jolla-store-li-account_network_error") + return jollaStore.accountNetworkErrorMessage() default: return "" } } else { - //: View placeholder when being offline - //% "Sorry, cannot connect to store right now. Please try again later." - return qsTrId("jolla-store-li-being_offline") + return jollaStore.beingOfflineMessage() } } hintText: { diff --git a/usr/share/voicecall-ui-jolla/VoiceCallManager.qml b/usr/share/voicecall-ui-jolla/AppVoiceCallManager.qml similarity index 95% rename from usr/share/voicecall-ui-jolla/VoiceCallManager.qml rename to usr/share/voicecall-ui-jolla/AppVoiceCallManager.qml index 7dd0d2a0..9ed32bda 100644 --- a/usr/share/voicecall-ui-jolla/VoiceCallManager.qml +++ b/usr/share/voicecall-ui-jolla/AppVoiceCallManager.qml @@ -11,13 +11,13 @@ import Sailfish.Silica 1.0 import Sailfish.Telephony 1.0 import Sailfish.AccessControl 1.0 import org.nemomobile.voicecall 1.0 -import MeeGo.QOfono 0.2 -import org.nemomobile.notifications 1.0 +import QOfono 0.2 +import Nemo.Notifications 1.0 import org.nemomobile.contacts 1.0 import org.nemomobile.ofono 1.0 import org.nemomobile.systemsettings 1.0 -import MeeGo.Connman 0.2 -import org.nemomobile.dbus 2.0 +import Connman 0.2 +import Nemo.DBus 2.0 import Nemo.Configuration 1.0 import "common/CallHistory.js" as CallHistory @@ -148,7 +148,7 @@ Item { } function promptForSim(number) { - return Telephony.promptForVoiceSim && !isEmergencyNumber(number) + return Telephony.promptForVoiceSim && (number === undefined || !isEmergencyNumber(number)) } // TODO: refactor DTMF handling in some centeralized place, add tests. @@ -400,6 +400,19 @@ Item { lastCaller = call.lineId lastModem = modemPath newIncomingCall = call + var caller = callerDetails[call.handlerId] + if (!doNotDisturb.value + || doNotDisturbRingtone.value == "on" + || (doNotDisturbRingtone.value == "contacts" && caller.person && caller.person.id != 0) + || (doNotDisturbRingtone.value == "favorites" && caller.person && caller.person.favorite)) { + if (simManager.indexOfModem(modemPath) === 1) { + if (profileControl.ringerTone2Enabled) { + telephony.playRingtone(profileControl.ringerTone2File) + } + } else { + telephony.playRingtone() + } + } } else if (callerDetails[call.handlerId].silenced) { newSilencedCall = call } @@ -663,6 +676,10 @@ Item { simDescriptionSeparator: " " + String.fromCharCode(0x2022) + " " } + ProfileControl { + id: profileControl + } + DBusInterface { id: pinQuery service: "com.jolla.PinQuery" @@ -769,6 +786,19 @@ Item { } } + ConfigurationValue { + id: doNotDisturb + defaultValue: false + key: "/lipstick/do_not_disturb" + } + + ConfigurationValue { + id: doNotDisturbRingtone + + defaultValue: "on" + key: "/lipstick/do_not_disturb_ringtone" + } + AboutSettings { id: aboutSettings } diff --git a/usr/share/voicecall-ui-jolla/calling/CellularErrorDialog.qml b/usr/share/voicecall-ui-jolla/calling/CellularErrorDialog.qml index 8c22c955..f4c02119 100644 --- a/usr/share/voicecall-ui-jolla/calling/CellularErrorDialog.qml +++ b/usr/share/voicecall-ui-jolla/calling/CellularErrorDialog.qml @@ -9,8 +9,8 @@ import QtQuick 2.1 import QtQuick.Window 2.1 import Sailfish.Silica 1.0 import Sailfish.Lipstick 1.0 -import org.nemomobile.dbus 2.0 -import org.nemomobile.notifications 1.0 +import Nemo.DBus 2.0 +import Nemo.Notifications 1.0 SystemDialog { id: root diff --git a/usr/share/voicecall-ui-jolla/calling/InCallKeypad.qml b/usr/share/voicecall-ui-jolla/calling/InCallKeypad.qml index b50cab21..0d2be4ca 100644 --- a/usr/share/voicecall-ui-jolla/calling/InCallKeypad.qml +++ b/usr/share/voicecall-ui-jolla/calling/InCallKeypad.qml @@ -1,6 +1,6 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 -import MeeGo.QOfono 0.2 +import QOfono 0.2 DockedPanel { id: root diff --git a/usr/share/voicecall-ui-jolla/calling/InCallView.qml b/usr/share/voicecall-ui-jolla/calling/InCallView.qml index 0550788a..e151f637 100644 --- a/usr/share/voicecall-ui-jolla/calling/InCallView.qml +++ b/usr/share/voicecall-ui-jolla/calling/InCallView.qml @@ -11,7 +11,7 @@ import Sailfish.Bluetooth 1.0 import Sailfish.Policy 1.0 import Sailfish.Telephony 1.0 import Nemo.Configuration 1.0 -import org.nemomobile.notifications 1.0 +import Nemo.Notifications 1.0 import org.nemomobile.systemsettings 1.0 import org.nemomobile.voicecall 1.0 as VoiceCall import "../common/CallHistory.js" as CallHistory @@ -38,16 +38,6 @@ SilicaFlickable { onIsMicrophoneMutedChanged: microphoneSwitch.checked = isMicrophoneMuted onCallStateChanged: { - if (bluetoothAudio == null) { - // Create this object on demand to reduce system load from responding to detection of - // nearby Bluetooth devices when there are no on-going calls. - bluetoothAudio = bluetoothAudioComponent.createObject(root) - } else if (bluetoothAudio.available) { - if (callState == 'dialing' || callState == 'incoming') { - bluetoothAudio.reset() - } - } - if (callState == 'null' || callState == 'disconnected') { if (inCallKeypad) { inCallKeypad.fade() @@ -60,6 +50,16 @@ SilicaFlickable { // Dismiss the notice about premium or toll-free number Notices._dismissCurrent() + } else { + if (bluetoothAudio == null) { + // Create this object on demand to reduce system load from responding to detection of + // nearby Bluetooth devices when there are no on-going calls. + bluetoothAudio = bluetoothAudioComponent.createObject(root) + } else if (bluetoothAudio.available) { + if (callState == 'dialing' || callState == 'incoming') { + bluetoothAudio.reset() + } + } } // Show a notice when calling a premium-rate or toll-free phone number @@ -180,7 +180,8 @@ SilicaFlickable { } MenuItem { // GSM 02.84 states that the maximum number of remote parties is 5 - visible: main.state === "active" && telephony.heldCall && (!telephony.conferenceCall || telephony.conferenceCall.childCalls.count < 5) + visible: main.state === "active" && telephony.heldCall + && (!telephony.conferenceCall || telephony.conferenceCall.childCalls.count < 5) //% "Merge calls" text: qsTrId("voicecall-me-merge_calls") onDelayedClick: { @@ -198,7 +199,8 @@ SilicaFlickable { Repeater { model: telephony.voiceCalls delegate: MenuItem { - visible: (callCount == 1 || (statusText === "held" && !telephony.silencedCall) || isSilenced) && parentCall == null && telephony.voiceCalls.count >= 1 + visible: (callCount == 1 || (statusText === "held" && !telephony.silencedCall) || isSilenced) + && parentCall == null && telephony.voiceCalls.count >= 1 onVisibleChanged: updateAction() property int callCount: telephony.effectiveCallCount onCallCountChanged: updateAction() @@ -206,7 +208,7 @@ SilicaFlickable { onCallStatusChanged: updateAction() property string mainState: main.state onMainStateChanged: updateAction() - property bool isSilenced: telephony.silencedCall ? telephony.silencedCall.handlerId == instance.handlerId : false + property bool isSilenced: telephony.silencedCall && telephony.silencedCall.handlerId == instance.handlerId onDelayedClick: { if (isSilenced) { @@ -302,7 +304,10 @@ SilicaFlickable { } HighlightImage { - opacity: telephony.primaryCall && telephony.primaryCall.isForwarded && !dtmfLabelTimer.running && (main.state === "active" || main.state === "calling" || main.state === "incoming") ? 1.0 : 0.0 + opacity: telephony.primaryCall && telephony.primaryCall.isForwarded + && !dtmfLabelTimer.running + && (main.state === "active" || main.state === "calling" || main.state === "incoming") + ? 1.0 : 0.0 Behavior on opacity { FadeAnimation {} } x: (parent.width - stateLabel.contentWidth)/2 - width - Theme.paddingMedium anchors.verticalCenter: stateLabel.verticalCenter @@ -427,7 +432,10 @@ SilicaFlickable { Row { id: buttonRow - property bool callButtonsEnabled: main.state !== 'null' && main.state !== "silenced" && main.state !== "disconnected" && main.state !== "incoming" + property bool callButtonsEnabled: main.state !== 'null' + && main.state !== "silenced" + && main.state !== "disconnected" + && main.state !== "incoming" spacing: Theme.paddingSmall enabled: callButtonsEnabled opacity: callButtonsEnabled ? 1.0 : 0.0 @@ -441,7 +449,8 @@ SilicaFlickable { Switch { id: bluetoothAudioSwitch visible: bluetoothAudioAvailable && bluetoothAudio.supportsCallAudio - icon.source: bluetoothAudioAvailable && bluetoothAudio.jollaIcon != "" ? "image://theme/" + bluetoothAudio.jollaIcon : "" + icon.source: bluetoothAudioAvailable && bluetoothAudio.iconName != "" + ? "image://theme/" + bluetoothAudio.iconName : "" automaticCheck: false checked: !speakerSwitch.checked && bluetoothAudioAvailable && bluetoothAudio.callAudioEnabled diff --git a/usr/share/voicecall-ui-jolla/calling/IncomingCallView.qml b/usr/share/voicecall-ui-jolla/calling/IncomingCallView.qml index 7c902fa5..baef1544 100644 --- a/usr/share/voicecall-ui-jolla/calling/IncomingCallView.qml +++ b/usr/share/voicecall-ui-jolla/calling/IncomingCallView.qml @@ -7,9 +7,9 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 -import org.nemomobile.policy 1.0 +import Nemo.Policy 1.0 import org.nemomobile.contacts 1.0 -import org.nemomobile.dbus 2.0 as NemoDBus +import Nemo.DBus 2.0 as NemoDBus IncomingCallViewBase { id: incomingCallView diff --git a/usr/share/voicecall-ui-jolla/common/CommHistoryService.qml b/usr/share/voicecall-ui-jolla/common/CommHistoryService.qml index b540aa74..6639276d 100644 --- a/usr/share/voicecall-ui-jolla/common/CommHistoryService.qml +++ b/usr/share/voicecall-ui-jolla/common/CommHistoryService.qml @@ -1,5 +1,5 @@ import QtQuick 2.0 -import org.nemomobile.dbus 2.0 +import Nemo.DBus 2.0 DBusInterface { id: service diff --git a/usr/share/voicecall-ui-jolla/common/MessagesInterface.qml b/usr/share/voicecall-ui-jolla/common/MessagesInterface.qml index 59610d27..9c683916 100644 --- a/usr/share/voicecall-ui-jolla/common/MessagesInterface.qml +++ b/usr/share/voicecall-ui-jolla/common/MessagesInterface.qml @@ -1,4 +1,4 @@ -import org.nemomobile.dbus 2.0 +import Nemo.DBus 2.0 DBusInterface { service: "org.sailfishos.Messages" diff --git a/usr/share/voicecall-ui-jolla/common/ReminderContextMenu.qml b/usr/share/voicecall-ui-jolla/common/ReminderContextMenu.qml index 4f3ae4fb..b0f4987d 100644 --- a/usr/share/voicecall-ui-jolla/common/ReminderContextMenu.qml +++ b/usr/share/voicecall-ui-jolla/common/ReminderContextMenu.qml @@ -19,7 +19,7 @@ ContextMenu { // formatDate() is relative to the current time, so the time is padded by a half a second // to allow for the time between Date.now() and formatDate() executing. text: Format.formatDate( - new Date(Date.now() + (modelData * 60 * 1000) + 500), Format.DurationElapsed) + new Date(Date.now() + (modelData * 60 * 1000) + 500), Format.TimeElapsed) onClicked: { var name = reminderMenu.person ? reminderMenu.person.displayLabel diff --git a/usr/share/voicecall-ui-jolla/main.qml b/usr/share/voicecall-ui-jolla/main.qml index 2d424a7a..1ad492db 100644 --- a/usr/share/voicecall-ui-jolla/main.qml +++ b/usr/share/voicecall-ui-jolla/main.qml @@ -13,9 +13,9 @@ import Sailfish.Telephony 1.0 import Sailfish.Lipstick 1.0 import Sailfish.Policy 1.0 import Sailfish.Contacts 1.0 -import org.nemomobile.dbus 2.0 +import Nemo.DBus 2.0 import org.nemomobile.contacts 1.0 -import org.nemomobile.notifications 1.0 +import Nemo.Notifications 1.0 import org.nemomobile.voicecall 1.0 as VoiceCall import com.jolla.voicecall 1.0 import "common/CallHistory.js" as CallHistory @@ -345,7 +345,7 @@ ApplicationWindow { MessagesInterface { id: messaging } - VoiceCallManager { + AppVoiceCallManager { id: telephony onUnrecoverableCallError: showCellularErrorDialog() diff --git a/usr/share/voicecall-ui-jolla/ota/OtaNotification.qml b/usr/share/voicecall-ui-jolla/ota/OtaNotification.qml index ea8f9171..743a2813 100644 --- a/usr/share/voicecall-ui-jolla/ota/OtaNotification.qml +++ b/usr/share/voicecall-ui-jolla/ota/OtaNotification.qml @@ -6,8 +6,8 @@ */ import QtQuick 2.0 -import org.nemomobile.notifications 1.0 -import org.nemomobile.dbus 2.0 +import Nemo.Notifications 1.0 +import Nemo.DBus 2.0 Notification { id: otaNotification diff --git a/usr/share/voicecall-ui-jolla/pages/DialerView.qml b/usr/share/voicecall-ui-jolla/pages/DialerView.qml index d6eebe2b..1888fb1c 100644 --- a/usr/share/voicecall-ui-jolla/pages/DialerView.qml +++ b/usr/share/voicecall-ui-jolla/pages/DialerView.qml @@ -9,8 +9,8 @@ import QtQuick 2.6 import Sailfish.Silica 1.0 import Sailfish.Telephony 1.0 import Nemo.Configuration 1.0 -import MeeGo.QOfono 0.2 -import org.nemomobile.dbus 2.0 +import QOfono 0.2 +import Nemo.DBus 2.0 import org.nemomobile.contacts 1.0 import "../common" import "dialer" diff --git a/usr/share/voicecall-ui-jolla/pages/PeopleView.qml b/usr/share/voicecall-ui-jolla/pages/PeopleView.qml index 8d5aa799..0b60d508 100644 --- a/usr/share/voicecall-ui-jolla/pages/PeopleView.qml +++ b/usr/share/voicecall-ui-jolla/pages/PeopleView.qml @@ -47,7 +47,7 @@ ContactBrowser { } if (_contactActionType === Telephony.Call - && Telephony.promptForVoiceSim + && telephony.promptForSim() && propertyData.propertyType === "phoneNumber") { propertyPicker.closeOnSelection = false diff --git a/usr/share/voicecall-ui-jolla/pages/callhistory/BasicHistoryItem.qml b/usr/share/voicecall-ui-jolla/pages/callhistory/BasicHistoryItem.qml index ffa06077..df41e228 100644 --- a/usr/share/voicecall-ui-jolla/pages/callhistory/BasicHistoryItem.qml +++ b/usr/share/voicecall-ui-jolla/pages/callhistory/BasicHistoryItem.qml @@ -29,7 +29,7 @@ Item { TimeStampLabel { id: timeStampLabel anchors.verticalCenter: parent.verticalCenter - formatType: time.getFullYear() !== main.today.getFullYear() ? Format.DateMedium : Formatter.TimepointRelativeCurrentDay + formatType: Formatter.TimepointRelative } } } diff --git a/usr/share/voicecall-ui-jolla/pages/dialer/CellBroadcast.qml b/usr/share/voicecall-ui-jolla/pages/dialer/CellBroadcast.qml index 48205c99..12b4f574 100644 --- a/usr/share/voicecall-ui-jolla/pages/dialer/CellBroadcast.qml +++ b/usr/share/voicecall-ui-jolla/pages/dialer/CellBroadcast.qml @@ -1,5 +1,5 @@ import QtQuick 2.0 -import MeeGo.QOfono 0.2 +import QOfono 0.2 OfonoCellBroadcast { id: cellBroadcast diff --git a/usr/share/voicecall-ui-jolla/pages/dialer/NumberField.qml b/usr/share/voicecall-ui-jolla/pages/dialer/NumberField.qml index 99228903..52285ad5 100644 --- a/usr/share/voicecall-ui-jolla/pages/dialer/NumberField.qml +++ b/usr/share/voicecall-ui-jolla/pages/dialer/NumberField.qml @@ -2,7 +2,7 @@ import QtQuick 2.6 import Sailfish.Silica 1.0 import Sailfish.Contacts 1.0 import Sailfish.Telephony 1.0 -import org.nemomobile.time 1.0 +import Nemo.Time 1.0 import org.nemomobile.contacts 1.0 MouseArea { @@ -30,7 +30,7 @@ MouseArea { // When pressing backspace or delete, make sure to always delete a dialable digit if (offset !== 0 && (selectionStart + leftEndOffset) != (selectionEnd + rightStartOffset)) { var erased = oldtext.substr(selectionStart + leftEndOffset, selectionEnd + rightStartOffset - selectionStart - leftEndOffset) - if (!(/[0-9\+\#\*]/.test(erased))) { + if (!(/[0-9\+\#\*,]/.test(erased))) { if (backspace) { leftEndOffset-- } else if (del) { diff --git a/usr/share/voicecall-ui-jolla/pages/dialer/SimCodes.qml b/usr/share/voicecall-ui-jolla/pages/dialer/SimCodes.qml index d78aa7b7..43fbaae6 100644 --- a/usr/share/voicecall-ui-jolla/pages/dialer/SimCodes.qml +++ b/usr/share/voicecall-ui-jolla/pages/dialer/SimCodes.qml @@ -1,6 +1,6 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 -import MeeGo.QOfono 0.2 +import QOfono 0.2 Item { id: root diff --git a/usr/share/voicecall-ui-jolla/pages/dialer/SupplementaryServices.qml b/usr/share/voicecall-ui-jolla/pages/dialer/SupplementaryServices.qml index 7c7479d4..20e38a37 100644 --- a/usr/share/voicecall-ui-jolla/pages/dialer/SupplementaryServices.qml +++ b/usr/share/voicecall-ui-jolla/pages/dialer/SupplementaryServices.qml @@ -1,6 +1,6 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 -import MeeGo.QOfono 0.2 +import QOfono 0.2 Item { id: root