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 += '
';
+ $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(;ca",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="",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>$2>");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>$2>");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=/