├── .github ├── ISSUE_TEMPLATE │ ├── bug-template.md │ ├── help-template.md │ └── suggestion-template.md └── workflows │ ├── build-devel.yml │ ├── build-sfos3.1.yml │ ├── build-sfos3.3.yml │ ├── build-sfos4.2.yml │ ├── build-sfos4.2_sdk-4.3.yml │ └── build-sfos4.2_sdk-latest.yml ├── .gitignore ├── .tx ├── config └── config.yml ├── .xdata ├── screenshots │ ├── screenshot-screenshot-storeman-01.png │ ├── screenshot-screenshot-storeman-02.png │ ├── screenshot-screenshot-storeman-03.png │ ├── screenshot-screenshot-storeman-04.png │ ├── screenshot-screenshot-storeman-06.png │ ├── screenshot-screenshot-storeman-07.png │ ├── screenshot-screenshot-storeman-08.png │ └── screenshot-screenshot-storeman-09.png └── social-media-icons │ ├── harbour-storeman_1280x640.png │ ├── harbour-storeman_1500x500.png │ ├── harbour-storeman_792x792.png │ └── harbour-storeman_960x640.png ├── LICENSE ├── README.md ├── data ├── harbour-storeman └── harbour.storeman.service ├── harbour-storeman.desktop ├── harbour-storeman.pro ├── icons ├── 108x108 │ └── harbour-storeman.png ├── 128x128 │ └── harbour-storeman.png ├── 172x172 │ └── harbour-storeman.png ├── 256x256 │ └── harbour-storeman.png ├── 480x480 │ └── harbour-storeman.png ├── 560x560 │ └── harbour-storeman.png ├── 86x86 │ └── harbour-storeman.png └── harbour-storeman.svg ├── qml ├── StoremanStyles.qml ├── components │ ├── AppInfoLabel.qml │ ├── AppInformation.qml │ ├── AppListDelegate.qml │ ├── AppPageMenu.qml │ ├── BackupLabel.qml │ ├── BackupOptions.qml │ ├── BookmarkButton.qml │ ├── CategoriesFilterDelegate.qml │ ├── CommentDelegate.qml │ ├── CommentField.qml │ ├── CommentLabel.qml │ ├── DisappearAnimation.qml │ ├── FancyPageHeader.qml │ ├── HtmlTagButton.qml │ ├── IconLabel.qml │ ├── IntervalView.qml │ ├── ListMenuItem.qml │ ├── ListViewPositionAnimation.qml │ ├── MainPageAppGridDelegate.qml │ ├── MainPageButton.qml │ ├── MenuSearchItem.qml │ ├── MenuStatusLabel.qml │ ├── MoreButton.qml │ ├── PackageInformation.qml │ ├── ParticipantsDelegate.qml │ ├── RatingBox.qml │ ├── RefreshMenuItem.qml │ ├── ScreenshotsBox.qml │ ├── StoremanHintLabel.qml │ ├── StoremanTapHint.qml │ ├── StoremanTouchInteractionHint.qml │ └── TagDelegate.qml ├── cover │ ├── CoverPage.qml │ └── background.svg ├── harbour-storeman.qml ├── models │ ├── DevelopersModel.qml │ ├── DummyCommentsModel.qml │ └── TranslatorsModel.qml ├── pages │ ├── AboutPage.qml │ ├── AppPage.qml │ ├── AuthorisationDialog.qml │ ├── BackupDialog.qml │ ├── BackupsPage.qml │ ├── BookmarksPage.qml │ ├── CategoriesFilterPage.qml │ ├── CategoriesPage.qml │ ├── CategoryPage.qml │ ├── ChangelogPage.qml │ ├── CommentsPage.qml │ ├── DevelopmentPage.qml │ ├── ErrorPage.qml │ ├── InstalledAppsPage.qml │ ├── IntervalPickerDialog.qml │ ├── LocalRpmsPage.qml │ ├── MainPage.qml │ ├── MainPageOrderDialog.qml │ ├── RecentAppsPage.qml │ ├── RepositoriesPage.qml │ ├── RepositoryPage.qml │ ├── RestoreDialog.qml │ ├── ScreenshotPage.qml │ ├── SearchPage.qml │ ├── SettingsPage.qml │ ├── SharePage.qml │ ├── TagAppsPage.qml │ ├── TagsPage.qml │ ├── TranslationsPage.qml │ ├── UnusedReposDialog.qml │ └── VotingPage.qml └── qmldir ├── rpm ├── harbour-storeman.changes ├── harbour-storeman.rpmlintrc └── harbour-storeman.spec ├── scripts ├── update_categories.py └── update_translators.py ├── src ├── harbour-storeman.cpp ├── networkaccessmanagerfactory.cpp ├── networkaccessmanagerfactory.h ├── ornabstractlistmodel.h ├── ornapplication.cpp ├── ornapplication.h ├── ornapplistitem.cpp ├── ornapplistitem.h ├── ornappsmodel.cpp ├── ornappsmodel.h ├── ornbackup.cpp ├── ornbackup.h ├── ornbookmarksmodel.cpp ├── ornbookmarksmodel.h ├── orncategoriesmodel.cpp ├── orncategoriesmodel.h ├── orncategorylistitem.cpp ├── orncategorylistitem.h ├── ornclient.cpp ├── ornclient.h ├── ornclient_p.h ├── orncommentlistitem.cpp ├── orncommentlistitem.h ├── orncommentsmodel.cpp ├── orncommentsmodel.h ├── ornconst.cpp ├── ornconst.h ├── orninstalledappsmodel.cpp ├── orninstalledappsmodel.h ├── orninstalledpackage.h ├── ornpackageversion.cpp ├── ornpackageversion.h ├── ornpkdaemon.cpp ├── ornpkdaemon.h ├── ornpktransaction.cpp ├── ornpktransaction.h ├── ornpm.cpp ├── ornpm.h ├── ornpm_p.h ├── ornproxymodel.cpp ├── ornproxymodel.h ├── ornrepo.cpp ├── ornrepo.h ├── ornrepomodel.cpp ├── ornrepomodel.h ├── ornsearchappsmodel.cpp ├── ornsearchappsmodel.h ├── ornsecrets.cpp ├── ornsecrets.h ├── ornsecrets_p.h ├── ornssu.cpp ├── ornssu.h ├── orntaglistitem.cpp ├── orntaglistitem.h ├── orntagsmodel.cpp ├── orntagsmodel.h ├── ornutils.cpp ├── ornutils.h ├── storeman.cpp ├── storeman.h └── storeman_p.h └── translations ├── README.md ├── harbour-storeman-cs.ts ├── harbour-storeman-da.ts ├── harbour-storeman-de.ts ├── harbour-storeman-el.ts ├── harbour-storeman-es.ts ├── harbour-storeman-et.ts ├── harbour-storeman-fi.ts ├── harbour-storeman-fr.ts ├── harbour-storeman-hu.ts ├── harbour-storeman-it.ts ├── harbour-storeman-nl.ts ├── harbour-storeman-nl_BE.ts ├── harbour-storeman-no.ts ├── harbour-storeman-pl.ts ├── harbour-storeman-pt.ts ├── harbour-storeman-ru.ts ├── harbour-storeman-sk.ts ├── harbour-storeman-sl.ts ├── harbour-storeman-sv.ts ├── harbour-storeman-tt.ts ├── harbour-storeman-zh.ts ├── harbour-storeman.ts └── translations.pri /.github/ISSUE_TEMPLATE/bug-template.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Report a bug for Storeman 4 | title: "[Bug] " 5 | labels: 'bug' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **SailfishOS VERSION** (Settings → About product → Build): 11 |
**HARDWARE** (Settings → About product → Manufacturer & Product name): 12 |
**Storeman VERSION** (Storeman → \ → About Storeman): 13 |
14 | 15 | #### BUG DESCRIPTION 16 | 17 | 18 | #### STEPS TO REPRODUCE 19 | 20 | 21 | #### ADDITIONAL INFORMATION 22 | 23 | \[Please consider which other pieces of information may be relevant: Denote if this is not always reproducible, if this is a regression (then name to which older version), attach relevant data such as log files or the systemd journal, provide screenshots etc.\] 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/help-template.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Help form 3 | about: Questions about Storeman 4 | title: "[Help] " 5 | labels: 'question' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **SailfishOS VERSION** (Settings → About product → Build): 11 |
**HARDWARE** (Settings → About product → Manufacturer & Product name): 12 |
**Storeman VERSION** (Storeman → \ → About Storeman): 13 |
14 | 15 | #### QUESTION 16 | 17 | 18 | #### STEPS TO REPRODUCE 19 | 20 | 21 | #### ADDITIONAL INFORMATION 22 | 23 | \[Please consider which other pieces of information may be relevant: Denote if this is not always reproducible, if this is a regression (then name to which older version), attach relevant data such as log files or the systemd journal, provide screenshots etc.\] 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/suggestion-template.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for Storeman 4 | title: "[Suggestion] " 5 | labels: 'feature request' 6 | assignees: '' 7 | 8 | --- 9 | 10 | #### DESCRIPTION 11 | 12 | 13 | #### ADDITIONAL INFORMATION 14 | 15 | \[Please consider which other pieces of information may be relevant: Attach relevant data, provide screenshots etc.\] 16 | -------------------------------------------------------------------------------- /.github/workflows/build-devel.yml: -------------------------------------------------------------------------------- 1 | name: CI - devel branch on SDK for 3.3.0 (i486) 2 | 3 | env: 4 | RELEASE: 3.3.0.14 5 | 6 | on: 7 | pull_request: 8 | branches: 9 | - devel 10 | # Allows to run this workflow manually from the Actions tab. 11 | workflow_dispatch: 12 | 13 | defaults: 14 | run: 15 | # Note that 'bash' provides -o pipefail, in contrast to the default (i.e., unspecified, which also uses bash) or 'sh', 16 | # see https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsshell 17 | shell: sh 18 | 19 | # Do not use concurrency in order to enforce checking every commit of a Pull Request. 20 | # See, e.g.: https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#example-only-cancel-in-progress-jobs-or-runs-for-the-current-workflow 21 | #concurrency: 22 | #group: ci-${{ github.ref_name }} 23 | # 'false' (default) allows for two concurrent runs, one executing and one freshly enqueued; 'true' for only one; no 'concurrency:' defined for multiple. 24 | #cancel-in-progress: false 25 | 26 | jobs: 27 | build: 28 | runs-on: ubuntu-24.04 29 | env: 30 | # Do not wait up to the default of 10 minutes for network timeouts in a workflow which runs ca. 3 minutes. 31 | SEGMENT_DOWNLOAD_TIMEOUT_MINS: 1 32 | steps: 33 | 34 | - name: Checkout 35 | uses: actions/checkout@v4 36 | 37 | # Caching Docker images is not ready yet, see 38 | # https://github.com/storeman-developers/harbour-storeman-installer/blob/devel/cache-docker-images_github-ci.md 39 | #- name: Cache Docker images of the Sailfish-SDK 40 | # id: cache-sdk 41 | # uses: actions/cache@v3 42 | # with: 43 | # path: $GITHUB_WORKSPACE/… 44 | # key: cache 45 | 46 | - name: Prepare 47 | run: mkdir RPMS 48 | 49 | - name: Build i486 50 | uses: coderus/github-sfos-build@old-stable 51 | with: 52 | # Solely builds for i486 on 3.3.0, because of https://github.com/sailfishos-patches/patchmanager/pull/437#issuecomment-1615317003 53 | release: ${{ env.RELEASE }} 54 | arch: i486 55 | 56 | - name: Upload build result 57 | uses: actions/upload-artifact@v4 58 | with: 59 | name: RPM-build-results_devel-i486-SFOS3.3.0+ 60 | path: RPMS/ 61 | 62 | # Just for fun, see https://feathericons.com/ and 63 | # https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions#branding 64 | #branding: # "Invalid workflow file: Unexpected value 'branding'"; maybe action MUST be located in '/' or the name MUST be action.yml or both, see e.g., https://github.com/actions/cache/blob/main/action.yml#L37 65 | # icon: 'gift' 66 | # color: 'purple' 67 | 68 | -------------------------------------------------------------------------------- /.github/workflows/build-sfos3.1.yml: -------------------------------------------------------------------------------- 1 | name: CI - sfos3.1 branch on SDK for 3.1.0 (armv7hl,i486) 2 | 3 | env: 4 | RELEASE: 3.1.0.12 5 | 6 | on: 7 | push: 8 | tags: 9 | - 'release*_sfos3.1/*' 10 | - 'rc*_sfos3.1/*' 11 | - 'beta*_sfos3.1/*' 12 | - 'alpha*_sfos3.1/*' 13 | - 'sfos3.1/*' 14 | pull_request: 15 | branches: 16 | - sfos3.1 17 | # Allows to run this workflow manually from the Actions tab. 18 | #workflow_dispatch: 19 | # Rather set a new release in the spec file and a new tag in the format N/X.Y.Z (e.g., release2_sfos3.1/0.6.3) to build a release version again. 20 | 21 | defaults: 22 | run: 23 | # Note that 'bash' provides -o pipefail, in contrast to the default (i.e., unspecified, which also uses bash) or 'sh', 24 | # see https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsshell 25 | shell: sh 26 | 27 | # See, e.g.: https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#example-only-cancel-in-progress-jobs-or-runs-for-the-current-workflow 28 | concurrency: 29 | group: ci-${{ github.ref_name }} 30 | # 'false' (default) allows for two concurrent runs, one executing and one freshly enqueued; 'true' for only one; no 'concurrency:' defined for multiple. 31 | cancel-in-progress: false 32 | 33 | jobs: 34 | build: 35 | runs-on: ubuntu-24.04 36 | env: 37 | # Do not wait up to the default of 10 minutes for network timeouts in a workflow which runs ca. 5 minutes. 38 | SEGMENT_DOWNLOAD_TIMEOUT_MINS: 2 39 | steps: 40 | 41 | - name: Checkout 42 | uses: actions/checkout@v4 43 | 44 | # Caching Docker images is not ready yet, see 45 | # https://github.com/storeman-developers/harbour-storeman-installer/blob/devel/cache-docker-images_github-ci.md 46 | #- name: Cache Docker images of the Sailfish-SDK 47 | # id: cache-sdk 48 | # uses: actions/cache@v3 49 | # with: 50 | # path: $GITHUB_WORKSPACE/… 51 | # key: cache 52 | 53 | - name: Prepare 54 | run: mkdir RPMS 55 | 56 | - name: Build armv7hl on ${{ env.RELEASE }} 57 | uses: coderus/github-sfos-build@old-stable 58 | with: 59 | release: ${{ env.RELEASE }} 60 | arch: armv7hl 61 | 62 | - name: Build i486 on ${{ env.RELEASE }} 63 | uses: coderus/github-sfos-build@old-stable 64 | with: 65 | release: ${{ env.RELEASE }} 66 | arch: i486 67 | 68 | - name: Upload build results 69 | uses: actions/upload-artifact@v4 70 | with: 71 | name: RPM-build-results_SFOS3.1.0-3.2.1 72 | path: RPMS/ 73 | 74 | # Just for fun, see https://feathericons.com/ and 75 | # https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions#branding 76 | #branding: # "Invalid workflow file: Unexpected value 'branding'"; maybe action MUST be located in '/' or the name MUST be action.yml or both, see e.g., https://github.com/actions/cache/blob/main/action.yml#L37 77 | # icon: 'gift' 78 | # color: 'purple' 79 | 80 | -------------------------------------------------------------------------------- /.github/workflows/build-sfos3.3.yml: -------------------------------------------------------------------------------- 1 | name: CI - sfos3.3 branch on SDK for 3.3.0 (armv7hl,i486) & for 4.0.1 (aarch64) 2 | 3 | env: 4 | RELEASE: 3.3.0.14 5 | 6 | on: 7 | push: 8 | tags: 9 | - 'release*_sfos3.3/*' 10 | - 'rc*_sfos3.3/*' 11 | - 'beta*_sfos3.3/*' 12 | - 'alpha*_sfos3.3/*' 13 | - 'sfos3.3/*' 14 | pull_request: 15 | branches: 16 | - sfos3.3 17 | # Allows to run this workflow manually from the Actions tab. 18 | #workflow_dispatch: 19 | # Rather set a new release in the spec file and a new tag in the format N/X.Y.Z (e.g., release2_sfos3.3/0.6.3) to build a release version again. 20 | 21 | defaults: 22 | run: 23 | # Note that 'bash' provides -o pipefail, in contrast to the default (i.e., unspecified, which also uses bash) or 'sh', 24 | # see https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsshell 25 | shell: sh 26 | 27 | # See, e.g.: https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#example-only-cancel-in-progress-jobs-or-runs-for-the-current-workflow 28 | concurrency: 29 | group: ci-${{ github.ref_name }} 30 | # 'false' (default) allows for two concurrent runs, one executing and one freshly enqueued; 'true' for only one; no 'concurrency:' defined for multiple. 31 | cancel-in-progress: false 32 | 33 | jobs: 34 | build: 35 | runs-on: ubuntu-24.04 36 | env: 37 | # Do not wait up to the default of 10 minutes for network timeouts in a workflow which runs ca. 10 minutes. 38 | SEGMENT_DOWNLOAD_TIMEOUT_MINS: 2 39 | steps: 40 | 41 | - name: Checkout 42 | uses: actions/checkout@v4 43 | 44 | # Caching Docker images is not ready yet, see 45 | # https://github.com/storeman-developers/harbour-storeman-installer/blob/devel/cache-docker-images_github-ci.md 46 | #- name: Cache Docker images of the Sailfish-SDK 47 | # id: cache-sdk 48 | # uses: actions/cache@v3 49 | # with: 50 | # path: $GITHUB_WORKSPACE/… 51 | # key: cache 52 | 53 | - name: Prepare 54 | run: mkdir RPMS 55 | 56 | - name: Build aarch64 on 4.0.1.45 57 | uses: coderus/github-sfos-build@old-stable 58 | with: 59 | release: 4.0.1.45 60 | arch: aarch64 61 | 62 | - name: Build armv7hl on ${{ env.RELEASE }} 63 | uses: coderus/github-sfos-build@old-stable 64 | with: 65 | release: ${{ env.RELEASE }} 66 | arch: armv7hl 67 | 68 | - name: Build i486 on ${{ env.RELEASE }} 69 | uses: coderus/github-sfos-build@old-stable 70 | with: 71 | release: ${{ env.RELEASE }} 72 | arch: i486 73 | 74 | - name: Upload build results 75 | uses: actions/upload-artifact@v4 76 | with: 77 | name: RPM-build-results_SFOS3.3.0-4.1.0 78 | path: RPMS/ 79 | 80 | # Just for fun, see https://feathericons.com/ and 81 | # https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions#branding 82 | #branding: # "Invalid workflow file: Unexpected value 'branding'"; maybe action MUST be located in '/' or the name MUST be action.yml or both, see e.g., https://github.com/actions/cache/blob/main/action.yml#L37 83 | # icon: 'gift' 84 | # color: 'purple' 85 | 86 | -------------------------------------------------------------------------------- /.github/workflows/build-sfos4.2.yml: -------------------------------------------------------------------------------- 1 | name: CI - sfos4.2 branch on SDK for 4.2.0 (aarch64,armv7hl,i486) 2 | 3 | env: 4 | RELEASE: 4.2.0.21 5 | 6 | on: 7 | push: 8 | tags: 9 | - 'release*_sfos4.2/*' 10 | - 'rc*_sfos4.2/*' 11 | - 'beta*_sfos4.2/*' 12 | - 'alpha*_sfos4.2/*' 13 | - 'sfos4.2/*' 14 | pull_request: 15 | branches: 16 | - sfos4.2 17 | # Allows to run this workflow manually from the Actions tab. 18 | #workflow_dispatch: 19 | # Rather set a new release in the spec file and a new tag in the format N/X.Y.Z (e.g., release2_sfos4.2/0.6.3) to build a release version again. 20 | 21 | defaults: 22 | run: 23 | # Note that 'bash' provides -o pipefail, in contrast to the default (i.e., unspecified, which also uses bash) or 'sh', 24 | # see https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsshell 25 | shell: sh 26 | 27 | # See, e.g.: https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#example-only-cancel-in-progress-jobs-or-runs-for-the-current-workflow 28 | concurrency: 29 | group: ci-${{ github.ref_name }} 30 | # 'false' (default) allows for two concurrent runs, one executing and one freshly enqueued; 'true' for only one; no 'concurrency:' defined for multiple. 31 | cancel-in-progress: false 32 | 33 | jobs: 34 | build: 35 | runs-on: ubuntu-24.04 36 | env: 37 | # Do not wait up to the default of 10 minutes for network timeouts in a workflow which runs ca. 10 minutes. 38 | SEGMENT_DOWNLOAD_TIMEOUT_MINS: 2 39 | steps: 40 | 41 | - name: Checkout 42 | uses: actions/checkout@v4 43 | 44 | # Caching Docker images is not ready yet, see 45 | # https://github.com/storeman-developers/harbour-storeman-installer/blob/devel/cache-docker-images_github-ci.md 46 | #- name: Cache Docker images of the Sailfish-SDK 47 | # id: cache-sdk 48 | # uses: actions/cache@v3 49 | # with: 50 | # path: $GITHUB_WORKSPACE/… 51 | # key: cache 52 | 53 | - name: Prepare 54 | run: mkdir RPMS 55 | 56 | - name: Build aarch64 on ${{ env.RELEASE }} 57 | uses: coderus/github-sfos-build@master 58 | with: 59 | release: ${{ env.RELEASE }} 60 | arch: aarch64 61 | 62 | - name: Build armv7hl on ${{ env.RELEASE }} 63 | uses: coderus/github-sfos-build@master 64 | with: 65 | release: ${{ env.RELEASE }} 66 | arch: armv7hl 67 | 68 | - name: Build i486 on ${{ env.RELEASE }} 69 | uses: coderus/github-sfos-build@master 70 | with: 71 | release: ${{ env.RELEASE }} 72 | arch: i486 73 | 74 | - name: Upload build results 75 | uses: actions/upload-artifact@v4 76 | with: 77 | name: RPM-build-results_SFOS4.2.0+ 78 | path: RPMS/ 79 | 80 | # Just for fun, see https://feathericons.com/ and 81 | # https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions#branding 82 | #branding: # "Invalid workflow file: Unexpected value 'branding'"; maybe action MUST be located in '/' or the name MUST be action.yml or both, see e.g., https://github.com/actions/cache/blob/main/action.yml#L37 83 | # icon: 'gift' 84 | # color: 'purple' 85 | 86 | -------------------------------------------------------------------------------- /.github/workflows/build-sfos4.2_sdk-4.3.yml: -------------------------------------------------------------------------------- 1 | name: CI - sfos4.2 branch on SDK for 4.3.0 (aarch64,armv7hl,i486) 2 | 3 | env: 4 | # For the available docker images, see https://github.com/CODeRUS/docker-sailfishos-platform-sdk 5 | # Binaries for 4.3.0 are known to run on the most recent SFOS release (5.0.0 as of April 2025), 6 | # but when this breaks a new CI workflow configuration file shall be created from this one for the 7 | # "oldest common denominator"-SDK known to generate binaries which run on the then current SailfishOS release. 8 | RELEASE: 4.3.0.12 9 | 10 | on: 11 | push: 12 | tags: 13 | - 'release*_sfos4.2/*' 14 | - 'rc*_sfos4.2/*' 15 | - 'beta*_sfos4.2/*' 16 | - 'alpha*_sfos4.2/*' 17 | - 'sfos4.2/*' 18 | # Allows to run this workflow manually from the Actions tab. 19 | #workflow_dispatch: 20 | # Rather set a new release in the spec file and a new tag in the format N/X.Y.Z (e.g., release2_sfos4.2/0.6.3) to build a release version again. 21 | 22 | defaults: 23 | run: 24 | # Note that 'bash' provides -o pipefail, in contrast to the default (i.e., unspecified, which also uses bash) or 'sh', 25 | # see https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsshell 26 | shell: sh 27 | 28 | # See, e.g.: https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#example-only-cancel-in-progress-jobs-or-runs-for-the-current-workflow 29 | concurrency: 30 | group: ci-${{ github.ref_name }} 31 | # 'false' (default) allows for two concurrent runs, one executing and one freshly enqueued; 'true' for only one; no 'concurrency:' defined for multiple. 32 | cancel-in-progress: false 33 | 34 | jobs: 35 | build: 36 | runs-on: ubuntu-24.04 37 | env: 38 | # Do not wait up to the default of 10 minutes for network timeouts in a workflow which runs ca. 10 minutes. 39 | SEGMENT_DOWNLOAD_TIMEOUT_MINS: 2 40 | steps: 41 | 42 | - name: Checkout 43 | uses: actions/checkout@v4 44 | 45 | # Caching Docker images is not ready yet, see 46 | # https://github.com/storeman-developers/harbour-storeman-installer/blob/devel/cache-docker-images_github-ci.md 47 | #- name: Cache Docker images of the Sailfish-SDK 48 | # id: cache-sdk 49 | # uses: actions/cache@v3 50 | # with: 51 | # path: $GITHUB_WORKSPACE/… 52 | # key: cache 53 | 54 | - name: Prepare 55 | run: mkdir RPMS 56 | 57 | - name: Build aarch64 on ${{ env.RELEASE }} 58 | uses: coderus/github-sfos-build@master 59 | with: 60 | release: ${{ env.RELEASE }} 61 | arch: aarch64 62 | 63 | - name: Build armv7hl on ${{ env.RELEASE }} 64 | uses: coderus/github-sfos-build@master 65 | with: 66 | release: ${{ env.RELEASE }} 67 | arch: armv7hl 68 | 69 | - name: Build i486 on ${{ env.RELEASE }} 70 | uses: coderus/github-sfos-build@master 71 | with: 72 | release: ${{ env.RELEASE }} 73 | arch: i486 74 | 75 | - name: Upload build results 76 | uses: actions/upload-artifact@v4 77 | with: 78 | name: RPM-build-results_SFOS4.3.0+ 79 | path: RPMS/ 80 | 81 | # Just for fun, see https://feathericons.com/ and 82 | # https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions#branding 83 | #branding: # "Invalid workflow file: Unexpected value 'branding'"; maybe action MUST be located in '/' or the name MUST be action.yml or both, see e.g., https://github.com/actions/cache/blob/main/action.yml#L37 84 | # icon: 'gift' 85 | # color: 'purple' 86 | 87 | -------------------------------------------------------------------------------- /.github/workflows/build-sfos4.2_sdk-latest.yml: -------------------------------------------------------------------------------- 1 | name: CI - sfos4.2 branch on latest SDK (aarch64,armv7hl,i486) 2 | 3 | env: 4 | # For the available docker images, see https://github.com/CODeRUS/docker-sailfishos-platform-sdk 5 | RELEASE: 5.0.0.43 6 | 7 | on: 8 | push: 9 | tags: 10 | - 'release*_sfos4.2/*' 11 | - 'rc*_sfos4.2/*' 12 | - 'beta*_sfos4.2/*' 13 | - 'alpha*_sfos4.2/*' 14 | - 'sfos4.2/*' 15 | pull_request: 16 | branches: 17 | - sfos4.2 18 | # Allows to run this workflow manually from the Actions tab. 19 | #workflow_dispatch: 20 | # Rather set a new release in the spec file and a new tag in the format N/X.Y.Z (e.g., release2_sfos4.2/0.6.3) to build a release version again. 21 | 22 | defaults: 23 | run: 24 | # Note that 'bash' provides -o pipefail, in contrast to the default (i.e., unspecified, which also uses bash) or 'sh', 25 | # see https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsshell 26 | shell: sh 27 | 28 | # See, e.g.: https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#example-only-cancel-in-progress-jobs-or-runs-for-the-current-workflow 29 | concurrency: 30 | group: ci-${{ github.ref_name }} 31 | # 'false' (default) allows for two concurrent runs, one executing and one freshly enqueued; 'true' for only one; no 'concurrency:' defined for multiple. 32 | cancel-in-progress: false 33 | 34 | jobs: 35 | build: 36 | runs-on: ubuntu-24.04 37 | env: 38 | # Do not wait up to the default of 10 minutes for network timeouts in a workflow which runs ca. 10 minutes. 39 | SEGMENT_DOWNLOAD_TIMEOUT_MINS: 2 40 | steps: 41 | 42 | - name: Checkout 43 | uses: actions/checkout@v4 44 | 45 | # Caching Docker images is not ready yet, see 46 | # https://github.com/storeman-developers/harbour-storeman-installer/blob/devel/cache-docker-images_github-ci.md 47 | #- name: Cache Docker images of the Sailfish-SDK 48 | # id: cache-sdk 49 | # uses: actions/cache@v3 50 | # with: 51 | # path: $GITHUB_WORKSPACE/… 52 | # key: cache 53 | 54 | - name: Prepare 55 | run: mkdir RPMS 56 | 57 | - name: Build aarch64 on ${{ env.RELEASE }} 58 | uses: coderus/github-sfos-build@master 59 | with: 60 | release: ${{ env.RELEASE }} 61 | arch: aarch64 62 | 63 | - name: Build armv7hl on ${{ env.RELEASE }} 64 | uses: coderus/github-sfos-build@master 65 | with: 66 | release: ${{ env.RELEASE }} 67 | arch: armv7hl 68 | 69 | - name: Build i486 on ${{ env.RELEASE }} 70 | uses: coderus/github-sfos-build@master 71 | with: 72 | release: ${{ env.RELEASE }} 73 | arch: i486 74 | 75 | - name: Upload build results 76 | uses: actions/upload-artifact@v4 77 | with: 78 | name: RPM-build-results_SFOS${{ env.RELEASE }}+ 79 | path: RPMS/ 80 | 81 | # Just for fun, see https://feathericons.com/ and 82 | # https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions#branding 83 | #branding: # "Invalid workflow file: Unexpected value 'branding'"; maybe action MUST be located in '/' or the name MUST be action.yml or both, see e.g., https://github.com/actions/cache/blob/main/action.yml#L37 84 | # icon: 'gift' 85 | # color: 'purple' 86 | 87 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pro.user 2 | 3 | -------------------------------------------------------------------------------- /.tx/config: -------------------------------------------------------------------------------- 1 | [main] 2 | host = https://www.transifex.com 3 | 4 | [harbour-storeman.harbour-storeman_ts] 5 | source_file = translations/harbour-storeman.ts 6 | source_lang = en 7 | type = QT 8 | file_filter = translations/harbour-storeman-.ts 9 | -------------------------------------------------------------------------------- /.tx/config.yml: -------------------------------------------------------------------------------- 1 | git: 2 | # Basic setup guide for GitHub: 3 | # https://help.transifex.com/en/articles/6265125 4 | filters: 5 | - filter_type: file 6 | # All supported i18n types (hard to find): https://docs.transifex.com/formats 7 | file_format: QT 8 | source_language: en 9 | source_file: translations/harbour-storeman.ts 10 | # Path expression to translation files, must be quoted in single-quotes 11 | # and must contain placeholder: 12 | translation_files_expression: 'translations/harbour-storeman-.ts' 13 | -------------------------------------------------------------------------------- /.xdata/screenshots/screenshot-screenshot-storeman-01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/storeman-developers/harbour-storeman/732b7370fba26a0206e0501a72ad2e4b4c992c56/.xdata/screenshots/screenshot-screenshot-storeman-01.png -------------------------------------------------------------------------------- /.xdata/screenshots/screenshot-screenshot-storeman-02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/storeman-developers/harbour-storeman/732b7370fba26a0206e0501a72ad2e4b4c992c56/.xdata/screenshots/screenshot-screenshot-storeman-02.png -------------------------------------------------------------------------------- /.xdata/screenshots/screenshot-screenshot-storeman-03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/storeman-developers/harbour-storeman/732b7370fba26a0206e0501a72ad2e4b4c992c56/.xdata/screenshots/screenshot-screenshot-storeman-03.png -------------------------------------------------------------------------------- /.xdata/screenshots/screenshot-screenshot-storeman-04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/storeman-developers/harbour-storeman/732b7370fba26a0206e0501a72ad2e4b4c992c56/.xdata/screenshots/screenshot-screenshot-storeman-04.png -------------------------------------------------------------------------------- /.xdata/screenshots/screenshot-screenshot-storeman-06.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/storeman-developers/harbour-storeman/732b7370fba26a0206e0501a72ad2e4b4c992c56/.xdata/screenshots/screenshot-screenshot-storeman-06.png -------------------------------------------------------------------------------- /.xdata/screenshots/screenshot-screenshot-storeman-07.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/storeman-developers/harbour-storeman/732b7370fba26a0206e0501a72ad2e4b4c992c56/.xdata/screenshots/screenshot-screenshot-storeman-07.png -------------------------------------------------------------------------------- /.xdata/screenshots/screenshot-screenshot-storeman-08.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/storeman-developers/harbour-storeman/732b7370fba26a0206e0501a72ad2e4b4c992c56/.xdata/screenshots/screenshot-screenshot-storeman-08.png -------------------------------------------------------------------------------- /.xdata/screenshots/screenshot-screenshot-storeman-09.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/storeman-developers/harbour-storeman/732b7370fba26a0206e0501a72ad2e4b4c992c56/.xdata/screenshots/screenshot-screenshot-storeman-09.png -------------------------------------------------------------------------------- /.xdata/social-media-icons/harbour-storeman_1280x640.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/storeman-developers/harbour-storeman/732b7370fba26a0206e0501a72ad2e4b4c992c56/.xdata/social-media-icons/harbour-storeman_1280x640.png -------------------------------------------------------------------------------- /.xdata/social-media-icons/harbour-storeman_1500x500.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/storeman-developers/harbour-storeman/732b7370fba26a0206e0501a72ad2e4b4c992c56/.xdata/social-media-icons/harbour-storeman_1500x500.png -------------------------------------------------------------------------------- /.xdata/social-media-icons/harbour-storeman_792x792.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/storeman-developers/harbour-storeman/732b7370fba26a0206e0501a72ad2e4b4c992c56/.xdata/social-media-icons/harbour-storeman_792x792.png -------------------------------------------------------------------------------- /.xdata/social-media-icons/harbour-storeman_960x640.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/storeman-developers/harbour-storeman/732b7370fba26a0206e0501a72ad2e4b4c992c56/.xdata/social-media-icons/harbour-storeman_960x640.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022-2024 olf (Olf0) 4 | 2017-2022 Petr Tsymbarovich (mentaljam / osetr) 5 | 2019-2022 Björn Bidar (Thaodan) 6 | 2024 Peter G. (nephros) 7 | 2024 citronalco 8 | 2020 Dmitry Gerasimov (dseight) 9 | 2019 Matti Viljanen (direc85) 10 | 2019 Miklós Márton (martonmiklos) 11 | 2018 elros34 12 | 2017 Christoph (inta) 13 | 14 | Permission is hereby granted, free of charge, to any person obtaining a copy 15 | of this software and associated documentation files (the "Software"), to deal 16 | in the Software without restriction, including without limitation the rights 17 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 18 | copies of the Software, and to permit persons to whom the Software is 19 | furnished to do so, subject to the following conditions: 20 | 21 | The above copyright notice and this permission notice (including the next 22 | paragraph) shall be included in all copies or substantial portions of the 23 | Software. 24 | 25 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 26 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 27 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 28 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 29 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 30 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 31 | SOFTWARE. 32 | 33 | Canonical source for this license: https://spdx.org/licenses/MIT.html 34 | -------------------------------------------------------------------------------- /data/harbour-storeman: -------------------------------------------------------------------------------- 1 | # Allow harbour-storeman to manage packages 2 | /usr/bin/harbour-storeman,r 3 | -------------------------------------------------------------------------------- /data/harbour.storeman.service: -------------------------------------------------------------------------------- 1 | [D-BUS Service] 2 | Name=harbour.storeman.service 3 | Exec=/usr/bin/invoker --type=silica-qt5 -s /usr/bin/harbour-storeman 4 | -------------------------------------------------------------------------------- /harbour-storeman.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Type=Application 3 | X-Nemo-Application-Type=silica-qt5 4 | Icon=harbour-storeman 5 | Exec=harbour-storeman 6 | Name=Storeman 7 | Categories=System;Utility;Network;Settings;PackageManager; 8 | 9 | [X-HarbourBackup] 10 | BackupPathList=.config/harbour-storeman/:.local/share/harbour-storeman/ 11 | -------------------------------------------------------------------------------- /icons/108x108/harbour-storeman.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/storeman-developers/harbour-storeman/732b7370fba26a0206e0501a72ad2e4b4c992c56/icons/108x108/harbour-storeman.png -------------------------------------------------------------------------------- /icons/128x128/harbour-storeman.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/storeman-developers/harbour-storeman/732b7370fba26a0206e0501a72ad2e4b4c992c56/icons/128x128/harbour-storeman.png -------------------------------------------------------------------------------- /icons/172x172/harbour-storeman.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/storeman-developers/harbour-storeman/732b7370fba26a0206e0501a72ad2e4b4c992c56/icons/172x172/harbour-storeman.png -------------------------------------------------------------------------------- /icons/256x256/harbour-storeman.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/storeman-developers/harbour-storeman/732b7370fba26a0206e0501a72ad2e4b4c992c56/icons/256x256/harbour-storeman.png -------------------------------------------------------------------------------- /icons/480x480/harbour-storeman.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/storeman-developers/harbour-storeman/732b7370fba26a0206e0501a72ad2e4b4c992c56/icons/480x480/harbour-storeman.png -------------------------------------------------------------------------------- /icons/560x560/harbour-storeman.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/storeman-developers/harbour-storeman/732b7370fba26a0206e0501a72ad2e4b4c992c56/icons/560x560/harbour-storeman.png -------------------------------------------------------------------------------- /icons/86x86/harbour-storeman.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/storeman-developers/harbour-storeman/732b7370fba26a0206e0501a72ad2e4b4c992c56/icons/86x86/harbour-storeman.png -------------------------------------------------------------------------------- /qml/StoremanStyles.qml: -------------------------------------------------------------------------------- 1 | pragma Singleton 2 | import QtQuick 2.0 3 | import Sailfish.Silica 1.0 4 | 5 | 6 | QtObject { 7 | readonly property string commentStyle: " 8 | " 23 | } 24 | -------------------------------------------------------------------------------- /qml/components/AppInfoLabel.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import Sailfish.Silica 1.0 3 | 4 | Label { 5 | property string label 6 | property string value 7 | 8 | width: parent.width 9 | color: Theme.highlightColor 10 | wrapMode: Text.WrapAtWordBoundaryOrAnywhere 11 | font.pixelSize: Theme.fontSizeExtraSmall 12 | textFormat: Text.StyledText 13 | text: "%1 %2" 14 | .arg(Theme.secondaryHighlightColor) 15 | .arg(label) 16 | .arg(value.replace(/\n/g, "
")) 17 | } 18 | -------------------------------------------------------------------------------- /qml/components/AppPageMenu.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import Sailfish.Silica 1.0 3 | import org.nemomobile.lipstick 0.1 4 | import harbour.orn 1.0 5 | 6 | PullDownMenu { 7 | readonly property bool _enableMenu: networkManager.connected && 8 | !itemInProgress(app.repoAlias) && 9 | !itemInProgress(app.packageName) 10 | 11 | id: pullMenu 12 | visible: OrnPm.initialised 13 | 14 | MenuItem { 15 | enabled: !app.running 16 | //% "Reload" 17 | text: qsTrId("orn-reload") 18 | onClicked: { 19 | flickable.visible = false 20 | app.ornRequest() 21 | } 22 | } 23 | 24 | MenuItem { 25 | id: repoMenuItem 26 | visible: text 27 | enabled: _enableMenu 28 | text: { 29 | if (!app.packageName || !app.repoAlias) { 30 | return "" 31 | } 32 | switch (app.repoStatus) { 33 | case OrnPm.RepoNotInstalled: 34 | //% "Add repository" 35 | return qsTrId("orn-repo-add") 36 | case OrnPm.RepoDisabled: 37 | //% "Enable repository" 38 | return qsTrId("orn-repo-enable") 39 | default: 40 | return qsTrId("orn-refresh-cache") 41 | } 42 | } 43 | 44 | onClicked: { 45 | switch (app.repoStatus) { 46 | case OrnPm.RepoNotInstalled: 47 | //% "Adding" 48 | Remorse.popupAction(page, qsTrId("orn-adding-repo"), function() { 49 | OrnPm.addRepo(app.userName) 50 | }) 51 | break 52 | case OrnPm.RepoDisabled: 53 | OrnPm.modifyRepo(app.repoAlias, OrnPm.EnableRepo) 54 | break 55 | default: 56 | OrnPm.refreshRepo(app.repoAlias, true) 57 | break 58 | } 59 | } 60 | } 61 | 62 | MenuItem { 63 | id: installMenuItem 64 | visible: text 65 | enabled: _enableMenu && app.packageName 66 | text: { 67 | switch (_packageStatus) { 68 | case OrnPm.PackageAvailable: 69 | //% "Install" 70 | return qsTrId("orn-install") 71 | case OrnPm.PackageInstalled: 72 | case OrnPm.PackageUpdateAvailable: 73 | //% "Remove" 74 | return qsTrId("orn-remove") 75 | default: 76 | // TODO: This also should be shown for apps with no solved packages 77 | // (for example when there is no i486 packages) 78 | //% "No packages available" 79 | return app.packageName ? "" : qsTrId("orn-no-packages") 80 | 81 | } 82 | } 83 | 84 | onClicked: { 85 | switch (_packageStatus) { 86 | case OrnPm.PackageAvailable: 87 | OrnPm.installPackage(app.availableId) 88 | break 89 | case OrnPm.PackageInstalled: 90 | case OrnPm.PackageUpdateAvailable: 91 | Remorse.popupAction(page, qsTrId("orn-removing"), function() { 92 | OrnPm.removePackage(app.installedId) 93 | }) 94 | break 95 | default: 96 | break 97 | } 98 | } 99 | } 100 | 101 | MenuItem { 102 | id: updateMenuItem 103 | visible: _packageStatus == OrnPm.PackageUpdateAvailable 104 | enabled: _enableMenu 105 | //% "Update" 106 | text: qsTrId("orn-update") 107 | onClicked: OrnPm.updatePackage(app.packageName) 108 | } 109 | 110 | MenuItem { 111 | id: launchMenuItem 112 | visible: app.desktopFile 113 | //% "Launch" 114 | text: qsTrId("orn-launch") 115 | onClicked: launcher.launchApplication() 116 | 117 | LauncherItem { 118 | id: launcher 119 | filePath: app.desktopFile 120 | } 121 | } 122 | 123 | MenuStatusLabel { } 124 | } 125 | -------------------------------------------------------------------------------- /qml/components/BackupLabel.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import Sailfish.Silica 1.0 3 | 4 | 5 | Label { 6 | color: Theme.highlightColor 7 | wrapMode: Text.WordWrap 8 | font.pixelSize: Theme.fontSizeSmall 9 | //% "

Backup to a file


" 10 | //% "

Backup allows you to save your current OpenRepos repositories, installed applications and bookmarks and " 11 | //% "restore them later (for example after factory reset). A backup is a local file that is saved to the
" 12 | //% "~/Documents/Storeman directory.


" 13 | //% "

Attention! You should copy your backups manually to some safe place before performing a factory reset. " 14 | //% "It could be your SD card, external device, cloud storage or something else.

" 15 | text: qsTrId("orn-backup-hint") 16 | } 17 | -------------------------------------------------------------------------------- /qml/components/BackupOptions.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import Sailfish.Silica 1.0 3 | import harbour.orn 1.0 4 | 5 | 6 | Column { 7 | readonly property string path: { 8 | var p = StandardPaths.documents + "/Storeman/" + textField.text 9 | if (p.lastIndexOf('.ini') === -1) { 10 | p += '.ini' 11 | } 12 | return p.trim() 13 | } 14 | readonly property bool _fileExists: path && Storeman.fileExists(path) 15 | readonly property bool acceptable: !textField.errorHighlight 16 | 17 | width: parent.width 18 | 19 | TextField { 20 | id: textField 21 | width: parent.width 22 | //% "A file name for backup" 23 | placeholderText: qsTrId("orn-backup-filenameph") 24 | inputMethodHints: Qt.ImhNoPredictiveText | Qt.ImhNoAutoUppercase 25 | validator: RegExpValidator { 26 | regExp: /((?!\/).)+/ 27 | } 28 | errorHighlight: !acceptableInput || _fileExists 29 | text: backupOptions.filename 30 | //% "File already exists" 31 | label: _fileExists ? qsTrId("orn-file-exists") : placeholderText 32 | 33 | onTextChanged: backupOptions.filename = text 34 | } 35 | 36 | SectionHeader { 37 | //% "What to backup" 38 | text: qsTrId("orn-backup-items") 39 | } 40 | 41 | TextSwitch { 42 | checked: backupOptions.repos 43 | text: qsTrId("orn-repositories") 44 | 45 | onCheckedChanged: backupOptions.repos = checked 46 | } 47 | 48 | TextSwitch { 49 | checked: backupOptions.installed 50 | //% "Installed applications" 51 | text: qsTrId("orn-backup-apps") 52 | 53 | onCheckedChanged: backupOptions.installed = checked 54 | } 55 | 56 | TextSwitch { 57 | checked: backupOptions.bookmarks 58 | text: qsTrId("orn-bookmarks") 59 | 60 | onCheckedChanged: backupOptions.bookmarks = checked 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /qml/components/BookmarkButton.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import Sailfish.Silica 1.0 3 | import harbour.orn 1.0 4 | 5 | IconButton { 6 | property int appId 7 | property var bookmarked: undefined 8 | property bool _updateState 9 | 10 | icon.source: (bookmarked ? "image://theme/icon-m-favorite-selected?" : 11 | "image://theme/icon-m-favorite?") + 12 | (pressed ? Theme.highlightColor : Theme.primaryColor) 13 | 14 | onClicked: { 15 | var f = bookmarked ? OrnClient.removeBookmark : OrnClient.addBookmark 16 | f(appId) 17 | if (_updateState) { 18 | bookmarked = !bookmarked 19 | } 20 | } 21 | 22 | Component.onCompleted: { 23 | _updateState = bookmarked === undefined 24 | if (_updateState) { 25 | bookmarked = OrnClient.hasBookmark(appId) 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /qml/components/CategoriesFilterDelegate.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import Sailfish.Silica 1.0 3 | 4 | BackgroundItem { 5 | property bool categoryVisible 6 | property alias text: label.text 7 | property alias textAlignment: label.horizontalAlignment 8 | property int depth: 0 9 | readonly property color _color: categoryVisible ? highlighted ? Theme.highlightColor : Theme.primaryColor : Theme.secondaryColor 10 | 11 | height: Theme.itemSizeSmall 12 | opacity: categoryVisible ? 1.0 : Theme.opacityLow 13 | 14 | Label { 15 | id: label 16 | anchors { 17 | left: parent.left 18 | right: image.left 19 | verticalCenter: parent.verticalCenter 20 | leftMargin: Theme.horizontalPageMargin + depth * Theme.paddingLarge 21 | rightMargin: Theme.paddingMedium 22 | } 23 | horizontalAlignment: Text.AlignRight 24 | truncationMode: TruncationMode.Fade 25 | color: _color 26 | } 27 | 28 | Image { 29 | id: image 30 | anchors { 31 | right: parent.right 32 | verticalCenter: parent.verticalCenter 33 | rightMargin: Theme.horizontalPageMargin 34 | } 35 | source: (categoryVisible ? "image://theme/icon-m-accept?" : "image://theme/icon-m-cancel?") + _color 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /qml/components/CommentLabel.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import Sailfish.Silica 1.0 3 | 4 | BackgroundItem { 5 | width: Screen.width 6 | height: Theme.itemSizeLarge 7 | onClicked: if (networkManager.connected) 8 | pageStack.push(Qt.resolvedUrl("../pages/AuthorisationDialog.qml")) 9 | 10 | Label { 11 | anchors { 12 | left: parent.left 13 | right: parent.right 14 | margins: Theme.horizontalPageMargin 15 | verticalCenter: parent.verticalCenter 16 | } 17 | color: parent.pressed ? Theme.highlightColor : Theme.primaryColor 18 | wrapMode: Text.WordWrap 19 | //% "Login to comment" 20 | text: networkManager.connected ? qsTrId("orn-login2comment") : 21 | qsTrId("orn-network-idle") 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /qml/components/DisappearAnimation.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.2 2 | 3 | 4 | ParallelAnimation { 5 | property bool show: true 6 | property var target: null 7 | 8 | id: animation 9 | 10 | onShowChanged: { 11 | if (show) { 12 | oa.from = 0.0 13 | oa.to = 1.0 14 | na.from = 0.0 15 | na.to = target.implicitHeight 16 | } else { 17 | oa.from = 1.0 18 | oa.to = 0.0 19 | na.from = target.implicitHeight 20 | na.to = 0.0 21 | } 22 | start() 23 | } 24 | 25 | OpacityAnimator { 26 | id: oa 27 | target: animation.target 28 | duration: 200 29 | easing.type: Easing.InOutQuad 30 | } 31 | 32 | NumberAnimation { 33 | id: na 34 | target: animation.target 35 | property: "height" 36 | duration: 200 37 | easing.type: Easing.InOutQuad 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /qml/components/FancyPageHeader.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import Sailfish.Silica 1.0 3 | 4 | Item { 5 | property alias title: header.title 6 | property alias description: header.description 7 | property alias iconSource: image.source 8 | 9 | width: parent.width 10 | height: header.height 11 | 12 | PageHeader { 13 | id: header 14 | width: parent.width - 15 | (image.visible ? image.width + Theme.paddingMedium : 0) 16 | } 17 | 18 | Image { 19 | id: image 20 | visible: source.toString() 21 | anchors { 22 | verticalCenter: header.verticalCenter 23 | right: parent.right 24 | rightMargin: Theme.paddingMedium 25 | } 26 | width: Theme.iconSizeLauncher 27 | height: Theme.iconSizeLauncher 28 | fillMode: Image.PreserveAspectFit 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /qml/components/HtmlTagButton.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import Sailfish.Silica 1.0 3 | 4 | // Silica button height is hardcoded 5 | MouseArea { 6 | property string tag 7 | property string attrs: "" 8 | property alias text: buttonText.text 9 | property bool _showPress: (pressed && containsMouse) || pressTimer.running 10 | 11 | height: Theme.itemSizeExtraSmall * 0.75 12 | width: Math.max(height, buttonText.width + Theme.paddingLarge) 13 | 14 | onPressedChanged: if (pressed) pressTimer.start() 15 | onCanceled: pressTimer.stop() 16 | 17 | onClicked: { 18 | var editor = body._editor 19 | var selected = editor.selectedText 20 | if (selected) { 21 | editor.remove(editor.selectionStart, editor.selectionEnd) 22 | } 23 | editor.insert(editor.cursorPosition, "<%0%1>%2".arg(tag).arg(attrs).arg(selected)) 24 | if (!selected) { 25 | // 3 is for "" length 26 | editor.cursorPosition = editor.cursorPosition - tag.length - 3 27 | } 28 | } 29 | 30 | Rectangle { 31 | anchors.fill: parent 32 | radius: Theme.paddingSmall 33 | color: _showPress ? Theme.rgba(Theme.highlightBackgroundColor, Theme.highlightBackgroundOpacity) 34 | : Theme.rgba(Theme.primaryColor, 0.2) 35 | 36 | Label { 37 | id: buttonText 38 | anchors.centerIn: parent 39 | horizontalAlignment: Qt.AlignHCenter 40 | color: _showPress ? Theme.highlightColor : Theme.primaryColor 41 | textFormat: Text.RichText 42 | } 43 | } 44 | 45 | Timer { 46 | id: pressTimer 47 | interval: Theme.minimumPressHighlightTime 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /qml/components/IconLabel.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import Sailfish.Silica 1.0 3 | 4 | Row { 5 | property bool running: false 6 | property string icon 7 | property alias text: label.text 8 | 9 | spacing: Theme.paddingSmall 10 | 11 | Loader { 12 | anchors.verticalCenter: parent.verticalCenter 13 | sourceComponent: running ? busyComponent : iconComponent 14 | } 15 | 16 | Component { 17 | id: busyComponent 18 | BusyIndicator { 19 | size: BusyIndicatorSize.ExtraSmall 20 | running: true 21 | } 22 | } 23 | 24 | Component { 25 | id: iconComponent 26 | Image { 27 | source: icon ? icon + "?" + Theme.highlightColor : "" 28 | } 29 | } 30 | 31 | Label { 32 | id: label 33 | anchors.verticalCenter: parent.verticalCenter 34 | font.pixelSize: Theme.fontSizeExtraSmall 35 | color: Theme.highlightColor 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /qml/components/IntervalView.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import Sailfish.Silica 1.0 3 | 4 | SilicaListView { 5 | id: view 6 | snapMode: ListView.SnapToItem 7 | clip: true 8 | quickScrollEnabled: false 9 | 10 | delegate: BackgroundItem { 11 | width: view.width 12 | height: currentIndex === index 13 | ? Theme.itemSizeLarge 14 | : Theme.itemSizeSmall 15 | 16 | Behavior on height { NumberAnimation {} } 17 | 18 | onClicked: { 19 | currentIndex = index 20 | animation.moveTo(currentIndex) 21 | } 22 | 23 | Label { 24 | text: index 25 | anchors.centerIn: parent 26 | color: highlighted 27 | ? Theme.highlightColor 28 | : Theme.primaryColor 29 | font.pixelSize: currentIndex === index 30 | ? Theme.fontSizeHuge 31 | : Theme.fontSizeLarge 32 | 33 | Behavior on font.pixelSize { NumberAnimation {} } 34 | } 35 | } 36 | 37 | Component.onCompleted: positionViewAtIndex(currentIndex, ListView.Center) 38 | 39 | ListViewPositionAnimation { 40 | id: animation 41 | target: view 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /qml/components/ListMenuItem.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import Sailfish.Silica 1.0 3 | import harbour.orn 1.0 4 | 5 | ListItem { 6 | property alias text: label.text 7 | property string iconSource 8 | readonly property string _color: 9 | pressed ? Theme.highlightColor : 10 | enabled ? Theme.primaryColor : Theme.secondaryHighlightColor 11 | 12 | id: item 13 | width: parent.width 14 | contentHeight: Theme.itemSizeLarge 15 | 16 | Row { 17 | anchors { 18 | left: parent.left 19 | right: parent.right 20 | leftMargin: Theme.horizontalPageMargin 21 | rightMargin: Theme.horizontalPageMargin 22 | verticalCenter: parent.verticalCenter 23 | } 24 | spacing: Theme.paddingMedium 25 | 26 | Image { 27 | id: icon 28 | anchors.verticalCenter: parent.verticalCenter 29 | width: Theme.iconSizeMedium 30 | height: Theme.iconSizeMedium 31 | fillMode: Image.PreserveAspectFit 32 | source: iconSource ? (iconSource + "?" + _color) : "" 33 | } 34 | 35 | Label { 36 | id: label 37 | anchors.verticalCenter: parent.verticalCenter 38 | color: _color 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /qml/components/ListViewPositionAnimation.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | 3 | NumberAnimation { 4 | property var mode: ListView.Center 5 | 6 | function moveTo(index) { 7 | stop() 8 | from = target.contentY 9 | target.positionViewAtIndex(index, mode) 10 | // FIXME: Sometimes it's not defined 11 | to = target.contentY 12 | start() 13 | } 14 | 15 | property: "contentY" 16 | } 17 | -------------------------------------------------------------------------------- /qml/components/MainPageAppGridDelegate.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import Sailfish.Silica 1.0 3 | 4 | BackgroundItem { 5 | id: delegate 6 | width: cellWidth 7 | height: Theme.itemSizeSmall 8 | visible: model.index < gridColumns * gridRows 9 | 10 | onClicked: { 11 | if (!model.isValid) { 12 | return 13 | } 14 | pageStack.push(Qt.resolvedUrl("../pages/AppPage.qml"), { 15 | appId: model.appId 16 | }) 17 | } 18 | 19 | Image { 20 | id: icon 21 | anchors { 22 | left: parent.left 23 | leftMargin: Theme.horizontalPageMargin 24 | verticalCenter: parent.verticalCenter 25 | } 26 | width: Theme.iconSizeMedium 27 | height: Theme.iconSizeMedium 28 | source: model.iconSource 29 | } 30 | 31 | Item { 32 | id: labelItem 33 | anchors { 34 | left: icon.right 35 | leftMargin: Theme.paddingMedium 36 | right: parent.right 37 | rightMargin: Theme.horizontalPageMargin 38 | verticalCenter: parent.verticalCenter 39 | } 40 | height: authorLabel.y + authorLabel.height 41 | 42 | Label { 43 | id: titleLabel 44 | width: parent.width 45 | text: model.title 46 | color: delegate.highlighted ? Theme.highlightColor : Theme.primaryColor 47 | font.pixelSize: Theme.fontSizeTiny 48 | truncationMode: TruncationMode.Fade 49 | } 50 | 51 | Label { 52 | id: authorLabel 53 | anchors.top: titleLabel.bottom 54 | width: parent.width 55 | text: model.userName 56 | color: Theme.secondaryHighlightColor 57 | font.pixelSize: Theme.fontSizeTiny 58 | truncationMode: TruncationMode.Fade 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /qml/components/MainPageButton.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import Sailfish.Silica 1.0 3 | 4 | MoreButton { 5 | height: Theme.itemSizeMedium 6 | 7 | Rectangle { 8 | anchors.fill: parent 9 | gradient: Gradient { 10 | GradientStop { position: 0.0; color: Theme.rgba(Theme.highlightBackgroundColor, 0.15) } 11 | GradientStop { position: 1.0; color: "transparent" } 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /qml/components/MenuSearchItem.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import Sailfish.Silica 1.0 3 | 4 | MenuItem { 5 | visible: networkManager.connected 6 | text: qsTrId("orn-search") 7 | onClicked: pageStack.push(Qt.resolvedUrl("../pages/SearchPage.qml")) 8 | } 9 | -------------------------------------------------------------------------------- /qml/components/MenuStatusLabel.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import Sailfish.Silica 1.0 3 | import harbour.orn 1.0 4 | 5 | MenuLabel { 6 | property PullDownMenu __menu: parent.parent 7 | 8 | visible: text 9 | text: { 10 | if (!OrnPm.initialised) { 11 | __menu.busy = true 12 | //% "Initialising" 13 | return qsTrId("orn-pmstate-initialising") 14 | } 15 | 16 | var len = _operations.length 17 | 18 | if (len === 0) { 19 | __menu.busy = false 20 | return "" 21 | } 22 | 23 | if (len > 1) { 24 | __menu.busy = true 25 | //: There are always more than 1 operations 26 | //% "%n operations are in progress" 27 | return qsTrId("orn-pmstate-multiple", _operations.length) 28 | } 29 | 30 | __menu.busy = true 31 | var op = _operations[0] 32 | switch (op.operation) { 33 | case OrnPm.AddingRepo: 34 | //% "Adding repo %0" 35 | return qsTrId("orn-pmstate-addingrepo").arg(op.item) 36 | case OrnPm.RemovingRepo: 37 | //% "Removing repo %0" 38 | return qsTrId("orn-pmstate-removingrepo").arg(op.item) 39 | case OrnPm.EnablingRepo: 40 | //% "Enabling repo %0" 41 | return qsTrId("orn-pmstate-enablingrepo").arg(op.item) 42 | case OrnPm.DisablingRepo: 43 | //% "Disabling repo %0" 44 | return qsTrId("orn-pmstate-disablingrepo").arg(op.item) 45 | case OrnPm.RefreshingRepo: 46 | //% "Refreshing %0" 47 | return qsTrId("orn-pmstate-refreshingrepo").arg(op.item) 48 | case OrnPm.InstallingPackage: 49 | //% "Installing package %0" 50 | return qsTrId("orn-pmstate-installingpackage").arg(op.item) 51 | case OrnPm.RemovingPackage: 52 | //% "Removing package %0" 53 | return qsTrId("orn-pmstate-removingpackage").arg(op.item) 54 | case OrnPm.UpdatingPackage: 55 | //% "Updating package %0" 56 | return qsTrId("orn-pmstate-updatingpackage").arg(op.item) 57 | case OrnPm.RefreshingCache: 58 | //% "Refreshing of cache" 59 | return qsTrId("orn-pmstate-refreshingcache") 60 | default: 61 | return "" 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /qml/components/MoreButton.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import Sailfish.Silica 1.0 3 | 4 | BackgroundItem { 5 | property alias text: label.text 6 | property alias textAlignment: label.horizontalAlignment 7 | property int depth: 0 8 | readonly property color _color: enabled ? highlighted ? Theme.highlightColor : Theme.primaryColor : Theme.secondaryColor 9 | 10 | height: Theme.itemSizeSmall 11 | 12 | Label { 13 | id: label 14 | anchors { 15 | left: parent.left 16 | right: image.left 17 | verticalCenter: parent.verticalCenter 18 | leftMargin: Theme.horizontalPageMargin + depth * Theme.paddingLarge 19 | rightMargin: Theme.paddingMedium 20 | } 21 | horizontalAlignment: Text.AlignRight 22 | truncationMode: TruncationMode.Fade 23 | color: _color 24 | } 25 | 26 | Image { 27 | id: image 28 | anchors { 29 | right: parent.right 30 | verticalCenter: parent.verticalCenter 31 | rightMargin: Theme.horizontalPageMargin 32 | } 33 | source: "image://theme/icon-m-right?" + _color 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /qml/components/ParticipantsDelegate.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import Sailfish.Silica 1.0 3 | 4 | Column { 5 | property alias title: label.text 6 | property alias model: repeater.model 7 | 8 | width: parent.width 9 | visible: model && model.count 10 | 11 | Label { 12 | id: label 13 | width: parent.width 14 | wrapMode: Text.WordWrap 15 | horizontalAlignment: Qt.AlignHCenter 16 | color: Theme.secondaryHighlightColor 17 | font.pixelSize: Theme.fontSizeSmall 18 | } 19 | 20 | Repeater { 21 | id: repeater 22 | 23 | delegate: Label { 24 | width: parent.width 25 | wrapMode: Text.WordWrap 26 | horizontalAlignment: Qt.AlignHCenter 27 | color: Theme.secondaryColor 28 | font.pixelSize: Theme.fontSizeSmall 29 | text: name 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /qml/components/RatingBox.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import Sailfish.Silica 1.0 3 | 4 | Loader { 5 | property int ratingCount 6 | property real rating 7 | readonly property int _rating: rating / 20.0 + 0.5 8 | 9 | sourceComponent: ratingCount > 0 ? starBox : notRated 10 | 11 | Component { 12 | id: starBox 13 | 14 | Row { 15 | Repeater { 16 | id: ratingBox 17 | model: 5 18 | 19 | Image { 20 | width: Theme.iconSizeExtraSmall 21 | height: Theme.iconSizeExtraSmall 22 | opacity: index < _rating ? 1.0 : 0.5 23 | source: "image://theme/icon-s-favorite?" + 24 | (index < _rating ? 25 | "gold" : Theme.secondaryColor) 26 | } 27 | } 28 | } 29 | } 30 | 31 | Component { 32 | id: notRated 33 | 34 | Label { 35 | height: Theme.iconSizeExtraSmall 36 | font.pixelSize: Theme.fontSizeTiny 37 | color: Theme.secondaryColor 38 | //% "Not rated yet" 39 | text: qsTrId("orn-notrated") 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /qml/components/RefreshMenuItem.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import Sailfish.Silica 1.0 3 | 4 | MenuItem { 5 | property var model 6 | 7 | visible: model 8 | enabled: networkManager.connected 9 | //% "Refresh" 10 | text: qsTrId("orn-refresh") 11 | onClicked: model.reset() 12 | } 13 | -------------------------------------------------------------------------------- /qml/components/ScreenshotsBox.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import Sailfish.Silica 1.0 3 | 4 | Row { 5 | id: screenshotBox 6 | width: parent.width 7 | 8 | Repeater { 9 | model: Math.min(app.screenshots.length, 3) 10 | 11 | Image { 12 | width: screenshotBox.width / 3 13 | height: width 14 | fillMode: Image.PreserveAspectCrop 15 | source: app.screenshots[index].thumb 16 | 17 | BusyIndicator { 18 | anchors.centerIn: parent 19 | running: parent.status === Image.Loading 20 | } 21 | 22 | MouseArea { 23 | anchors.fill: parent 24 | onClicked: pageStack.push( 25 | Qt.resolvedUrl("../pages/ScreenshotPage.qml"), { 26 | model: app.screenshots, 27 | currentIndex: model.index 28 | }, PageStackAction.Immediate) 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /qml/components/StoremanHintLabel.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import Sailfish.Silica 1.0 3 | 4 | 5 | MouseArea { 6 | property bool _doClose: false 7 | property var hint 8 | property string text 9 | property alias invert: label.invert 10 | 11 | signal finished() 12 | 13 | anchors.fill: parent 14 | 15 | onClicked: { 16 | if (_doClose) { 17 | timer.stop() 18 | hint.stop() 19 | } else { 20 | _doClose = true 21 | timer.start() 22 | } 23 | } 24 | 25 | Timer { 26 | id: timer 27 | interval: 2000 28 | onTriggered: _doClose = false 29 | } 30 | 31 | InteractionHintLabel { 32 | id: label 33 | anchors { 34 | top: invert ? parent.top : undefined 35 | bottom: invert ? undefined : parent.bottom 36 | } 37 | opacity: hint && hint.running ? 1.0 : 0.0 38 | //% "Tap again to close the hint" 39 | text: _doClose ? qsTrId("orn-hint-close") : parent.text 40 | 41 | Behavior on opacity { 42 | FadeAnimation { 43 | duration: 1000 44 | 45 | onRunningChanged: { 46 | if (!running && label.opacity === 0.0) { 47 | _doClose = false 48 | finished() 49 | } 50 | } 51 | } 52 | } 53 | } 54 | } 55 | 56 | -------------------------------------------------------------------------------- /qml/components/StoremanTapHint.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.2 2 | import Sailfish.Silica 1.0 3 | 4 | Image { 5 | property alias running: blinkAnimation.running 6 | 7 | function start() { 8 | blinkAnimation.start() 9 | } 10 | 11 | function stop() { 12 | blinkAnimation.stop() 13 | } 14 | 15 | id: root 16 | anchors.centerIn: parent 17 | source: "image://theme/graphic-gesture-hint?" + Theme.primaryColor 18 | opacity: 0.0 19 | 20 | onRunningChanged: if (!running) opacity = 0.0 21 | 22 | SequentialAnimation { 23 | id: blinkAnimation 24 | loops: Animation.Infinite 25 | 26 | OpacityAnimator { 27 | target: root 28 | from: 0.0 29 | to: 1.0 30 | duration: 200 31 | } 32 | 33 | PauseAnimation { 34 | duration: 200 35 | } 36 | 37 | OpacityAnimator { 38 | target: root 39 | from: 1.0 40 | to: 0.0 41 | duration: 800 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /qml/components/StoremanTouchInteractionHint.qml: -------------------------------------------------------------------------------- 1 | import Sailfish.Silica 1.0 2 | 3 | TouchInteractionHint { 4 | direction: TouchInteraction.Left 5 | loops: Infinity 6 | } 7 | -------------------------------------------------------------------------------- /qml/components/TagDelegate.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import Sailfish.Silica 1.0 3 | 4 | 5 | ListItem { 6 | id: delegate 7 | contentHeight: label.height + Theme.paddingMedium * 2 8 | 9 | onClicked: pageStack.push(Qt.resolvedUrl("../pages/TagAppsPage.qml"), 10 | { 11 | tagId: model.tagId, 12 | tagName: model.name, 13 | previousAppId: page.previousAppId 14 | }) 15 | 16 | Label { 17 | id: label 18 | anchors { 19 | verticalCenter: parent.verticalCenter 20 | left: parent.left 21 | right: parent.right 22 | leftMargin: Theme.horizontalPageMargin 23 | rightMargin: Theme.horizontalPageMargin 24 | } 25 | wrapMode: Text.WordWrap 26 | text: model.name 27 | color: delegate.highlighted ? Theme.highlightColor : Theme.primaryColor 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /qml/cover/CoverPage.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import Sailfish.Silica 1.0 3 | import harbour.orn 1.0 4 | 5 | CoverBackground { 6 | 7 | function _activatePage(name, callback) { 8 | var namelength = name.length 9 | const pageOnStack = pageStack.find(function(p) { 10 | return p.toString().substr(0, namelength) === name 11 | }) 12 | if (pageOnStack) { 13 | if (callback) { 14 | callback(pageOnStack) 15 | } 16 | pageStack.pop(pageOnStack, PageStackAction.Immediate) 17 | } else { 18 | pageStack.push(Qt.resolvedUrl("../pages/%1.qml".arg(name)), {}, PageStackAction.Immediate) 19 | } 20 | __silica_applicationwindow_instance.activate() 21 | } 22 | 23 | Image { 24 | anchors.fill: parent 25 | source: Qt.resolvedUrl("./background.svg") 26 | fillMode: Image.PreserveAspectFit 27 | opacity: 0.15 28 | } 29 | 30 | Label { 31 | anchors { 32 | horizontalCenter: parent.horizontalCenter 33 | top: parent.top 34 | topMargin: Theme.paddingLarge 35 | } 36 | color: networkManager.connected 37 | ? Theme.highlightColor 38 | : Theme.highlightDimmerColor 39 | horizontalAlignment: Text.AlignHCenter 40 | font.pixelSize: Theme.fontSizeLarge 41 | text: applicationDisplayName 42 | } 43 | 44 | Label { 45 | visible: OrnPm.updatesAvailable 46 | anchors.centerIn: parent 47 | width: parent.width - 2 * (Screen.sizeCategory > Screen.Medium 48 | ? Theme.paddingMedium 49 | : Theme.paddingLarge) 50 | color: Theme.primaryColor 51 | horizontalAlignment: Text.AlignHCenter 52 | wrapMode: Text.Wrap 53 | //% "Updates available" 54 | text: qsTrId("orn-cover-updates-available") 55 | } 56 | 57 | CoverActionList { 58 | id: coverAction 59 | enabled: networkManager.connected 60 | 61 | CoverAction { 62 | iconSource: "image://theme/icon-cover-search" 63 | onTriggered: _activatePage("SearchPage", function(p) { 64 | p.reset() 65 | }) 66 | } 67 | 68 | CoverAction { 69 | iconSource: "image://theme/icon-cover-" + (OrnPm.updatesAvailable ? "next" : "sync") 70 | onTriggered: { 71 | if (OrnPm.updatesAvailable) { 72 | _activatePage("InstalledAppsPage") 73 | } else if (!OrnPm.refreshingCache) { 74 | OrnPm.refreshRepos() 75 | } 76 | } 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /qml/cover/background.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /qml/models/DevelopersModel.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | 3 | ListModel { 4 | 5 | ListElement { 6 | //% "Developers" 7 | role: qsTrId("orn-developers") 8 | participants: [ 9 | ListElement { 10 | name: "Petr Tsymbarovich (mentaljam / osetr)" 11 | link: "https://github.com/mentaljam" 12 | }, 13 | ListElement { 14 | name: "olf (Olf0)" 15 | link: "https://github.com/Olf0" 16 | }, 17 | ListElement { 18 | name: "Peter G. (nephros)" 19 | link: "https://github.com/nephros" 20 | }, 21 | ListElement { 22 | name: "Matti Viljanen (direc85)" 23 | link: "https://github.com/direc85" 24 | }, 25 | ListElement { 26 | name: "Björn Bidar (Thaodan)" 27 | link: "https://github.com/Thaodan" 28 | }, 29 | ListElement { 30 | name: "citronalco" 31 | link: "https://github.com/citronalco" 32 | }, 33 | ListElement { 34 | name: "Dmitry Gerasimov (dseight)" 35 | link: "https://github.com/dseight" 36 | }, 37 | ListElement { 38 | name: "elros34" 39 | link: "https://github.com/elros34" 40 | }, 41 | ListElement { 42 | name: "Christoph (inta)" 43 | link: "https://github.com/inta" 44 | }, 45 | ListElement { 46 | name: "Miklós Márton (martonmiklos)" 47 | link: "https://github.com/martonmiklos" 48 | } 49 | ] 50 | } 51 | 52 | ListElement { 53 | role: "OpenRepos" 54 | participants: [ 55 | ListElement { 56 | name: "Basil Semuonov (custodian / thecust)" 57 | link: "https://github.com/custodian" 58 | } 59 | ] 60 | } 61 | 62 | ListElement { 63 | //% "Application icon" 64 | role: qsTrId("orn-appicon") 65 | participants: [ 66 | ListElement { 67 | name: "Laurent Chambon (Laurent_C)" 68 | link: "mailto:l.chambon@gmail.com" 69 | } 70 | ] 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /qml/models/DummyCommentsModel.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | 3 | ListModel { 4 | 5 | ListElement { 6 | userId: 1 7 | created: qsTrId("orn-just-now") 8 | commentId: 2 9 | //: https://simple.wikipedia.org/wiki/42_(answer) 10 | //% "Deep Thought" 11 | userName: qsTrId("orn-dcm-user2") 12 | text: "42" 13 | parentId: 1 14 | parentUserName: qsTrId("orn-dcm-user1") 15 | } 16 | 17 | ListElement { 18 | userId: 0 19 | //% "7.5 million years ago" 20 | created: qsTrId("orn-hint-commentdelegate-created") 21 | commentId: 1 22 | //: https://simple.wikipedia.org/wiki/42_(answer) 23 | //% "A little white mouse" 24 | userName: qsTrId("orn-dcm-user1") 25 | //: https://simple.wikipedia.org/wiki/42_(answer) 26 | //% "What is the Answer to the Ultimate Question of Life, the Universe, and Everything?" 27 | text: qsTrId("orn-dcm-question") 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /qml/pages/AboutPage.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import Sailfish.Silica 1.0 3 | 4 | Page { 5 | allowedOrientations: defaultAllowedOrientations 6 | 7 | SilicaFlickable { 8 | anchors.fill: parent 9 | contentHeight: column.height + Theme.paddingMedium 10 | 11 | Column { 12 | id: column 13 | width: parent.width 14 | spacing: Theme.paddingMedium 15 | 16 | PageHeader { 17 | id: header 18 | //% "About Storeman" 19 | title: qsTrId("orn-about") 20 | } 21 | 22 | Item { 23 | height: icon.height + Theme.paddingMedium 24 | width: parent.width 25 | 26 | Image { 27 | id: icon 28 | anchors.horizontalCenter: parent.horizontalCenter 29 | source: "/usr/share/icons/hicolor/480x480/apps/harbour-storeman.png" 30 | } 31 | } 32 | 33 | Label { 34 | anchors.horizontalCenter: parent.horizontalCenter 35 | color: Theme.highlightColor 36 | text: applicationDisplayName + " " + Qt.application.version 37 | } 38 | 39 | Label { 40 | anchors { 41 | left: parent.left 42 | right: parent.right 43 | leftMargin: Theme.horizontalPageMargin 44 | rightMargin: Theme.horizontalPageMargin 45 | } 46 | height: implicitHeight + Theme.paddingMedium 47 | color: Theme.highlightColor 48 | linkColor: Theme.primaryColor 49 | font.pixelSize: Theme.fontSizeSmall 50 | wrapMode: Text.WordWrap 51 | horizontalAlignment: Qt.AlignHCenter 52 | //% "

OpenRepos client application for SailfishOS
 

" 53 | //% "

Storeman is Free Software (FLOSS), distributed under the terms of the MIT license.
 

" 54 | //% "

Any issues (bug reports, feature suggestions, help requests etc.) shall be filed at GitHub (you may use the button below).

" 55 | text: qsTrId("orn-app-description-full").arg("https://github.com/storeman-developers/harbour-storeman/raw/master/LICENSE") 56 | onLinkActivated: Qt.openUrlExternally(link) 57 | } 58 | 59 | ButtonLayout { 60 | width: parent.width 61 | 62 | Button { 63 | text: "@GitHub" 64 | onClicked: Qt.openUrlExternally("https://github.com/storeman-developers") 65 | } 66 | 67 | Button { 68 | text: "@OpenRepos" 69 | onClicked: { 70 | pageStack.push(Qt.resolvedUrl("AppPage.qml"), { 71 | appId: 11621 72 | }) 73 | } 74 | } 75 | 76 | Button { 77 | //% "Donation" 78 | text: qsTrId("orn-donation") 79 | onClicked: Qt.openUrlExternally("https://openrepos.net/donate") 80 | } 81 | 82 | Button { 83 | text: qsTrId("orn-translations") 84 | onClicked: pageStack.push(Qt.resolvedUrl("TranslationsPage.qml")) 85 | } 86 | 87 | Button { 88 | text: qsTrId("orn-development") 89 | onClicked: pageStack.push(Qt.resolvedUrl("DevelopmentPage.qml")) 90 | } 91 | } 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /qml/pages/AuthorisationDialog.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import Sailfish.Silica 1.0 3 | import harbour.orn 1.0 4 | 5 | Dialog { 6 | id: dialog 7 | objectName: "AuthorisationDialog" 8 | allowedOrientations: defaultAllowedOrientations 9 | canAccept: networkManager.connected && 10 | usernameField.text && 11 | usernameField.acceptableInput && 12 | passwordField.acceptableInput 13 | 14 | onAccepted: OrnClient.login(usernameField.text, passwordField.text, savePasswordSwitch.checked) 15 | 16 | DialogHeader { 17 | id: header 18 | //% "Log in" 19 | acceptText: qsTrId("orn-login-action") 20 | } 21 | 22 | SilicaFlickable { 23 | anchors { 24 | left: parent.left 25 | top: header.bottom 26 | right: parent.right 27 | bottom: parent.bottom 28 | } 29 | clip: true 30 | contentHeight: content.height 31 | 32 | Column { 33 | id: content 34 | width: parent.width 35 | 36 | Label { 37 | anchors { 38 | left: parent.left 39 | right: parent.right 40 | leftMargin: Theme.horizontalPageMargin 41 | rightMargin: Theme.horizontalPageMargin 42 | } 43 | height: implicitHeight + Theme.paddingLarge 44 | verticalAlignment: Qt.AlignVCenter 45 | //% "Log in to OpenRepos.net to comment applications and " 46 | //% "reply to others comments.

" 47 | //% "Storeman does not send your password to third-parties." 48 | text: qsTrId("orn-login-help") 49 | color: Theme.highlightColor 50 | font.pixelSize: Theme.fontSizeSmall 51 | wrapMode: Text.WordWrap 52 | } 53 | 54 | // Spacer 55 | Item { 56 | width: 1 57 | height: Theme.paddingMedium 58 | } 59 | 60 | TextField { 61 | id: usernameField 62 | width: parent.width 63 | //: A translated string should comprise less than 27 characters 64 | //% "Username" 65 | placeholderText: qsTrId("orn-username") 66 | label: placeholderText 67 | validator: RegExpValidator { 68 | regExp: new RegExp([ 69 | /([a-zA-Z0-9_]{1,}|(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*/, 70 | /|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*/, 71 | /")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:/, 72 | /(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}(?:(2(5[0-5]|[0-4][0-9])|/, 73 | /1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|/, 74 | /\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\]))/ 75 | ].map(function(r) {return r.source}).join('')) 76 | } 77 | inputMethodHints: Qt.ImhNoPredictiveText | Qt.ImhNoAutoUppercase 78 | 79 | EnterKey.iconSource: "image://theme/icon-m-enter-next" 80 | EnterKey.enabled: text && acceptableInput 81 | EnterKey.onClicked: passwordField.forceActiveFocus() 82 | 83 | Component.onCompleted: text = OrnClient.userName 84 | } 85 | 86 | PasswordField { 87 | id: passwordField 88 | width: parent.width 89 | validator: RegExpValidator { regExp: /^.{1,}$/ } 90 | 91 | EnterKey.iconSource: "image://theme/icon-m-enter-close" 92 | EnterKey.onClicked: focus = false 93 | } 94 | 95 | TextSwitch { 96 | id: savePasswordSwitch 97 | width: parent.width 98 | //% "Save password" 99 | text: qsTrId("orn-save-password") 100 | //% "Save password to the encrypted device storage to perform automatic re-login." 101 | description: qsTrId("orn-save-password-help") 102 | } 103 | } 104 | 105 | VerticalScrollDecorator { } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /qml/pages/BackupDialog.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import QtQml 2.2 3 | import Sailfish.Silica 1.0 4 | import harbour.orn 1.0 5 | import "../components" 6 | 7 | 8 | Dialog { 9 | property OrnBackup backup 10 | // This is need for restoring options after orientation changed 11 | property QtObject backupOptions: QtObject { 12 | property string filename: "Storeman-%1.ini".arg(Qt.formatDateTime(new Date(), "yyyyMMddhhmmss")) 13 | property bool repos: true 14 | property bool installed: true 15 | property bool bookmarks: true 16 | } 17 | 18 | id: dialog 19 | allowedOrientations: defaultAllowedOrientations 20 | canAccept: loader.item.acceptable && ( 21 | backupOptions.bookmarks || 22 | backupOptions.repos || 23 | backupOptions.installed 24 | ) 25 | 26 | onAccepted: { 27 | var items = 0 28 | if (backupOptions.repos) { 29 | items = items | OrnBackup.BackupRepos 30 | } 31 | if (backupOptions.installed) { 32 | items = items | OrnBackup.BackupInstalled 33 | } 34 | if (backupOptions.bookmarks) { 35 | items = items | OrnBackup.BackupBookmarks 36 | } 37 | backup.backup(loader.item.path, items) 38 | } 39 | 40 | Component { 41 | id: portraitLayout 42 | 43 | SilicaFlickable { 44 | property alias path: options.path 45 | property alias acceptable: options.acceptable 46 | 47 | contentHeight: options.height + label.height 48 | clip: true 49 | 50 | BackupOptions { 51 | id: options 52 | } 53 | 54 | BackupLabel { 55 | id: label 56 | anchors { 57 | top: options.bottom 58 | topMargin: Theme.paddingLarge 59 | left: parent.left 60 | right: parent.right 61 | leftMargin: Theme.horizontalPageMargin 62 | rightMargin: Theme.horizontalPageMargin 63 | } 64 | } 65 | 66 | VerticalScrollDecorator { } 67 | } 68 | } 69 | 70 | Component { 71 | id: landscapeLayout 72 | 73 | Item { 74 | property alias path: options.path 75 | property alias acceptable: options.acceptable 76 | 77 | clip: true 78 | 79 | SilicaFlickable { 80 | anchors { 81 | top: parent.top 82 | right: parent.horizontalCenter 83 | bottom: parent.bottom 84 | left: parent.left 85 | } 86 | contentHeight: label.height 87 | 88 | BackupLabel { 89 | id: label 90 | anchors { 91 | left: parent.left 92 | right: parent.right 93 | margins: Theme.horizontalPageMargin 94 | } 95 | } 96 | } 97 | 98 | SilicaFlickable { 99 | anchors { 100 | top: parent.top 101 | right: parent.right 102 | bottom: parent.bottom 103 | left: parent.horizontalCenter 104 | } 105 | contentHeight: options.height 106 | 107 | BackupOptions { 108 | id: options 109 | } 110 | } 111 | } 112 | } 113 | 114 | DialogHeader { 115 | id: header 116 | //% "Backup" 117 | acceptText: qsTrId("orn-backup") 118 | } 119 | 120 | Loader { 121 | id: loader 122 | anchors { 123 | top: header.bottom 124 | right: parent.right 125 | bottom: parent.bottom 126 | left: parent.left 127 | } 128 | 129 | sourceComponent: dialog.isPortrait ? portraitLayout : landscapeLayout 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /qml/pages/BookmarksPage.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import Sailfish.Silica 1.0 3 | import harbour.orn 1.0 4 | import "../components" 5 | 6 | Page { 7 | id: page 8 | allowedOrientations: defaultAllowedOrientations 9 | 10 | OrnBookmarksModel { 11 | id: bookmarksModel 12 | } 13 | 14 | Connections { 15 | target: bookmarksModel 16 | onRowsInserted: proxyModel.sort(Qt.AscendingOrder) 17 | } 18 | 19 | SilicaListView { 20 | id: bookmarksList 21 | anchors.fill: parent 22 | model: OrnProxyModel { 23 | id: proxyModel 24 | sortRole: OrnBookmarksModel.SortRole 25 | sortCaseSensitivity: Qt.CaseInsensitive 26 | sourceModel: bookmarksModel 27 | } 28 | 29 | header: PageHeader { 30 | //% "Bookmarks" 31 | title: qsTrId("orn-bookmarks") 32 | } 33 | 34 | section { 35 | property: "title" 36 | criteria: ViewSection.FirstCharacter 37 | delegate: SectionHeader { 38 | text: section.toUpperCase() 39 | } 40 | } 41 | 42 | delegate: AppListDelegate {} 43 | 44 | PullDownMenu { 45 | id: menu 46 | 47 | RefreshMenuItem { 48 | model: bookmarksModel 49 | } 50 | 51 | MenuSearchItem {} 52 | 53 | MenuStatusLabel {} 54 | } 55 | 56 | VerticalScrollDecorator {} 57 | 58 | BusyIndicator { 59 | size: BusyIndicatorSize.Large 60 | anchors.centerIn: parent 61 | running: bookmarksModel.fetching 62 | } 63 | 64 | ViewPlaceholder { 65 | enabled: !bookmarksModel.fetching && bookmarksList.count === 0 66 | //% "Your bookmarked applications will be shown here" 67 | text: qsTrId("orn-no-bookmarks") 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /qml/pages/CategoriesFilterPage.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import Sailfish.Silica 1.0 3 | import harbour.orn 1.0 4 | import "../components" 5 | 6 | Page { 7 | allowedOrientations: defaultAllowedOrientations 8 | 9 | onStatusChanged: { 10 | if (status === PageStatus.Active) { 11 | categoriesList.model = categoriesModel 12 | } 13 | } 14 | 15 | OrnCategoriesModel { 16 | id: categoriesModel 17 | } 18 | 19 | Connections { 20 | target: networkManager 21 | onStateChanged: { 22 | if (categoriesList.count === 0 && 23 | networkManager.connected) { 24 | categoriesModel.reset() 25 | } 26 | } 27 | } 28 | 29 | SilicaListView { 30 | id: categoriesList 31 | anchors.fill: parent 32 | 33 | header: PageHeader { 34 | //% "Categories filter" 35 | title: qsTrId("orn-categories-filter") 36 | //% "Select which categories to show" 37 | description: qsTrId("orn-categories-filter-descr") 38 | } 39 | 40 | delegate: CategoriesFilterDelegate { 41 | categoryVisible: model.visible 42 | height: Theme.itemSizeExtraSmall 43 | text: model.name 44 | depth: model.depth 45 | textAlignment: Text.AlignLeft 46 | 47 | onClicked: { 48 | const setVisible = !model.visible 49 | OrnClient.toggleCategoryVisibility(model.categoryId) 50 | for (var i in model.children) { 51 | OrnClient.setCategoryVisibility(model.children[i], setVisible) 52 | } 53 | } 54 | } 55 | 56 | PullDownMenu { 57 | MenuStatusLabel {} 58 | } 59 | 60 | VerticalScrollDecorator {} 61 | 62 | BusyIndicator { 63 | size: BusyIndicatorSize.Large 64 | anchors.centerIn: parent 65 | running: !viewPlaceholder.text && 66 | categoriesList.count === 0 67 | } 68 | 69 | ViewPlaceholder { 70 | id: viewPlaceholder 71 | enabled: text 72 | text: networkManager.connected ? "" : qsTrId("orn-network-idle") 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /qml/pages/CategoriesPage.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import Sailfish.Silica 1.0 3 | import harbour.orn 1.0 4 | import "../components" 5 | 6 | Page { 7 | allowedOrientations: defaultAllowedOrientations 8 | 9 | onStatusChanged: { 10 | if (status === PageStatus.Active) { 11 | categoriesList.model = categoriesModel 12 | } 13 | } 14 | 15 | OrnProxyModel { 16 | id: categoriesModel 17 | sourceModel: OrnCategoriesModel {} 18 | filterRole: OrnCategoriesModel.VisibilityRole 19 | } 20 | 21 | Connections { 22 | target: networkManager 23 | onStateChanged: { 24 | if (categoriesList.count === 0 && 25 | networkManager.connected) { 26 | categoriesModel.reset() 27 | } 28 | } 29 | } 30 | 31 | SilicaListView { 32 | id: categoriesList 33 | anchors.fill: parent 34 | 35 | header: PageHeader { 36 | //% "Categories" 37 | title: qsTrId("orn-categories") 38 | } 39 | 40 | delegate: MoreButton { 41 | height: Theme.itemSizeExtraSmall 42 | text: model.name 43 | depth: model.depth 44 | textAlignment: Text.AlignLeft 45 | 46 | onClicked: pageStack.push(Qt.resolvedUrl("CategoryPage.qml"), { 47 | categoryId: model.categoryId, 48 | categoryName: model.name 49 | }) 50 | } 51 | 52 | PullDownMenu { 53 | MenuSearchItem {} 54 | MenuStatusLabel {} 55 | } 56 | 57 | VerticalScrollDecorator {} 58 | 59 | BusyIndicator { 60 | size: BusyIndicatorSize.Large 61 | anchors.centerIn: parent 62 | running: !viewPlaceholder.text && 63 | categoriesList.count === 0 64 | } 65 | 66 | ViewPlaceholder { 67 | id: viewPlaceholder 68 | enabled: text 69 | text: networkManager.connected ? "" : qsTrId("orn-network-idle") 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /qml/pages/CategoryPage.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import Sailfish.Silica 1.0 3 | import harbour.orn 1.0 4 | import "../components" 5 | 6 | Page { 7 | property int categoryId 8 | property string categoryName 9 | 10 | allowedOrientations: defaultAllowedOrientations 11 | 12 | OrnAppsModel { 13 | id: categoryModel 14 | resource: "categories/%0/apps".arg(categoryId) 15 | onFetchingChanged: { 16 | if (!fetching && rowCount() === 0) { 17 | //% "Currently there are no apps in this category" 18 | viewPlaceholder.text = qsTrId("orn-category-noapps") 19 | } 20 | } 21 | } 22 | 23 | SilicaListView { 24 | id: categoryList 25 | anchors.fill: parent 26 | 27 | header: PageHeader { 28 | title: categoryName 29 | } 30 | 31 | model: networkManager.connected ? categoryModel : null 32 | 33 | delegate: AppListDelegate { } 34 | 35 | PullDownMenu { 36 | id: menu 37 | 38 | RefreshMenuItem { 39 | model: categoryModel 40 | } 41 | 42 | MenuSearchItem {} 43 | 44 | MenuStatusLabel {} 45 | } 46 | 47 | VerticalScrollDecorator {} 48 | 49 | BusyIndicator { 50 | size: BusyIndicatorSize.Large 51 | anchors.centerIn: parent 52 | running: !viewPlaceholder.enabled && 53 | categoryList.count === 0 && 54 | !menu.active 55 | } 56 | 57 | ViewPlaceholder { 58 | id: viewPlaceholder 59 | enabled: text 60 | text: { 61 | hintText = "" 62 | if (!networkManager.connected) { 63 | return qsTrId("orn-network-idle") 64 | } 65 | if (categoryModel.networkError) { 66 | hintText = qsTrId("orn-pull-refresh") 67 | return qsTrId("orn-network-error") 68 | } 69 | return "" 70 | } 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /qml/pages/ChangelogPage.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import Sailfish.Silica 1.0 3 | import "../components" 4 | 5 | Page { 6 | property string appName 7 | property string appIconSource 8 | property alias changelog: label.text 9 | 10 | allowedOrientations: defaultAllowedOrientations 11 | 12 | SilicaFlickable { 13 | anchors.fill: parent 14 | contentHeight: content.height 15 | 16 | Column { 17 | id: content 18 | width: parent.width 19 | 20 | FancyPageHeader { 21 | id: header 22 | title: qsTrId("orn-changelog") 23 | description: appName 24 | iconSource: appIconSource 25 | } 26 | 27 | Label { 28 | id: label 29 | anchors { 30 | left: parent.left 31 | right: parent.right 32 | leftMargin: Theme.horizontalPageMargin 33 | rightMargin: Theme.horizontalPageMargin 34 | } 35 | color: Theme.primaryColor 36 | linkColor: Theme.highlightColor 37 | font.pixelSize: Theme.fontSizeSmall 38 | wrapMode: Text.WordWrap 39 | onLinkActivated: Qt.openUrlExternally(link) 40 | } 41 | } 42 | 43 | VerticalScrollDecorator { } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /qml/pages/DevelopmentPage.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import Sailfish.Silica 1.0 3 | import "../models" 4 | import "../components" 5 | 6 | Page { 7 | allowedOrientations: defaultAllowedOrientations 8 | 9 | SilicaListView { 10 | anchors.fill: parent 11 | model: DevelopersModel { } 12 | 13 | header: PageHeader { 14 | //% "Credits" 15 | title: qsTrId("orn-development") 16 | } 17 | 18 | delegate: Item { 19 | width: parent.width 20 | height: column.height + Theme.paddingLarge 21 | 22 | Column { 23 | id: column 24 | x: Theme.horizontalPageMargin 25 | width: parent.width - Theme.horizontalPageMargin * 2 26 | anchors.verticalCenter: parent.verticalCenter 27 | spacing: Theme.paddingSmall 28 | 29 | Label { 30 | width: parent.width 31 | wrapMode: Text.WordWrap 32 | horizontalAlignment: Qt.AlignHCenter 33 | color: Theme.highlightColor 34 | text: role 35 | } 36 | 37 | Column { 38 | width: parent.width 39 | 40 | Repeater { 41 | model: participants 42 | delegate: Label { 43 | width: parent.width 44 | wrapMode: Text.WordWrap 45 | horizontalAlignment: Qt.AlignHCenter 46 | color: Theme.secondaryColor 47 | linkColor: Theme.primaryColor 48 | font.pixelSize: Theme.fontSizeSmall 49 | text: link ? "%1".arg(link).arg(name) : name 50 | onLinkActivated: Qt.openUrlExternally(link) 51 | } 52 | } 53 | } 54 | } 55 | } 56 | 57 | PullDownMenu { 58 | 59 | MenuItem { 60 | //% "Source code & Issue tracker" 61 | text: qsTrId("orn-sources") 62 | onClicked: Qt.openUrlExternally("https://github.com/storeman-developers/harbour-storeman") 63 | } 64 | 65 | MenuStatusLabel { } 66 | } 67 | 68 | VerticalScrollDecorator { } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /qml/pages/ErrorPage.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import Sailfish.Silica 1.0 3 | 4 | Page { 5 | property alias message: messageLabel.text 6 | 7 | objectName: "ErrorPage" 8 | allowedOrientations: defaultAllowedOrientations 9 | 10 | SilicaFlickable { 11 | anchors.fill: parent 12 | contentHeight: column.height 13 | 14 | Column { 15 | id: column 16 | width: parent.width 17 | 18 | PageHeader { 19 | title: qsTrId("orn-error") 20 | } 21 | 22 | Label { 23 | id: messageLabel 24 | anchors { 25 | left: parent.left 26 | right: parent.right 27 | leftMargin: Theme.horizontalPageMargin 28 | rightMargin: Theme.horizontalPageMargin 29 | } 30 | color: Theme.highlightColor 31 | wrapMode: Text.WordWrap 32 | } 33 | } 34 | 35 | VerticalScrollDecorator { } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /qml/pages/IntervalPickerDialog.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import Sailfish.Silica 1.0 3 | import "../components" 4 | 5 | Dialog { 6 | property alias hour: hoursView.currentIndex 7 | property alias minute: minutesView.currentIndex 8 | 9 | id: dialog 10 | canAccept: hour > 0 || minute >= 10 11 | 12 | DialogHeader { 13 | id: header 14 | //% "At least 10 minutes" 15 | title: qsTrId("orn-updates-check-interval-minimum") 16 | palette.highlightColor: canAccept ? Theme.highlightColor : Theme.errorColor 17 | } 18 | 19 | IntervalView { 20 | id: hoursView 21 | anchors { 22 | top: header.bottom 23 | right: parent.horizontalCenter 24 | bottom: parent.bottom 25 | left: parent.left 26 | bottomMargin: Theme.paddingLarge 27 | } 28 | model: 48 29 | } 30 | 31 | IntervalView { 32 | id: minutesView 33 | anchors { 34 | top: header.bottom 35 | right: parent.right 36 | bottom: parent.bottom 37 | left: parent.horizontalCenter 38 | bottomMargin: Theme.paddingLarge 39 | } 40 | model: 60 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /qml/pages/RecentAppsPage.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import Sailfish.Silica 1.0 3 | import harbour.orn 1.0 4 | import "../components" 5 | 6 | Page { 7 | property OrnProxyModel model 8 | 9 | id: page 10 | allowedOrientations: defaultAllowedOrientations 11 | 12 | SilicaListView { 13 | id: appsList 14 | anchors.fill: parent 15 | model: networkManager.connected ? page.model : null 16 | 17 | header: PageHeader { 18 | //% "Recently updated" 19 | title: qsTrId("orn-recently-updated") 20 | } 21 | 22 | delegate: AppListDelegate { } 23 | 24 | section { 25 | property: "sinceUpdate" 26 | delegate: SectionHeader { 27 | text: section 28 | } 29 | } 30 | 31 | PullDownMenu { 32 | id: menu 33 | 34 | RefreshMenuItem { 35 | model: page.model 36 | } 37 | 38 | MenuSearchItem {} 39 | 40 | MenuStatusLabel {} 41 | } 42 | 43 | VerticalScrollDecorator {} 44 | 45 | BusyIndicator { 46 | size: BusyIndicatorSize.Large 47 | anchors.centerIn: parent 48 | running: !viewPlaceholder.text && 49 | appsList.count === 0 && 50 | !menu.active 51 | } 52 | 53 | ViewPlaceholder { 54 | id: viewPlaceholder 55 | enabled: text 56 | text: { 57 | hintText = "" 58 | if (!networkManager.connected) { 59 | //% "Network is unavailable" 60 | return qsTrId("orn-network-idle") 61 | } 62 | if (page.model.sourceModel.networkError) { 63 | //% "Pull down to refresh" 64 | hintText = qsTrId("orn-pull-refresh") 65 | //% "A network error occurred" 66 | return qsTrId("orn-network-error") 67 | } 68 | return "" 69 | } 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /qml/pages/RepositoryPage.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import Sailfish.Silica 1.0 3 | import harbour.orn 1.0 4 | import "../components" 5 | 6 | Page { 7 | property int userId 8 | property string userName 9 | property string userIcon 10 | property int previousAppId: -1 11 | 12 | id: page 13 | allowedOrientations: defaultAllowedOrientations 14 | 15 | SilicaListView { 16 | id: appsList 17 | anchors.fill: parent 18 | 19 | header: FancyPageHeader { 20 | id: header 21 | //% "Repository" 22 | title: qsTrId("orn-repository") 23 | description: userName 24 | iconSource: userIcon 25 | } 26 | 27 | model: OrnProxyModel { 28 | id: proxyModel 29 | sortRole: OrnAppsModel.SortRole 30 | filterRole: OrnAppsModel.VisibilityRole 31 | sourceModel: OrnAppsModel { 32 | id: appsModel 33 | fetchable: false 34 | resource: "users/%0/apps".arg(userId) 35 | onRowsInserted: proxyModel.sort(Qt.AscendingOrder) 36 | } 37 | } 38 | 39 | delegate: AppListDelegate { 40 | returnToUser: true 41 | showUser: false 42 | previousAppId: page.previousAppId 43 | } 44 | 45 | PullDownMenu { 46 | id: menu 47 | 48 | RefreshMenuItem { 49 | model: appsModel 50 | } 51 | 52 | MenuSearchItem {} 53 | 54 | MenuStatusLabel {} 55 | } 56 | 57 | VerticalScrollDecorator {} 58 | 59 | BusyIndicator { 60 | size: BusyIndicatorSize.Large 61 | anchors.centerIn: parent 62 | running: !viewPlaceholder.text && 63 | appsList.count === 0 && 64 | !menu.active 65 | } 66 | 67 | ViewPlaceholder { 68 | id: viewPlaceholder 69 | enabled: text 70 | text: { 71 | hintText = "" 72 | if (!networkManager.connected) { 73 | return qsTrId("orn-network-idle") 74 | } 75 | if (appsModel.networkError) { 76 | hintText = qsTrId("orn-pull-refresh") 77 | return qsTrId("orn-network-error") 78 | } 79 | return "" 80 | } 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /qml/pages/RestoreDialog.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import QtQml 2.2 3 | import Sailfish.Silica 1.0 4 | import harbour.orn 1.0 5 | 6 | Dialog { 7 | property OrnBackup backup 8 | property string filePath 9 | readonly property var _details: backup.details(filePath) 10 | 11 | id: dialog 12 | allowedOrientations: defaultAllowedOrientations 13 | canAccept: networkManager.connected 14 | 15 | onAccepted: backup.restore(filePath) 16 | 17 | DialogHeader { 18 | id: header 19 | //% "Restore from a file" 20 | title: qsTrId("orn-restore-title") 21 | //% "Restore" 22 | acceptText: qsTrId("orn-restore") 23 | } 24 | 25 | SilicaFlickable { 26 | anchors { 27 | top: header.bottom 28 | left: parent.left 29 | right: parent.right 30 | bottom: parent.bottom 31 | } 32 | contentHeight: column.height 33 | 34 | Column { 35 | id: column 36 | width: parent.width 37 | 38 | Label { 39 | anchors { 40 | left: parent.left 41 | right: parent.right 42 | margins: Theme.horizontalPageMargin 43 | } 44 | color: Theme.highlightColor 45 | wrapMode: Text.WordWrap 46 | font.pixelSize: Theme.fontSizeSmall 47 | //% "Restore OpenRepos repositories and installed apps from the selected file. This action will not affect your current repositories and will not remove installed applications." 48 | text: qsTrId("orn-restore-hint") 49 | } 50 | 51 | SectionHeader { 52 | //% "Details" 53 | text: qsTrId("orn-details") 54 | } 55 | 56 | DetailItem { 57 | //% "Created" 58 | label: qsTrId("orn-created") 59 | value: _details.created.toLocaleString(Qt.locale(), Locale.NarrowFormat) 60 | } 61 | 62 | DetailItem { 63 | //% "Total repositories" 64 | label: qsTrId("orn-total-repos") 65 | value: _details.repos 66 | } 67 | 68 | DetailItem { 69 | //% "Installed packages" 70 | label: qsTrId("orn-installed-packages") 71 | value: _details.packages 72 | } 73 | 74 | DetailItem { 75 | label: qsTrId("orn-bookmarks") 76 | value: _details.bookmarks 77 | } 78 | } 79 | 80 | VerticalScrollDecorator { } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /qml/pages/ScreenshotPage.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import Sailfish.Silica 1.0 3 | 4 | FullscreenContentPage { 5 | property alias model: slideshow.model 6 | property alias currentIndex: slideshow.currentIndex 7 | 8 | id: page 9 | allowedOrientations: defaultAllowedOrientations 10 | 11 | SlideshowView { 12 | id: slideshow 13 | anchors.fill: parent 14 | 15 | delegate: Item { 16 | width: slideshow.width 17 | height: slideshow.height 18 | 19 | BusyIndicator { 20 | anchors.centerIn: parent 21 | size: BusyIndicatorSize.Large 22 | running: image.status === Image.Loading 23 | color: Theme.highlightColor 24 | } 25 | 26 | Image { 27 | id: image 28 | anchors.fill: parent 29 | source: modelData.url 30 | fillMode: Image.PreserveAspectFit 31 | opacity: status === Image.Ready ? 1.0 : 0.0 32 | 33 | Behavior on opacity { FadeAnimation { } } 34 | } 35 | 36 | MouseArea { 37 | anchors.fill: parent 38 | onClicked: pageStack.pop() 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /qml/pages/SearchPage.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import Sailfish.Silica 1.0 3 | import harbour.orn 1.0 4 | import "../components" 5 | 6 | Page { 7 | property string initialSearch 8 | 9 | function reset() { 10 | searchModel.searchKey = "" 11 | //% "Search results will be shown here" 12 | viewPlaceholder.text = qsTrId("orn-searchpage-placeholder-default") 13 | //% "Type some keywords in the field above" 14 | viewPlaceholder.hintText = qsTrId("orn-searchpage-placeholder-default-hint") 15 | view.headerItem.searchField.forceActiveFocus() 16 | } 17 | 18 | function _search(text) { 19 | searchModel.searchKey = text 20 | viewPlaceholder.text = "" 21 | viewPlaceholder.hintText = "" 22 | forceActiveFocus() 23 | } 24 | 25 | id: page 26 | allowedOrientations: defaultAllowedOrientations 27 | 28 | onStatusChanged: { 29 | if (status === PageStatus.Active) { 30 | if (initialSearch) { 31 | view.headerItem.searchField.text = initialSearch 32 | _search(initialSearch) 33 | } else if (!view.headerItem.searchField.text) { 34 | reset() 35 | } 36 | } 37 | } 38 | 39 | SilicaListView 40 | { 41 | id: view 42 | anchors.fill: parent 43 | model: OrnProxyModel { 44 | id: proxyModel 45 | sourceModel: OrnSearchAppsModel { 46 | id: searchModel 47 | onFetchingChanged: { 48 | if (!fetching && proxyModel.rowCount() === 0) { 49 | //% "Nothing found" 50 | viewPlaceholder.text = qsTrId("orn-searchpage-placeholder-noresults") 51 | //% "Try to change search keywords" 52 | viewPlaceholder.hintText = qsTrId("orn-searchpage-placeholder-noresults-hint") 53 | } 54 | } 55 | } 56 | filterRole: OrnSearchAppsModel.VisibilityRole 57 | } 58 | 59 | header: Column { 60 | property alias searchField: searchField 61 | 62 | width: parent.width 63 | 64 | PageHeader { 65 | //: The search menu item and the search page header text - should be a noun 66 | //% "Search" 67 | title: qsTrId("orn-search") 68 | } 69 | 70 | SearchField { 71 | id: searchField 72 | width: parent.width 73 | //: The search field placeholder text - should be a verb 74 | //% "Search" 75 | placeholderText: qsTrId("orn-searchfield-placeholder") 76 | 77 | EnterKey.enabled: text.length > 0 78 | EnterKey.iconSource: "image://theme/icon-m-enter-accept" 79 | EnterKey.onClicked: _search(text) 80 | 81 | onTextChanged: if (!text) reset() 82 | } 83 | } 84 | 85 | delegate: AppListDelegate { } 86 | 87 | VerticalScrollDecorator { } 88 | 89 | ViewPlaceholder { 90 | id: viewPlaceholder 91 | enabled: text 92 | } 93 | 94 | BusyIndicator { 95 | id: busyIndicator 96 | size: BusyIndicatorSize.Large 97 | anchors.centerIn: parent 98 | running: parent.count === 0 && !viewPlaceholder.enabled 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /qml/pages/SharePage.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import Sailfish.Silica 1.0 3 | import Sailfish.TransferEngine 1.0 4 | 5 | SharePage { 6 | id: page 7 | 8 | property string link 9 | property string linkTitle 10 | 11 | //% "Share link" 12 | header: qsTrId("orn-share-link") 13 | mimeType: "text/x-url" 14 | showAddAccount: false 15 | content: { 16 | "type": "text/x-url", 17 | "status": page.link, 18 | "linkTitle": page.linkTitle 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /qml/pages/TagAppsPage.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import Sailfish.Silica 1.0 3 | import harbour.orn 1.0 4 | import "../components" 5 | 6 | Page { 7 | property int tagId 8 | property string tagName 9 | property int previousAppId: -1 10 | 11 | id: page 12 | allowedOrientations: defaultAllowedOrientations 13 | 14 | SilicaListView { 15 | id: appsList 16 | anchors.fill: parent 17 | 18 | model: OrnProxyModel { 19 | id: proxyModel 20 | sortRole: OrnAppsModel.SortRole 21 | filterRole: OrnAppsModel.VisibilityRole 22 | sourceModel: OrnAppsModel { 23 | id: appsModel 24 | resource: tagId !== 0 ? "tags/%0/apps".arg(tagId) : "" 25 | onRowsInserted: proxyModel.sort(Qt.AscendingOrder) 26 | } 27 | } 28 | 29 | header: PageHeader { 30 | id: header 31 | //% "Tagged Applications" 32 | title: qsTrId("orn-tag-apps") 33 | description: tagName 34 | } 35 | 36 | delegate: AppListDelegate { 37 | previousAppId: page.previousAppId 38 | previousStep: 2 39 | } 40 | 41 | PullDownMenu { 42 | id: menu 43 | 44 | RefreshMenuItem { 45 | model: appsModel 46 | } 47 | 48 | MenuStatusLabel { } 49 | } 50 | 51 | VerticalScrollDecorator { } 52 | 53 | BusyIndicator { 54 | size: BusyIndicatorSize.Large 55 | anchors.centerIn: parent 56 | running: !viewPlaceholder.text && 57 | appsList.count === 0 && 58 | !menu.active 59 | } 60 | 61 | ViewPlaceholder { 62 | id: viewPlaceholder 63 | enabled: text 64 | text: { 65 | hintText = "" 66 | if (!networkManager.connected) { 67 | return qsTrId("orn-network-idle") 68 | } 69 | if (appsModel.networkError) { 70 | hintText = qsTrId("orn-pull-refresh") 71 | return qsTrId("orn-network-error") 72 | } 73 | return "" 74 | } 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /qml/pages/TagsPage.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import Sailfish.Silica 1.0 3 | import harbour.orn 1.0 4 | import "../components" 5 | 6 | 7 | Page { 8 | property alias tagIds: tagsModel.tagIds 9 | property string appName 10 | property string appIconSource 11 | property int previousAppId: -1 12 | 13 | id: page 14 | allowedOrientations: defaultAllowedOrientations 15 | 16 | SilicaListView { 17 | id: tagsList 18 | anchors.fill: parent 19 | model: OrnProxyModel { 20 | id: proxyModel 21 | sortRole: OrnTagsModel.AppsCountRole 22 | sourceModel: OrnTagsModel { 23 | id: tagsModel 24 | onRowsInserted: proxyModel.sort(Qt.DescendingOrder) 25 | } 26 | } 27 | 28 | header: FancyPageHeader { 29 | id: header 30 | //% "Tags" 31 | title: qsTrId("orn-tags") 32 | description: appName 33 | iconSource: appIconSource 34 | } 35 | 36 | delegate: TagDelegate { } 37 | 38 | PullDownMenu { 39 | id: menu 40 | 41 | RefreshMenuItem { 42 | model: tagsModel 43 | } 44 | 45 | MenuStatusLabel { } 46 | } 47 | 48 | VerticalScrollDecorator { } 49 | 50 | BusyIndicator { 51 | size: BusyIndicatorSize.Large 52 | anchors.centerIn: parent 53 | running: tagsModel.fetching && !menu.active 54 | } 55 | 56 | ViewPlaceholder { 57 | enabled: text 58 | text: { 59 | hintText = "" 60 | if (!networkManager.connected) { 61 | return qsTrId("orn-network-idle") 62 | } 63 | if (tagsModel.networkError) { 64 | hintText = qsTrId("orn-pull-refresh") 65 | return qsTrId("orn-network-error") 66 | } 67 | return "" 68 | } 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /qml/pages/TranslationsPage.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import Sailfish.Silica 1.0 3 | import "../components" 4 | import "../models" 5 | 6 | Page { 7 | allowedOrientations: defaultAllowedOrientations 8 | 9 | SilicaListView { 10 | anchors.fill: parent 11 | model: TranslatorsModel { } 12 | 13 | header: PageHeader { 14 | //% "Translations" 15 | title: qsTrId("orn-translations") 16 | } 17 | 18 | delegate: Item { 19 | width: parent.width 20 | height: column.height + Theme.paddingLarge 21 | 22 | Column { 23 | id: column 24 | x: Theme.horizontalPageMargin 25 | width: parent.width - Theme.horizontalPageMargin * 2 26 | anchors.verticalCenter: parent.verticalCenter 27 | spacing: Theme.paddingSmall 28 | 29 | Label { 30 | width: parent.width 31 | wrapMode: Text.WordWrap 32 | horizontalAlignment: Qt.AlignHCenter 33 | color: Theme.highlightColor 34 | text: { 35 | var l = Qt.locale(locale) 36 | var res = l.nativeLanguageName 37 | res = res.charAt(0).toUpperCase() + res.slice(1) 38 | if (locale.length > 2) { 39 | res += " (%0)".arg(l.nativeCountryName) 40 | } 41 | return res 42 | } 43 | } 44 | 45 | ParticipantsDelegate { 46 | //% "Coordinators" 47 | title: qsTrId("orn-coordinators") 48 | model: coordinators 49 | } 50 | 51 | ParticipantsDelegate { 52 | //% "Translators" 53 | title: qsTrId("orn-translators") 54 | model: translators 55 | } 56 | 57 | ParticipantsDelegate { 58 | //% "Reviewers" 59 | title: qsTrId("orn-reviewers") 60 | model: reviewers 61 | } 62 | } 63 | } 64 | 65 | PullDownMenu { 66 | 67 | MenuItem { 68 | text: "Storeman@Transifex" 69 | onClicked: Qt.openUrlExternally("https://app.transifex.com/mentaljam/harbour-storeman") 70 | } 71 | 72 | MenuStatusLabel { } 73 | } 74 | 75 | VerticalScrollDecorator { } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /qml/pages/UnusedReposDialog.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import Sailfish.Silica 1.0 3 | import harbour.orn 1.0 4 | 5 | Dialog { 6 | property alias repos: listView.model 7 | property var _reposToRemove: repos.reduce(function(res, author) { 8 | res[author] = true 9 | return res 10 | }, {}) 11 | 12 | function _setRepo(author, remove) { 13 | if (remove) { 14 | _reposToRemove[author] = remove 15 | } else { 16 | delete _reposToRemove[author] 17 | } 18 | } 19 | 20 | id: dialog 21 | allowedOrientations: defaultAllowedOrientations 22 | 23 | Component.onCompleted: console.log(_reposToRemove) 24 | 25 | onAccepted: Object.keys(_reposToRemove).forEach(removeAuthorRepo) 26 | 27 | SilicaListView { 28 | id: listView 29 | anchors.fill: parent 30 | 31 | header: Column { 32 | width: parent.width 33 | 34 | DialogHeader { 35 | id: header 36 | acceptText: qsTrId("orn-remove") 37 | //% "Unused repositories" 38 | title: qsTrId("orn-unused-repos") 39 | } 40 | 41 | Label { 42 | anchors { 43 | left: parent.left 44 | right: parent.right 45 | leftMargin: Theme.horizontalPageMargin 46 | rightMargin: Theme.horizontalPageMargin 47 | } 48 | color: Theme.highlightColor 49 | wrapMode: Text.WordWrap 50 | textFormat: Text.RichText 51 | //% "

There are no installed packages for the next repositories.

" 52 | //% "

Do you want to remove them now?

" 53 | text: qsTrId("orn-unused-repos-text") 54 | } 55 | } 56 | 57 | delegate: TextSwitch { 58 | checked: true 59 | text: modelData 60 | onCheckedChanged: _setRepo(modelData, checked) 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /qml/qmldir: -------------------------------------------------------------------------------- 1 | module harbour.orn 2 | singleton StoremanStyles 1.0 StoremanStyles.qml 3 | -------------------------------------------------------------------------------- /rpm/harbour-storeman.rpmlintrc: -------------------------------------------------------------------------------- 1 | # References: An exceptionally comprehensive example rpmlintrc file 2 | # https://github.com/coreos/tectonic-rpms/blob/master/rpmlint-config 3 | # but adheres to the old syntax, not the new TOML one: https://toml.io/en/ 4 | # See also https://fedoraproject.org/wiki/Common_Rpmlint_issues and 5 | # https://en.opensuse.org/openSUSE:Packaging_checks#Building_Packages_in_spite_of_errors 6 | 7 | # On behalf of Jolla's tar_git / SailfishOS-OBS: 8 | # - It re-writes the DistURL, rendering it inconsistent 9 | addFilter('invalid-url DistURL obs:') 10 | # - It has a limited list of FLOSS-licenses, most SDPX-IDs are missing 11 | addFilter('invalid-license') 12 | # - It extracts strange changelog entries out of Git, if a %%changelog section is used 13 | addFilter('incoherent-version-in-changelog') 14 | # - It sometimes re-writes the %version-%release strings of package names, 15 | # when referencing (only) a branch (i.e., not a git tag), for example, 16 | # 0.5.2-1 to 0.5.2+main.20230129011931.1.g584263a-1.8.1.jolla 17 | addFilter('filename-too-long-for-joliet') 18 | 19 | # On behalf of the SailfishOS:Chum metadata definition: 20 | # - which often forces one to do 21 | addFilter('description-line-too-long') 22 | setBadness('description-line-too-long', 0) 23 | 24 | # On our own behalf: 25 | # - This is how it ought to be 26 | addFilter('obsolete-not-provided harbour-storeman-installer') 27 | addFilter('unversioned Obsoletes: Obsoletes: *harbour-storeman-installer') 28 | # - This is also how it ought to be 29 | addFilter('dangerous-command-in-%post[un]* rm') 30 | 31 | -------------------------------------------------------------------------------- /scripts/update_categories.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from urllib import request 4 | import json 5 | import re 6 | 7 | URL = 'https://openrepos.net/api/v1/categories' 8 | PATTERN = re.compile(r'[ &]+') 9 | 10 | def process(json_array): 11 | for o in json_array: 12 | tid = o['tid'].rjust(4) 13 | name = o['name'] 14 | trid = PATTERN.sub('-', name).lower() 15 | print(f'//% "{name}"\n{{ {tid}, QT_TRID_NOOP("orn-cat-{trid}") }},') 16 | if 'childrens' in o: 17 | process(o['childrens']) 18 | 19 | if __name__ == '__main__': 20 | response = request.urlopen(URL) 21 | categories = json.loads(response.read().decode('UTF-8')) 22 | process(categories) 23 | -------------------------------------------------------------------------------- /scripts/update_translators.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from os import path 4 | import sys 5 | import configparser 6 | from getpass import getpass 7 | import requests 8 | import json 9 | 10 | # This API is deprectated and does not work any longer: 11 | URL = 'http://www.transifex.com/api/2/project/harbour-storeman/languages/' 12 | # Either use Transifex's Python library: https://developers.transifex.com/reference/api-python-sdk 13 | # Or use Transifex's REST API: https://developers.transifex.com/reference/get_languages 14 | 15 | def credentials(): 16 | '''Get Transifex user name and password''' 17 | trc_path = path.join(path.expanduser('~'), '.transifexrc') 18 | if path.isfile(trc_path): 19 | print('Fetching credentials from', trc_path) 20 | trc = configparser.ConfigParser() 21 | try: 22 | trc.read(trc_path) 23 | section = trc['https://www.transifex.com'] 24 | return section['username'], section['password'] 25 | except Exception as e: 26 | print('Error reading .transifexrc:', e) 27 | sys.exit(1) 28 | else: 29 | return ( 30 | input('Transifex username: '), 31 | getpass(prompt='Transifex password: ') 32 | ) 33 | 34 | def participants(writer, name, tr): 35 | writer.write(f' {name}: [') 36 | arr = tr[name] 37 | last_i = len(arr) - 1 38 | for i, p in enumerate(arr): 39 | writer.write(f'\n ListElement {{ name: "{p}" }}') 40 | writer.write(',' if i < last_i else '\n ') 41 | writer.write(']\n') 42 | 43 | if __name__ == '__main__': 44 | auth = credentials() 45 | print('Fetching', URL) 46 | data = requests.get(URL, auth=auth).content 47 | data = json.loads(data.decode('UTF-8')) 48 | model_file = path.normpath(path.dirname(__file__) + '/../qml/models/TranslatorsModel.qml') 49 | print('Writing', model_file) 50 | with open(model_file, 'w', encoding='UTF-8') as writer: 51 | writer.write('import QtQuick 2.0\n\nListModel {') 52 | for tr in data: 53 | writer.write('\n ListElement {\n') 54 | writer.write(f' locale: "{tr["language_code"]}"\n') 55 | participants(writer, 'coordinators', tr) 56 | participants(writer, 'translators', tr) 57 | participants(writer, 'reviewers', tr) 58 | writer.write(' }\n') 59 | writer.write('}\n') 60 | -------------------------------------------------------------------------------- /src/harbour-storeman.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | #include "ornclient.h" 8 | #include "ornpm.h" 9 | #include "ornapplication.h" 10 | #include "ornappsmodel.h" 11 | #include "ornsearchappsmodel.h" 12 | #include "ornrepomodel.h" 13 | #include "orninstalledappsmodel.h" 14 | #include "ornproxymodel.h" 15 | #include "orncommentsmodel.h" 16 | #include "orncategoriesmodel.h" 17 | #include "orntagsmodel.h" 18 | #include "ornbookmarksmodel.h" 19 | #include "ornbackup.h" 20 | #include "networkaccessmanagerfactory.h" 21 | #include "storeman.h" 22 | #include 23 | 24 | 25 | void registerTypes() 26 | { 27 | #ifndef QT_DEBUG 28 | auto uri = "harbour.orn"; 29 | #else 30 | # define uri "harbour.orn" 31 | #endif 32 | 33 | qmlRegisterType (uri, 1, 0, "OrnApplication"); 34 | qmlRegisterType (uri, 1, 0, "OrnAppsModel"); 35 | qmlRegisterType (uri, 1, 0, "OrnSearchAppsModel"); 36 | qmlRegisterType (uri, 1, 0, "OrnRepoModel"); 37 | qmlRegisterType(uri, 1, 0, "OrnInstalledAppsModel"); 38 | qmlRegisterType (uri, 1, 0, "OrnProxyModel"); 39 | qmlRegisterType (uri, 1, 0, "OrnCommentsModel"); 40 | qmlRegisterType (uri, 1, 0, "OrnCategoriesModel"); 41 | qmlRegisterType (uri, 1, 0, "OrnTagsModel"); 42 | qmlRegisterType (uri, 1, 0, "OrnBookmarksModel"); 43 | qmlRegisterType (uri, 1, 0, "OrnBackup"); 44 | 45 | qmlRegisterSingletonType (uri, 1, 0, "OrnClient", OrnClient::qmlInstance); 46 | qmlRegisterSingletonType (uri, 1, 0, "OrnPm", OrnPm::qmlInstance); 47 | qmlRegisterSingletonType (uri, 1, 0, "Storeman", Storeman::qmlInstance); 48 | 49 | qRegisterMetaType>(); 50 | qRegisterMetaType>(); 51 | } 52 | 53 | int main(int argc, char *argv[]) 54 | { 55 | registerTypes(); 56 | 57 | SailfishApp::application(argc, argv); 58 | QGuiApplication::setApplicationVersion(QStringLiteral(STOREMAN_VERSION)); 59 | // QGuiApplication::setApplicationDisplayName(QStringLiteral("Storeman")); (available in Qt.application object since Qt 5.9) cannot be used, 60 | // because SFOS 4.4.0 still deploys QT 5.6 and Storeman supports SFOS ≥ 3.1.0! 61 | // For a detailed discussion see https://github.com/storeman-developers/harbour-storeman/issues/356#issuecomment-1192249781 62 | 63 | auto view = SailfishApp::createView(); 64 | 65 | NetworkAccessManagerFactory factory; 66 | view->engine()->setNetworkAccessManagerFactory(&factory); 67 | 68 | view->setSource(SailfishApp::pathToMainQml()); 69 | view->show(); 70 | 71 | return QGuiApplication::exec(); 72 | } 73 | -------------------------------------------------------------------------------- /src/networkaccessmanagerfactory.cpp: -------------------------------------------------------------------------------- 1 | #include "networkaccessmanagerfactory.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | 9 | QNetworkAccessManager *NetworkAccessManagerFactory::create(QObject *parent) 10 | { 11 | auto manager = new QNetworkAccessManager(parent); 12 | auto diskCache = new QNetworkDiskCache(manager); 13 | diskCache->setCacheDirectory(QStandardPaths::writableLocation(QStandardPaths::CacheLocation) 14 | .append("/network")); 15 | qDebug() << "Setting qml network cache dir to" << diskCache->cacheDirectory(); 16 | manager->setCache(diskCache); 17 | return manager; 18 | } 19 | -------------------------------------------------------------------------------- /src/networkaccessmanagerfactory.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | class NetworkAccessManagerFactory : public QQmlNetworkAccessManagerFactory 6 | { 7 | public: 8 | NetworkAccessManagerFactory() = default; 9 | 10 | // QQmlNetworkAccessManagerFactory interface 11 | public: 12 | QNetworkAccessManager *create(QObject *parent) override; 13 | }; 14 | -------------------------------------------------------------------------------- /src/ornapplistitem.cpp: -------------------------------------------------------------------------------- 1 | #include "ornapplistitem.h" 2 | #include "orncategorylistitem.h" 3 | #include "ornutils.h" 4 | #include "ornconst.h" 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | 12 | QString sinceLabel(quint32 value) 13 | { 14 | auto curDate = QDate::currentDate(); 15 | auto date = QDateTime::fromMSecsSinceEpoch(qint64(value) * 1000).date(); 16 | auto days = date.daysTo(curDate); 17 | if (days == 0) 18 | { 19 | //% "Today" 20 | return qtTrId("orn-today"); 21 | } 22 | if (days == 1) 23 | { 24 | //% "Yesterday" 25 | return qtTrId("orn-yesterday"); 26 | } 27 | if (days < 7 && date.dayOfWeek() < curDate.dayOfWeek()) 28 | { 29 | //% "This week" 30 | return qtTrId("orn-this-week"); 31 | } 32 | if (days < curDate.daysInMonth() && date.day() < curDate.day()) 33 | { 34 | //% "This month" 35 | return qtTrId("orn-this-month"); 36 | } 37 | //: Output format for the month and year - %0 is a long month name and %1 is a year (for example "May 2017") 38 | //% "%0 %1" 39 | return qtTrId("orn-month-format").arg( 40 | QDate::longMonthName(date.month(), QDate::StandaloneFormat)).arg(date.year()); 41 | } 42 | 43 | OrnAppListItem::OrnAppListItem(const QJsonObject &data) 44 | : valid(data.size() > 1) 45 | , appId(data[OrnConst::appid].toVariant().toUInt()) 46 | , title(OrnUtils::toString(data[OrnConst::title])) 47 | , iconSource(OrnUtils::toString(data[OrnConst::icon].toObject()[OrnConst::url])) 48 | { 49 | auto created = OrnUtils::toUint(data[OrnConst::created]); 50 | if (created > 0) 51 | { 52 | createDate.setMSecsSinceEpoch(qint64(created) * 1000); 53 | sinceUpdate = sinceLabel(created); 54 | } 55 | 56 | auto ratingobj = data[OrnConst::rating].toObject(); 57 | ratingCount = OrnUtils::toUint(ratingobj[OrnConst::count]); 58 | rating = ratingobj[OrnConst::rating].toString().toFloat(); 59 | 60 | userName = OrnUtils::toString(data[OrnConst::user].toObject()[OrnConst::name]); 61 | 62 | auto categories = data[OrnConst::category].toArray(); 63 | auto tid = OrnUtils::toUint(categories.last().toObject()[OrnConst::tid]); 64 | category = OrnCategoryListItem::categoryName(tid); 65 | categoryId = tid; 66 | 67 | package = OrnUtils::toString(data[OrnConst::package].toObject()[OrnConst::name]); 68 | 69 | if (iconSource.isEmpty() || 70 | iconSource.endsWith(QStringLiteral("icon-defaultpackage.png"))) 71 | { 72 | iconSource = QStringLiteral("image://theme/icon-launcher-default"); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/ornapplistitem.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | class QJsonObject; 7 | 8 | struct OrnAppListItem 9 | { 10 | OrnAppListItem(const QJsonObject &data); 11 | 12 | bool valid; 13 | quint32 appId; 14 | quint32 ratingCount; 15 | float rating; 16 | QString title; 17 | QString userName; 18 | QString iconSource; 19 | QString sinceUpdate; 20 | QString category; 21 | quint32 categoryId; 22 | QString package; 23 | QDateTime createDate; 24 | }; 25 | -------------------------------------------------------------------------------- /src/ornappsmodel.cpp: -------------------------------------------------------------------------------- 1 | #include "ornappsmodel.h" 2 | #include "ornapplistitem.h" 3 | #include "ornpm.h" 4 | #include "ornclient.h" 5 | 6 | 7 | OrnAppsModel::OrnAppsModel(bool fetchable, QObject *parent) 8 | : OrnAbstractListModel(fetchable, parent) 9 | { 10 | connect(OrnPm::instance(), &OrnPm::packageStatusChanged, 11 | this, [this](const QString &packageName, int status) 12 | { 13 | Q_UNUSED(status) 14 | 15 | auto size = mData.size(); 16 | for (size_t i = 0; i < size; ++i) 17 | { 18 | if (mData[i].package == packageName) 19 | { 20 | auto ind = this->createIndex(int(i), 0); 21 | emit this->dataChanged(ind, ind, {PackageStatusRole}); 22 | return; 23 | } 24 | } 25 | }); 26 | 27 | connect(OrnClient::instance(), &OrnClient::bookmarkChanged, 28 | this, [this](quint32 appid, bool bookmarked) 29 | { 30 | Q_UNUSED(bookmarked) 31 | 32 | for (size_t i = 0, size = mData.size(); i < size; ++i) 33 | { 34 | if (mData[i].appId == appid) 35 | { 36 | auto ind = this->createIndex(int(i), 0); 37 | emit this->dataChanged(ind, ind, {BookmarkRole}); 38 | return; 39 | } 40 | } 41 | }); 42 | 43 | connect(OrnClient::instance(), &OrnClient::categoryVisibilityChanged, 44 | this, [this](quint32 categoryId, bool visible) 45 | { 46 | Q_UNUSED(visible) 47 | 48 | for (size_t i = 0, size = mData.size(); i < size; ++i) 49 | { 50 | if (mData[i].categoryId == categoryId) 51 | { 52 | auto ind = this->createIndex(int(i), 0); 53 | emit this->dataChanged(ind, ind, {VisibilityRole}); 54 | return; 55 | } 56 | } 57 | }); 58 | } 59 | 60 | void OrnAppsModel::setFetchable(bool fetchable) 61 | { 62 | if (mFetchable != fetchable) 63 | { 64 | mFetchable = fetchable; 65 | emit this->fetchableChanged(); 66 | this->reset(); 67 | } 68 | } 69 | 70 | void OrnAppsModel::setResource(const QString &resource) 71 | { 72 | if (mResource != resource) 73 | { 74 | mResource = resource; 75 | emit this->resourceChanged(); 76 | this->reset(); 77 | } 78 | } 79 | 80 | QVariant OrnAppsModel::data(const QModelIndex &index, int role) const 81 | { 82 | if (!index.isValid()) 83 | { 84 | return QVariant(); 85 | } 86 | 87 | const auto &app = mData[size_t(index.row())]; 88 | switch (role) 89 | { 90 | case SortRole: 91 | return app.title.toLower(); 92 | case ValidityRole: 93 | return app.valid; 94 | case BookmarkRole: 95 | return OrnClient::instance()->hasBookmark(app.appId); 96 | case PackageStatusRole: 97 | return OrnPm::instance()->packageStatus(app.package); 98 | case AppIdRole: 99 | return app.appId; 100 | case CreateDateRole: 101 | return app.createDate; 102 | case RatingCountRole: 103 | return app.ratingCount; 104 | case RatingRole: 105 | return app.rating; 106 | case TitleRole: 107 | return app.title; 108 | case UserNameRole: 109 | return app.userName; 110 | case IconSourceRole: 111 | return app.iconSource; 112 | case SinceUpdateRole: 113 | return app.sinceUpdate; 114 | case CategoryRole: 115 | return app.category; 116 | case CategoryIdRole: 117 | return app.categoryId; 118 | case VisibilityRole: 119 | return OrnClient::instance()->categoryVisible(app.categoryId); 120 | default: 121 | return QVariant(); 122 | } 123 | } 124 | 125 | QHash OrnAppsModel::roleNames() const 126 | { 127 | return { 128 | { ValidityRole, "isValid" }, 129 | { BookmarkRole, "isBookmarked" }, 130 | { PackageStatusRole, "packageStatus" }, 131 | { AppIdRole, "appId" }, 132 | { CreateDateRole, "createDate" }, 133 | { RatingCountRole, "ratingCount" }, 134 | { RatingRole, "rating" }, 135 | { TitleRole, "title" }, 136 | { UserNameRole, "userName" }, 137 | { IconSourceRole, "iconSource" }, 138 | { SinceUpdateRole, "sinceUpdate" }, 139 | { CategoryRole, "category" }, 140 | { CategoryIdRole, "categoryId" }, 141 | { VisibilityRole, "visible" } 142 | }; 143 | } 144 | 145 | void OrnAppsModel::fetchMore(const QModelIndex &parent) 146 | { 147 | if (parent.isValid() || mResource.isEmpty()) 148 | { 149 | return; 150 | } 151 | OrnAbstractListModel::fetch(mResource); 152 | } 153 | -------------------------------------------------------------------------------- /src/ornappsmodel.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "ornabstractlistmodel.h" 4 | #include "ornapplistitem.h" 5 | 6 | class OrnAppsModel : public OrnAbstractListModel 7 | { 8 | Q_OBJECT 9 | Q_PROPERTY(bool fetchable READ fetchable WRITE setFetchable NOTIFY fetchableChanged) 10 | Q_PROPERTY(QString resource READ resource WRITE setResource NOTIFY resourceChanged) 11 | 12 | public: 13 | 14 | enum Role 15 | { 16 | SortRole = Qt::UserRole, 17 | ValidityRole, 18 | BookmarkRole, 19 | PackageStatusRole, 20 | AppIdRole, 21 | CreateDateRole, 22 | RatingCountRole, 23 | RatingRole, 24 | TitleRole, 25 | UserNameRole, 26 | IconSourceRole, 27 | SinceUpdateRole, 28 | CategoryRole, 29 | CategoryIdRole, 30 | VisibilityRole, 31 | }; 32 | Q_ENUM(Role) 33 | 34 | OrnAppsModel(bool fetchable = true, QObject *parent = nullptr); 35 | 36 | bool fetchable() const 37 | { return mFetchable; } 38 | 39 | void setFetchable(bool fetchable); 40 | 41 | QString resource() const 42 | { return mResource; } 43 | 44 | void setResource(const QString &resource); 45 | 46 | signals: 47 | void fetchableChanged(); 48 | void resourceChanged(); 49 | 50 | private: 51 | QString mResource; 52 | 53 | // QAbstractItemModel interface 54 | public: 55 | QVariant data(const QModelIndex &index, int role) const override; 56 | QHash roleNames() const override; 57 | void fetchMore(const QModelIndex &parent) override; 58 | }; 59 | -------------------------------------------------------------------------------- /src/ornbackup.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | class OrnBackup : public QObject 8 | { 9 | Q_OBJECT 10 | Q_PROPERTY(Status status READ status NOTIFY statusChanged) 11 | 12 | public: 13 | 14 | enum BackupItem 15 | { 16 | BackupRepos = 0x001, 17 | BackupInstalled = 0x002, 18 | BackupBookmarks = 0x004, 19 | }; 20 | Q_ENUM(BackupItem) 21 | Q_DECLARE_FLAGS(BackupItems, BackupItem) 22 | Q_FLAGS(BackupItems) 23 | 24 | enum Status 25 | { 26 | Idle, 27 | BackingUp, 28 | RestoringBookmarks, 29 | RestoringRepos, 30 | RefreshingRepos, 31 | SearchingPackages, 32 | InstallingPackages, 33 | }; 34 | Q_ENUM(Status) 35 | 36 | enum Error 37 | { 38 | NoError, 39 | DirectoryError, 40 | }; 41 | Q_ENUM(Error) 42 | 43 | explicit OrnBackup(QObject *parent = nullptr); 44 | 45 | Status status() const; 46 | 47 | Q_INVOKABLE static QVariantMap details(const QString &path); 48 | Q_INVOKABLE void backup(const QString &filePath, OrnBackup::BackupItems items); 49 | Q_INVOKABLE void restore(const QString &filePath); 50 | Q_INVOKABLE QStringList notFound() const; 51 | 52 | signals: 53 | void statusChanged(); 54 | void backupError(OrnBackup::Error err); 55 | void backedUp(); 56 | void restored(); 57 | 58 | private slots: 59 | void pSearchPackages(); 60 | void pAddPackage(quint32 info, const QString &packageId, const QString &summary); 61 | void pInstallPackages(); 62 | void pFinishRestore(); 63 | 64 | private: 65 | void setStatus(OrnBackup::Status status); 66 | void pBackup(const QString &filePath, BackupItems items); 67 | void pRestore(const QString &filePath); 68 | void pRefreshRepos(); 69 | 70 | private: 71 | Status mStatus{Idle}; 72 | QStringList mNamesToSearch; 73 | // Name, version 74 | QHash mInstalled; 75 | QMultiHash mPackagesToInstall; 76 | }; 77 | -------------------------------------------------------------------------------- /src/ornbookmarksmodel.cpp: -------------------------------------------------------------------------------- 1 | #include "ornbookmarksmodel.h" 2 | #include "ornapplistitem.h" 3 | #include "ornclient.h" 4 | #include "ornconst.h" 5 | 6 | #include 7 | #include 8 | 9 | QString compactResource(quint32 appid) 10 | { 11 | return QStringLiteral("apps/%0/compact").arg(appid); 12 | } 13 | 14 | OrnBookmarksModel::OrnBookmarksModel(QObject *parent) 15 | : OrnAppsModel(false, parent) 16 | { 17 | auto client = OrnClient::instance(); 18 | connect(client, &OrnClient::bookmarkChanged, this, 19 | [this, client](quint32 appId, bool bookmarked) 20 | { 21 | if (this->canFetchMore(QModelIndex())) 22 | { 23 | // Model was not initialized yet so just ignore the signal 24 | return; 25 | } 26 | 27 | if (bookmarked) 28 | { 29 | auto request = client->apiRequest(compactResource(appId)); 30 | qDebug() << "Fetching data from" << request.url().toString(); 31 | auto reply = client->networkAccessManager()->get(request); 32 | connect(reply, &QNetworkReply::finished, [this, client, reply, appId]() 33 | { 34 | auto doc = client->processReply(reply); 35 | if (doc.isObject()) 36 | { 37 | qDebug() << "Adding app" << appId << "to bookmarks model"; 38 | QJsonArray arr({doc.object()}); 39 | this->processReply(QJsonDocument(arr)); 40 | } 41 | }); 42 | } 43 | else 44 | { 45 | for (size_t i = 0, s = mData.size(); i < s; ++i) 46 | { 47 | if (mData[i].appId == appId) 48 | { 49 | qDebug() << "Removing app" << appId << "from bookmarks model"; 50 | this->beginRemoveRows(QModelIndex(), i, i); 51 | mData.erase(mData.begin() + i); 52 | mData.shrink_to_fit(); 53 | this->endRemoveRows(); 54 | return; 55 | } 56 | } 57 | } 58 | }); 59 | } 60 | 61 | void OrnBookmarksModel::fetchMore(const QModelIndex &parent) 62 | { 63 | if (parent.isValid()) 64 | { 65 | return; 66 | } 67 | 68 | auto client = OrnClient::instance(); 69 | auto bookmarks = client->bookmarks(); 70 | auto size = bookmarks.size(); 71 | 72 | if (size == 0) 73 | { 74 | return; 75 | } 76 | 77 | mFetching = true; 78 | emit this->fetchingChanged(); 79 | 80 | for (const auto &appid : bookmarks) 81 | { 82 | auto request = client->apiRequest(compactResource(appid)); 83 | qDebug() << "Fetching data from" << request.url().toString(); 84 | auto reply = client->networkAccessManager()->get(request); 85 | connect(reply, &QNetworkReply::finished, this, 86 | [this, client, size, reply, appid]() 87 | { 88 | auto doc = client->processReply(reply); 89 | if (doc.isObject()) 90 | { 91 | mFetchedApps.append(doc.object()); 92 | } 93 | else 94 | { 95 | QJsonObject badApp; 96 | badApp.insert(OrnConst::appid, QString::number(appid)); 97 | mFetchedApps.append(badApp); 98 | } 99 | if (mFetchedApps.size() == size) 100 | { 101 | this->processReply(QJsonDocument(mFetchedApps)); 102 | mFetchedApps = QJsonArray(); 103 | mFetching = false; 104 | emit this->fetchingChanged(); 105 | } 106 | }); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/ornbookmarksmodel.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "ornappsmodel.h" 4 | 5 | class OrnBookmarksModel : public OrnAppsModel 6 | { 7 | Q_OBJECT 8 | 9 | public: 10 | explicit OrnBookmarksModel(QObject *parent = nullptr); 11 | 12 | private: 13 | QJsonArray mFetchedApps; 14 | 15 | // QAbstractItemModel interface 16 | public: 17 | void fetchMore(const QModelIndex &parent) override; 18 | }; 19 | -------------------------------------------------------------------------------- /src/orncategoriesmodel.cpp: -------------------------------------------------------------------------------- 1 | #include "orncategoriesmodel.h" 2 | #include "ornclient.h" 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include 11 | 12 | OrnCategoriesModel::OrnCategoriesModel(QObject *parent) 13 | : OrnAbstractListModel(false, parent) 14 | { 15 | connect(OrnClient::instance(), &OrnClient::categoryVisibilityChanged, 16 | this, [this](quint32 categoryId, bool visible) 17 | { 18 | Q_UNUSED(visible) 19 | 20 | for (size_t i = 0, size = mData.size(); i < size; ++i) 21 | { 22 | if (mData[i].categoryId == categoryId) 23 | { 24 | auto ind = this->createIndex(i, 0); 25 | emit this->dataChanged(ind, ind, {VisibilityRole}); 26 | return; 27 | } 28 | } 29 | }); 30 | } 31 | 32 | QVariantList childCategories(const std::deque &data, quint32 parentID) 33 | { 34 | QVariantList res; 35 | for (const auto &cat : data) 36 | { 37 | if (cat.parents.contains(parentID)) { 38 | res.append(cat.categoryId); 39 | } 40 | } 41 | return res; 42 | } 43 | 44 | QVariant OrnCategoriesModel::data(const QModelIndex &index, int role) const 45 | { 46 | if (!index.isValid()) 47 | { 48 | return QVariant(); 49 | } 50 | 51 | const auto &category = mData[index.row()]; 52 | switch (role) { 53 | case CategoryIdRole: 54 | return category.categoryId; 55 | case AppsCountRole: 56 | return category.appsCount; 57 | case DepthRole: 58 | return category.depth; 59 | case NameRole: 60 | return category.name; 61 | case VisibilityRole: 62 | return OrnClient::instance()->categoryVisible(category.categoryId); 63 | case ChildrenRole: 64 | return childCategories(mData, category.categoryId); 65 | default: 66 | return QVariant(); 67 | } 68 | } 69 | 70 | void OrnCategoriesModel::fetchMore(const QModelIndex &parent) 71 | { 72 | if (parent.isValid()) 73 | { 74 | return; 75 | } 76 | OrnAbstractListModel::fetch(QStringLiteral("categories")); 77 | } 78 | 79 | QHash OrnCategoriesModel::roleNames() const 80 | { 81 | return { 82 | { CategoryIdRole, "categoryId" }, 83 | { AppsCountRole, "appsCount" }, 84 | { DepthRole, "depth" }, 85 | { NameRole, "name" }, 86 | { VisibilityRole, "visible" }, 87 | { ChildrenRole, "children" } 88 | }; 89 | } 90 | 91 | void OrnCategoriesModel::processReply(const QJsonDocument &jsonDoc) 92 | { 93 | auto categoriesArray = jsonDoc.array(); 94 | if (categoriesArray.isEmpty()) 95 | { 96 | qWarning() << "Api reply is empty"; 97 | return; 98 | } 99 | 100 | QString childrenKey{QStringLiteral("childrens")}; 101 | std::function compare = 102 | [](const OrnCategoryListItem &a, const OrnCategoryListItem &b) -> bool 103 | { 104 | return a.name < b.name; 105 | }; 106 | std::function&, const QJsonObject&)> parse; 107 | parse = [&childrenKey, &compare, &parse](std::deque &data, const QJsonObject &jsonObject) 108 | { 109 | data.emplace_back(jsonObject); 110 | if (jsonObject.contains(childrenKey)) 111 | { 112 | auto childrenArray = jsonObject[childrenKey].toArray(); 113 | for (const QJsonValueRef child : childrenArray) 114 | { 115 | parse(data, child.toObject()); 116 | } 117 | auto end = data.end(); 118 | std::sort(end - childrenArray.size(), end, compare); 119 | } 120 | }; 121 | 122 | for (const QJsonValueRef category : categoriesArray) 123 | { 124 | parse(mData, category.toObject()); 125 | } 126 | 127 | auto size = mData.size(); 128 | this->beginInsertRows(QModelIndex(), 0, size - 1); 129 | this->endInsertRows(); 130 | qDebug() << size << "items have been added to the model"; 131 | mFetching = false; 132 | mCanFetchMore = false; 133 | emit this->fetchingChanged(); 134 | } 135 | -------------------------------------------------------------------------------- /src/orncategoriesmodel.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "ornabstractlistmodel.h" 4 | #include "orncategorylistitem.h" 5 | 6 | /** 7 | * @brief The categories model class 8 | * This will work properly only if api response contains sorted categories 9 | */ 10 | class OrnCategoriesModel : public OrnAbstractListModel 11 | { 12 | Q_OBJECT 13 | public: 14 | enum Role 15 | { 16 | CategoryIdRole = Qt::UserRole, 17 | AppsCountRole, 18 | DepthRole, 19 | NameRole, 20 | VisibilityRole, 21 | ChildrenRole, 22 | }; 23 | Q_ENUM(Role) 24 | 25 | explicit OrnCategoriesModel(QObject *parent = nullptr); 26 | 27 | // QAbstractItemModel interface 28 | public: 29 | QVariant data(const QModelIndex &index, int role) const override; 30 | void fetchMore(const QModelIndex &parent) override; 31 | QHash roleNames() const override; 32 | 33 | // OrnAbstractListModel interface 34 | protected: 35 | void processReply(const QJsonDocument &jsonDoc) override; 36 | }; 37 | -------------------------------------------------------------------------------- /src/orncategorylistitem.cpp: -------------------------------------------------------------------------------- 1 | #include "orncategorylistitem.h" 2 | #include "ornutils.h" 3 | #include "ornconst.h" 4 | 5 | #include 6 | #include 7 | #include 8 | 9 | #include 10 | 11 | using categories_t = const QMap; 12 | 13 | Q_GLOBAL_STATIC_WITH_ARGS(categories_t, categories, ({ 14 | //% "Coding Competition" 15 | { 3092, QT_TRID_NOOP("orn-cat-coding-competition") }, 16 | //% "Applications" 17 | { 1, QT_TRID_NOOP("orn-cat-applications") }, 18 | //% "Application" 19 | { 257, QT_TRID_NOOP("orn-cat-application") }, 20 | //% "Adult Content" 21 | { 4206, QT_TRID_NOOP("orn-cat-adult-content") }, 22 | //% "Ambience & Themes" 23 | { 1845, QT_TRID_NOOP("orn-cat-ambience-themes") }, 24 | //% "Business" 25 | { 2, QT_TRID_NOOP("orn-cat-business") }, 26 | //% "City guides & maps" 27 | { 3, QT_TRID_NOOP("orn-cat-city-guides-maps") }, 28 | //% "Education & Science" 29 | { 1324, QT_TRID_NOOP("orn-cat-education-science") }, 30 | //% "Entertainment" 31 | { 4, QT_TRID_NOOP("orn-cat-entertainment") }, 32 | //% "Music" 33 | { 5, QT_TRID_NOOP("orn-cat-music") }, 34 | //% "Network" 35 | { 8, QT_TRID_NOOP("orn-cat-network") }, 36 | //% "News & info" 37 | { 6, QT_TRID_NOOP("orn-cat-news-info") }, 38 | //% "Patches" 39 | { 2983, QT_TRID_NOOP("orn-cat-patches") }, 40 | //% "Photo & video" 41 | { 7, QT_TRID_NOOP("orn-cat-photo-video") }, 42 | //% "Public Transport" 43 | { 3755, QT_TRID_NOOP("orn-cat-public-transport") }, 44 | //% "Social Networks" 45 | { 9, QT_TRID_NOOP("orn-cat-social-networks") }, 46 | //% "Sports" 47 | { 10, QT_TRID_NOOP("orn-cat-sports") }, 48 | //% "System" 49 | { 147, QT_TRID_NOOP("orn-cat-system") }, 50 | //% "Unknown" 51 | { 250, QT_TRID_NOOP("orn-cat-unknown") }, 52 | //% "Utilities" 53 | { 11, QT_TRID_NOOP("orn-cat-utilities") }, 54 | //% "Games" 55 | { 12, QT_TRID_NOOP("orn-cat-games") }, 56 | //% "Game" 57 | { 256, QT_TRID_NOOP("orn-cat-game") }, 58 | //% "Action" 59 | { 13, QT_TRID_NOOP("orn-cat-action") }, 60 | //% "Adventure" 61 | { 14, QT_TRID_NOOP("orn-cat-adventure") }, 62 | //% "Arcade" 63 | { 15, QT_TRID_NOOP("orn-cat-arcade") }, 64 | //% "Card & casino" 65 | { 16, QT_TRID_NOOP("orn-cat-card-casino") }, 66 | //% "Education" 67 | { 17, QT_TRID_NOOP("orn-cat-education") }, 68 | //% "Puzzle" 69 | { 18, QT_TRID_NOOP("orn-cat-puzzle") }, 70 | //% "Sports" 71 | { 19, QT_TRID_NOOP("orn-cat-sports") }, 72 | //% "Strategy" 73 | { 20, QT_TRID_NOOP("orn-cat-strategy") }, 74 | //% "Trivia" 75 | { 21, QT_TRID_NOOP("orn-cat-trivia") }, 76 | //% "Translations" 77 | { 3413, QT_TRID_NOOP("orn-cat-translations") }, 78 | //% "Fonts" 79 | { 3155, QT_TRID_NOOP("orn-cat-fonts") }, 80 | //% "Libraries" 81 | { 247, QT_TRID_NOOP("orn-cat-libraries") }, 82 | })); 83 | 84 | static const QString KEY_DEPTH {QStringLiteral("depth")}; 85 | static const QString KEY_PARENTS {QStringLiteral("parents")}; 86 | 87 | OrnCategoryListItem::OrnCategoryListItem(const QJsonObject &data) 88 | : categoryId(OrnUtils::toUint(data[OrnConst::tid])) 89 | , appsCount(OrnUtils::toUint(data[OrnConst::appsCount])) 90 | , depth(data[KEY_DEPTH].toVariant().toUInt()) 91 | , name(categoryName(categoryId)) 92 | { 93 | auto array = data[KEY_PARENTS].toArray(); 94 | for (const QJsonValueRef v : array) 95 | { 96 | parents << OrnUtils::toUint(v); 97 | } 98 | } 99 | 100 | QString OrnCategoryListItem::categoryName(quint32 tid) 101 | { 102 | if (categories->contains(tid)) 103 | { 104 | return qtTrId((*categories)[tid]); 105 | } 106 | qWarning() << "Categories dictionary does not contain tid" 107 | << tid << "- dictionary update can be required"; 108 | //% "Unknown category" 109 | return qtTrId("orn-cat-unknown2"); 110 | } 111 | -------------------------------------------------------------------------------- /src/orncategorylistitem.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | class QJsonObject; 7 | 8 | struct OrnCategoryListItem 9 | { 10 | friend class OrnCategoriesModel; 11 | 12 | OrnCategoryListItem(const QJsonObject &data); 13 | 14 | static QString categoryName(quint32 tid); 15 | 16 | quint32 categoryId; 17 | quint32 appsCount; 18 | quint32 depth; 19 | QString name; 20 | QList parents; 21 | }; 22 | -------------------------------------------------------------------------------- /src/ornclient.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | class QQmlEngine; 7 | class QJSEngine; 8 | class QNetworkAccessManager; 9 | class QNetworkRequest; 10 | class QNetworkReply; 11 | 12 | class OrnClientPrivate; 13 | 14 | class OrnClient : public QObject 15 | { 16 | friend class OrnBackup; 17 | 18 | Q_OBJECT 19 | Q_PROPERTY(bool authorised READ authorised NOTIFY authorisedChanged) 20 | Q_PROPERTY(bool cookieIsValid READ cookieIsValid NOTIFY cookieIsValidChanged) 21 | Q_PROPERTY(bool isPublisher READ isPublisher NOTIFY authorisedChanged) 22 | Q_PROPERTY(quint32 userId READ userId NOTIFY authorisedChanged) 23 | Q_PROPERTY(QString userName READ userName NOTIFY authorisedChanged) 24 | Q_PROPERTY(QString userIconSource READ userIconSource NOTIFY authorisedChanged) 25 | 26 | public: 27 | 28 | enum Error 29 | { 30 | NetworkError, 31 | AuthorisationError, 32 | CommentSendError, 33 | CommentDeleteError, 34 | }; 35 | Q_ENUM(Error) 36 | 37 | enum CommentAction 38 | { 39 | CommentAdded, 40 | CommentEdited, 41 | CommentDeleted, 42 | }; 43 | Q_ENUM(CommentAction) 44 | 45 | static OrnClient *instance(); 46 | static inline QObject *qmlInstance(QQmlEngine *engine, QJSEngine *scriptEngine) 47 | { 48 | Q_UNUSED(engine) 49 | Q_UNUSED(scriptEngine) 50 | 51 | return OrnClient::instance(); 52 | } 53 | 54 | QNetworkRequest apiRequest(const QString &resource, const QUrlQuery &query = QUrlQuery()) const; 55 | QNetworkAccessManager *networkAccessManager() const; 56 | QJsonDocument processReply(QNetworkReply *reply, Error code = NetworkError); 57 | 58 | bool authorised() const; 59 | bool cookieIsValid() const; 60 | bool isPublisher() const; 61 | quint32 userId() const; 62 | QString userName() const; 63 | QString userIconSource(); 64 | 65 | Q_INVOKABLE QList bookmarks() const; 66 | Q_INVOKABLE bool hasBookmark(quint32 appId) const; 67 | Q_INVOKABLE bool addBookmark(quint32 appId); 68 | Q_INVOKABLE bool removeBookmark(quint32 appId); 69 | 70 | Q_INVOKABLE bool categoryVisible(quint32 categoryId) const; 71 | Q_INVOKABLE void setCategoryVisibility(quint32 categoryId, bool visible); 72 | Q_INVOKABLE void toggleCategoryVisibility(quint32 categoryId); 73 | 74 | public slots: 75 | void login(const QString &username, QString password, bool savePassword = false); 76 | void logout(); 77 | 78 | void comment(quint32 appId, const QString &body, quint32 parentId = 0); 79 | void editComment(quint32 appId, quint32 commentId, const QString &body); 80 | void deleteComment(quint32 appId, quint32 commentId); 81 | 82 | void vote(quint32 appId, quint32 value); 83 | 84 | signals: 85 | void error(OrnClient::Error code); 86 | void authorisedChanged(); 87 | void dayToExpiry(); 88 | void cookieIsValidChanged(); 89 | void commentActionFinished(OrnClient::CommentAction action, quint32 appId, quint32 cid); 90 | void bookmarkChanged(quint32 appid, bool bookmarked); 91 | void categoryVisibilityChanged(quint32 categoryId, bool visible); 92 | void userVoteFinished(quint32 appId, quint32 userVote, quint32 count, float rating); 93 | 94 | private: 95 | explicit OrnClient(QObject *parent = nullptr); 96 | 97 | Q_DECLARE_PRIVATE(OrnClient) 98 | }; 99 | -------------------------------------------------------------------------------- /src/ornclient_p.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "ornsecrets.h" 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | class QSettings; 11 | class QTimer; 12 | class QNetworkAccessManager; 13 | class OrnClient; 14 | 15 | class OrnClientPrivate : public QObjectPrivate 16 | { 17 | Q_DISABLE_COPY(OrnClientPrivate) 18 | Q_DECLARE_PUBLIC(OrnClient) 19 | 20 | public: 21 | OrnClientPrivate() = default; 22 | ~OrnClientPrivate() override; 23 | 24 | void removeUser(); 25 | bool relogin(); 26 | void setCookieTimer(); 27 | QVariant userProperty(const QString &key) const; 28 | 29 | QSettings *settings{nullptr}; 30 | QTimer *cookieTimer{nullptr}; 31 | QNetworkAccessManager *nam{nullptr}; 32 | QSet bookmarks; 33 | QSet hiddenCategories; 34 | QByteArray lang; 35 | QByteArray userToken; 36 | QNetworkCookie userCookie; 37 | OrnSecrets secrets; 38 | 39 | static void prepareComment(QJsonObject &object, const QString &body); 40 | }; 41 | -------------------------------------------------------------------------------- /src/orncommentlistitem.cpp: -------------------------------------------------------------------------------- 1 | #include "orncommentlistitem.h" 2 | #include "ornutils.h" 3 | #include "ornconst.h" 4 | 5 | #include 6 | 7 | 8 | OrnCommentListItem::OrnCommentListItem(const QJsonObject &data) 9 | : commentId(OrnUtils::toUint(data[OrnConst::cid])) 10 | , parentId(OrnUtils::toUint(data[OrnConst::pid])) 11 | , created(OrnUtils::toUint(data[OrnConst::created])) 12 | , text(OrnUtils::toString(data[OrnConst::text])) 13 | { 14 | auto user = data[OrnConst::user].toObject(); 15 | userId = OrnUtils::toUint(user[OrnConst::uid]); 16 | userName = OrnUtils::toString(user[OrnConst::name]); 17 | userIconSource = OrnUtils::toString(user[OrnConst::picture].toObject()[OrnConst::url]); 18 | } 19 | -------------------------------------------------------------------------------- /src/orncommentlistitem.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | class QJsonObject; 6 | 7 | struct OrnCommentListItem 8 | { 9 | OrnCommentListItem(const QJsonObject &data); 10 | 11 | quint32 commentId; 12 | quint32 parentId; 13 | quint32 created; 14 | quint32 userId; 15 | QString userName; 16 | QString userIconSource; 17 | QString text; 18 | }; 19 | -------------------------------------------------------------------------------- /src/orncommentsmodel.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "ornabstractlistmodel.h" 4 | #include "orncommentlistitem.h" 5 | 6 | struct OrnCommentListItem; 7 | class QNetworkReply; 8 | 9 | class OrnCommentsModel : public OrnAbstractListModel 10 | { 11 | Q_OBJECT 12 | Q_PROPERTY(quint32 appId READ appId WRITE setAppId NOTIFY appIdChanged) 13 | 14 | public: 15 | enum Role 16 | { 17 | CommentIdRole = Qt::UserRole, 18 | ParentIdRole, 19 | CreatedRole, 20 | UserIdRole, 21 | UserNameRole, 22 | ParentUserNameRole, 23 | UserIconSourceRole, 24 | TextRole 25 | }; 26 | Q_ENUM(Role) 27 | 28 | explicit OrnCommentsModel(QObject *parent = nullptr); 29 | 30 | quint32 appId() const; 31 | void setAppId(quint32 appId); 32 | 33 | Q_INVOKABLE int findItemRow(quint32 cid) const; 34 | 35 | signals: 36 | void appIdChanged(); 37 | 38 | private: 39 | QNetworkReply *fetchComment(quint32 cid); 40 | QJsonObject processReply(QNetworkReply *reply); 41 | 42 | private: 43 | quint32 mAppId{0}; 44 | 45 | // QAbstractItemModel interface 46 | public: 47 | QVariant data(const QModelIndex &index, int role) const override; 48 | void fetchMore(const QModelIndex &parent) override; 49 | QHash roleNames() const override; 50 | }; 51 | -------------------------------------------------------------------------------- /src/ornconst.cpp: -------------------------------------------------------------------------------- 1 | #include "ornconst.h" 2 | 3 | const QString OrnConst::appid {QStringLiteral("appid")}; 4 | const QString OrnConst::appsCount {QStringLiteral("apps_count")}; 5 | const QString OrnConst::body {QStringLiteral("body")}; 6 | const QString OrnConst::bookmarks {QStringLiteral("bookmarks")}; 7 | const QString OrnConst::category {QStringLiteral("category")}; 8 | const QString OrnConst::changelog {QStringLiteral("changelog")}; 9 | const QString OrnConst::cid {QStringLiteral("cid")}; 10 | const QString OrnConst::comments {QStringLiteral("comments")}; 11 | const QString OrnConst::commentsCount {QStringLiteral("comments_count")}; 12 | const QString OrnConst::commentsOpen {QStringLiteral("comments_open")}; 13 | const QString OrnConst::count {QStringLiteral("count")}; 14 | const QString OrnConst::created {QStringLiteral("created")}; 15 | const QString OrnConst::downloads {QStringLiteral("downloads")}; 16 | const QString OrnConst::icon {QStringLiteral("icon")}; 17 | const QString OrnConst::id {QStringLiteral("id")}; 18 | const QString OrnConst::installed {QStringLiteral("installed")}; 19 | const QString OrnConst::large {QStringLiteral("large")}; 20 | const QString OrnConst::mail {QStringLiteral("mail")}; 21 | const QString OrnConst::name {QStringLiteral("name")}; 22 | const QString OrnConst::package {QStringLiteral("package")}; 23 | const QString OrnConst::picture {QStringLiteral("picture")}; 24 | const QString OrnConst::pid {QStringLiteral("pid")}; 25 | const QString OrnConst::publisher {QStringLiteral("publisher")}; 26 | const QString OrnConst::rating {QStringLiteral("rating")}; 27 | const QString OrnConst::realname {QStringLiteral("realname")}; 28 | const QString OrnConst::roles {QStringLiteral("roles")}; 29 | const QString OrnConst::screenshots {QStringLiteral("screenshots")}; 30 | const QString OrnConst::tags {QStringLiteral("tags")}; 31 | const QString OrnConst::text {QStringLiteral("text")}; 32 | const QString OrnConst::thumb {QStringLiteral("thumb")}; 33 | const QString OrnConst::thumbs {QStringLiteral("thumbs")}; 34 | const QString OrnConst::tid {QStringLiteral("tid")}; 35 | const QString OrnConst::title {QStringLiteral("title")}; 36 | const QString OrnConst::uid {QStringLiteral("uid")}; 37 | const QString OrnConst::und {QStringLiteral("und")}; 38 | const QString OrnConst::updated {QStringLiteral("updated")}; 39 | const QString OrnConst::url {QStringLiteral("url")}; 40 | const QString OrnConst::user {QStringLiteral("user")}; 41 | const QString OrnConst::userVote {QStringLiteral("user_vote")}; 42 | const QString OrnConst::value {QStringLiteral("value")}; 43 | -------------------------------------------------------------------------------- /src/ornconst.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | struct OrnConst { 6 | static const QString appid; 7 | static const QString appsCount; 8 | static const QString body; 9 | static const QString bookmarks; 10 | static const QString category; 11 | static const QString changelog; 12 | static const QString cid; 13 | static const QString comments; 14 | static const QString commentsCount; 15 | static const QString commentsOpen; 16 | static const QString count; 17 | static const QString created; 18 | static const QString downloads; 19 | static const QString icon; 20 | static const QString id; 21 | static const QString installed; 22 | static const QString large; 23 | static const QString mail; 24 | static const QString name; 25 | static const QString package; 26 | static const QString picture; 27 | static const QString pid; 28 | static const QString publisher; 29 | static const QString rating; 30 | static const QString realname; 31 | static const QString roles; 32 | static const QString screenshots; 33 | static const QString tags; 34 | static const QString text; 35 | static const QString thumb; 36 | static const QString thumbs; 37 | static const QString tid; 38 | static const QString title; 39 | static const QString uid; 40 | static const QString und; 41 | static const QString updated; 42 | static const QString url; 43 | static const QString user; 44 | static const QString userVote; 45 | static const QString value; 46 | }; 47 | -------------------------------------------------------------------------------- /src/orninstalledappsmodel.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "orninstalledpackage.h" 6 | 7 | class OrnInstalledAppsModel : public QAbstractListModel 8 | { 9 | Q_OBJECT 10 | 11 | public: 12 | 13 | enum Roles 14 | { 15 | NameRole = Qt::UserRole + 1, 16 | TitleRole, 17 | TitleUnlocalizedRole, 18 | VersionRole, 19 | IconRole, 20 | SortRole, 21 | SectionRole, 22 | UpdateAvailableRole, 23 | UpdateVersionRole, 24 | IdRole 25 | }; 26 | Q_ENUM(Roles) 27 | 28 | explicit OrnInstalledAppsModel(QObject *parent = nullptr); 29 | 30 | public slots: 31 | void reset(); 32 | 33 | private slots: 34 | void onInstalledPackages(const OrnInstalledPackageList &packages); 35 | void onPackageInstalled(const QString &packageName); 36 | void onPackageRemoved(const QString &packageName); 37 | void onUpdatablePackagesChanged(); 38 | 39 | private: 40 | bool mResetting{false}; 41 | OrnInstalledPackageList mData; 42 | 43 | // QAbstractItemModel interface 44 | public: 45 | int rowCount(const QModelIndex &parent) const override; 46 | QVariant data(const QModelIndex &index, int role) const override; 47 | QHash roleNames() const override; 48 | }; 49 | -------------------------------------------------------------------------------- /src/orninstalledpackage.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | struct OrnInstalledPackage 7 | { 8 | bool updateAvailable{false}; 9 | QString id; 10 | QString name; 11 | QString title; 12 | QString titleUnlocalized; 13 | QString icon; 14 | }; 15 | 16 | using OrnInstalledPackageList = QList; 17 | 18 | Q_DECLARE_METATYPE(QList) 19 | -------------------------------------------------------------------------------- /src/ornpackageversion.cpp: -------------------------------------------------------------------------------- 1 | #include "ornpackageversion.h" 2 | 3 | #include 4 | 5 | 6 | QVariantList splitVersion(const QString &version) 7 | { 8 | QVariantList parts; 9 | static QRegularExpression sep_re(QStringLiteral("[.+~-]")); 10 | bool ok; 11 | const auto sparts = version.split(sep_re); 12 | for (const auto &s : sparts) 13 | { 14 | auto v = s.toInt(&ok); 15 | parts << (ok ? QVariant(v) : QVariant(s)); 16 | } 17 | return parts; 18 | } 19 | 20 | OrnPackageVersion::OrnPackageVersion(const QString &version) 21 | : version(version) 22 | , versionParts(splitVersion(version)) 23 | {} 24 | 25 | OrnPackageVersion::OrnPackageVersion(quint64 dsize, quint64 isize, 26 | const QString &version, QString arch, QString alias) 27 | : downloadSize(dsize) 28 | , installSize(isize) 29 | , version(version) 30 | , arch(std::move(arch)) 31 | , repoAlias(std::move(alias)) 32 | , versionParts(splitVersion(version)) 33 | {} 34 | 35 | void OrnPackageVersion::clear() 36 | { 37 | downloadSize = 0; 38 | installSize = 0; 39 | version.clear(); 40 | arch.clear(); 41 | repoAlias.clear(); 42 | versionParts.clear(); 43 | } 44 | 45 | QString OrnPackageVersion::packageId(const QString &name) const 46 | { 47 | QString id(name); 48 | QChar sep(';'); 49 | id.reserve(name.size() + version.size() + arch.size() + repoAlias.size() + 3); 50 | id.append(sep).append(version).append(sep).append(arch).append(sep).append(repoAlias); 51 | return id; 52 | } 53 | 54 | bool OrnPackageVersion::operator ==(const OrnPackageVersion &other) const 55 | { 56 | return downloadSize == other.downloadSize && 57 | installSize == other.installSize && 58 | version == other.version && 59 | arch == other.arch && 60 | repoAlias == other.repoAlias; 61 | } 62 | 63 | bool OrnPackageVersion::operator <(const OrnPackageVersion &other) const 64 | { 65 | if (versionParts == other.versionParts) 66 | { 67 | return false; 68 | } 69 | 70 | auto leftSize = versionParts.size(); 71 | auto rightSize = other.versionParts.size(); 72 | auto leftIsShorter = leftSize < rightSize; 73 | auto shorterLength = leftIsShorter ? leftSize : rightSize; 74 | 75 | // Compare the same parts 76 | for (int i = 0; i < shorterLength; ++i) 77 | { 78 | const auto &lp = versionParts[i]; 79 | const auto &rp = other.versionParts[i]; 80 | if (lp != rp) 81 | { 82 | return lp < rp; 83 | } 84 | } 85 | 86 | // If the same parts are equal then the shorter version is lesser 87 | return leftIsShorter; 88 | } 89 | -------------------------------------------------------------------------------- /src/ornpackageversion.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | struct OrnPackageVersion 6 | { 7 | quint64 downloadSize{0}; 8 | quint64 installSize{0}; 9 | QString version; 10 | QString arch; 11 | QString repoAlias; 12 | 13 | OrnPackageVersion() = default; 14 | OrnPackageVersion(const QString &version); 15 | OrnPackageVersion(quint64 dsize, quint64 isize, 16 | const QString &version, QString arch, QString alias); 17 | 18 | void clear(); 19 | 20 | QString packageId(const QString &name) const; 21 | 22 | bool operator ==(const OrnPackageVersion &other) const; 23 | inline bool operator !=(const OrnPackageVersion &other) const 24 | { return !this->operator ==(other); } 25 | 26 | bool operator <(const OrnPackageVersion &other) const; 27 | 28 | private: 29 | QVariantList versionParts; 30 | }; 31 | 32 | using OrnPackageVersionList = QList; 33 | 34 | Q_DECLARE_METATYPE(QList) 35 | -------------------------------------------------------------------------------- /src/ornpkdaemon.cpp: -------------------------------------------------------------------------------- 1 | #include "ornpkdaemon.h" 2 | #include "ornpktransaction.h" 3 | 4 | #include 5 | 6 | const QString OrnPkDaemon::serviceName{QStringLiteral("org.freedesktop.PackageKit")}; 7 | 8 | OrnPkDaemon::OrnPkDaemon(QObject *parent) 9 | : QDBusAbstractInterface( 10 | serviceName, 11 | QStringLiteral("/org/freedesktop/PackageKit"), 12 | "org.freedesktop.PackageKit", 13 | QDBusConnection::systemBus(), 14 | parent 15 | ) 16 | { 17 | 18 | } 19 | 20 | OrnPkTransaction *OrnPkDaemon::transaction() { 21 | auto reply = call(QStringLiteral("CreateTransaction")); 22 | Q_ASSERT_X(reply.type() != QDBusMessage::ErrorMessage, Q_FUNC_INFO, 23 | qPrintable(reply.errorName().append(": ").append(reply.errorMessage()))); 24 | 25 | auto t = new OrnPkTransaction( 26 | qvariant_cast(reply.arguments().constFirst()).path(), 27 | false, 28 | parent() 29 | ); 30 | Q_ASSERT(t->isValid()); 31 | 32 | return t; 33 | } 34 | -------------------------------------------------------------------------------- /src/ornpkdaemon.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | class OrnPkTransaction; 6 | 7 | class OrnPkDaemon : public QDBusAbstractInterface 8 | { 9 | Q_OBJECT 10 | 11 | public: 12 | static const QString serviceName; 13 | 14 | explicit OrnPkDaemon(QObject *parent = nullptr); 15 | 16 | OrnPkTransaction *transaction(); 17 | 18 | signals: 19 | void UpdatesChanged(); 20 | void TransactionListChanged(const QStringList &transactions); 21 | }; 22 | -------------------------------------------------------------------------------- /src/ornpktransaction.cpp: -------------------------------------------------------------------------------- 1 | #include "ornpktransaction.h" 2 | #include "ornpkdaemon.h" 3 | 4 | #include 5 | 6 | #include 7 | #include 8 | 9 | constexpr quint64 FlagNone{PackageKit::Transaction::TransactionFlagNone}; 10 | 11 | #ifdef QT_DEBUG 12 | QDebug operator<<(QDebug dbg, const OrnPkTransaction *t) { 13 | QDebugStateSaver state{dbg}; 14 | dbg.quote().nospace() << "PackageKit::Transaction(" << t->path() << ")"; 15 | return dbg; 16 | } 17 | #endif 18 | 19 | OrnPkTransaction::OrnPkTransaction(const QString &path, bool conn, QObject *parent) 20 | : QDBusAbstractInterface( 21 | OrnPkDaemon::serviceName, 22 | path, 23 | "org.freedesktop.PackageKit.Transaction", 24 | QDBusConnection::systemBus(), 25 | parent 26 | ) 27 | { 28 | if (conn) { 29 | #ifdef QT_DEBUG 30 | connect(this, &OrnPkTransaction::Finished, this, [this](quint32 exit, quint32 runtime) { 31 | qDebug() << this 32 | << (exit == PackageKit::Transaction::ExitSuccess ? "finished in" : "failed after") 33 | << runtime << "msec"; 34 | this->deleteLater(); 35 | }); 36 | connect(this, &OrnPkTransaction::ErrorCode, this, [this](quint32 code, const QString &details) { 37 | qDebug().noquote().nospace() << this << " error code " << code << ": " << details; 38 | }); 39 | #else 40 | connect(this, &OrnPkTransaction::Finished, this, &OrnPkTransaction::deleteLater); 41 | #endif 42 | } 43 | } 44 | 45 | void OrnPkTransaction::resolve(const QStringList &names) { 46 | QString method{QStringLiteral("Resolve")}; 47 | qDebug().nospace() 48 | << "Calling " << this << "->" << method.toLatin1().data() 49 | << "(" << FlagNone << ", " << names << ")"; 50 | asyncCall(method, FlagNone, names); 51 | } 52 | 53 | void OrnPkTransaction::installPackages(const QStringList &ids) { 54 | QString method{QStringLiteral("InstallPackages")}; 55 | qDebug().nospace() 56 | << "Calling " << this << "->" << method.toLatin1().data() 57 | << "(" << FlagNone << ", " << ids << ")"; 58 | asyncCall(method, FlagNone, ids); 59 | } 60 | 61 | void OrnPkTransaction::updatePackages(const QStringList &ids) { 62 | QString method{QStringLiteral("UpdatePackages")}; 63 | qDebug().nospace() 64 | << "Calling " << this << "->" << method.toLatin1().data() 65 | << "(" << FlagNone << ", " << ids << ")"; 66 | asyncCall(method, FlagNone, ids); 67 | } 68 | 69 | void OrnPkTransaction::removePackages(const QStringList &ids, bool autoremove) { 70 | QString method{QStringLiteral("RemovePackages")}; 71 | qDebug().nospace() 72 | << "Calling " << this << "->" << method.toLatin1().data() 73 | << "(" << FlagNone << ", " << ids << ", false, " << autoremove << ")"; 74 | asyncCall(method, FlagNone, ids, false, autoremove); 75 | } 76 | 77 | void OrnPkTransaction::installFiles(const QStringList &files) { 78 | QString method{QStringLiteral("InstallFiles")}; 79 | qDebug().nospace() 80 | << "Calling " << this << "->" << method.toLatin1().data() 81 | << "(" << FlagNone << ", " << files << ")"; 82 | asyncCall(method, FlagNone, files); 83 | } 84 | 85 | void OrnPkTransaction::repoRefreshNow(const QString &alias, const QString &force) { 86 | QString method{QStringLiteral("RepoSetData")}; 87 | qDebug().nospace().noquote() 88 | << "Calling " << this << "->" << method 89 | << "(\"" << alias << "\", \"refresh-now\", " << force << ")"; 90 | asyncCall(method, alias, "refresh-now", force); 91 | } 92 | 93 | void OrnPkTransaction::refreshCache(bool force) { 94 | QString method{QStringLiteral("RefreshCache")}; 95 | qDebug().nospace().noquote() 96 | << "Calling " << this << "->" << method << "(" << force << ")"; 97 | asyncCall(method, force); 98 | } 99 | 100 | void OrnPkTransaction::getUpdates() { 101 | QString method{QStringLiteral("GetUpdates")}; 102 | qDebug().nospace().noquote() 103 | << "Calling " << this << "->" << method << "(" << FlagNone << ")"; 104 | asyncCall(method, FlagNone); 105 | } 106 | -------------------------------------------------------------------------------- /src/ornpktransaction.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "ornutils.h" 4 | 5 | #include 6 | 7 | class OrnPkTransaction : public QDBusAbstractInterface 8 | { 9 | Q_OBJECT 10 | 11 | #ifdef QT_DEBUG 12 | friend QDebug operator<<(QDebug dbg, const OrnPkTransaction *t); 13 | #endif 14 | 15 | public: 16 | OrnPkTransaction(const QString &path, bool conn, QObject *parent = nullptr); 17 | 18 | void resolve(const QStringList &names); 19 | void installPackages(const QStringList &ids); 20 | void updatePackages(const QStringList &ids); 21 | void removePackages(const QStringList &ids, bool autoremove = false); 22 | 23 | void installFiles(const QStringList &files); 24 | 25 | void repoRefreshNow(const QString &alias, const QString &force); 26 | void repoRefreshNow(const QString &alias, bool force = false) { 27 | repoRefreshNow(alias, OrnUtils::stringify(force)); 28 | } 29 | 30 | void refreshCache(bool force = false); 31 | void getUpdates(); 32 | 33 | Q_PROPERTY(uint Role READ role CONSTANT) 34 | uint role() const { 35 | return property("Role").toUInt(); 36 | } 37 | 38 | signals: 39 | void ErrorCode(quint32 code, const QString &details); 40 | void Finished(quint32 exit, quint32 runtime); 41 | void Package(quint32 info, const QString &packageId, const QString &summary); 42 | void ItemProgress(const QString &id, uint status, uint percentage); 43 | }; 44 | -------------------------------------------------------------------------------- /src/ornpm_p.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "ornpm.h" 4 | #include "orninstalledpackage.h" 5 | #include "ornpkdaemon.h" 6 | #include "ornpktransaction.h" 7 | #include "ornssu.h" 8 | 9 | #include 10 | #include 11 | #include 12 | 13 | #include 14 | 15 | #define PK_FLAG_NONE quint64(0) 16 | 17 | class OrnPmPrivate : public QObjectPrivate 18 | { 19 | Q_DISABLE_COPY(OrnPmPrivate) 20 | Q_DECLARE_PUBLIC(OrnPm) 21 | 22 | public: 23 | using ornpm_signal_t = void (OrnPm::*)(const QString &); 24 | 25 | OrnPmPrivate() = default; 26 | ~OrnPmPrivate() override = default; 27 | 28 | void initialise(); 29 | OrnPkTransaction *transaction(); 30 | OrnPkTransaction *currentTransaction(); 31 | void preparePackageVersions(const QString &packageName); 32 | bool enableRepos(bool enable); 33 | void removeAllRepos(); 34 | void onRepoModified(const QString &alias, OrnPm::RepoAction action); 35 | OrnInstalledPackageList prepareInstalledPackages(const QString &packageName); 36 | 37 | bool startOperation(const QString &name, OrnPm::Operation operation); 38 | void finishOperation(const QString &name); 39 | 40 | void onItemProgress(const QString &id, uint status, uint percentage); 41 | 42 | OrnPkTransaction *packageTransaction(const QString &packageName, OrnPm::Operation operation, OrnPm::PackageStatus status, ornpm_signal_t sgnl); 43 | 44 | // Check for updates 45 | void getUpdates(); 46 | // Refresh repos 47 | void refreshNextRepo(quint32 exit, quint32 runtime); 48 | 49 | void processSolvables(bool enabled, std::function callback) const; 50 | 51 | // 52 | using RepoHash = QHash; 53 | using StringSet = QSet; 54 | using StringHash = QHash; 55 | using OperationHash = QHash; 56 | 57 | bool initialised{false}; 58 | #ifdef QT_DEBUG 59 | quint64 refreshRuntime{0}; 60 | #endif 61 | QString solvPathTmpl; 62 | StringSet archs; 63 | OrnSsu *ssuInterface{nullptr}; 64 | OrnPkDaemon *pkDaemon{nullptr}; 65 | RepoHash repos; 66 | StringHash installedPackages; 67 | StringHash updatablePackages; 68 | StringHash newUpdatablePackages; 69 | OperationHash operations; 70 | QStringList reposToRefresh; 71 | QString forceRefresh; 72 | }; 73 | -------------------------------------------------------------------------------- /src/ornproxymodel.cpp: -------------------------------------------------------------------------------- 1 | #include "ornproxymodel.h" 2 | #include "ornabstractlistmodel.h" 3 | 4 | 5 | OrnProxyModel::OrnProxyModel(QObject *parent) 6 | : QSortFilterProxyModel(parent) 7 | { 8 | } 9 | 10 | int OrnProxyModel::limit() const 11 | { 12 | return mLimit; 13 | } 14 | 15 | void OrnProxyModel::setLimit(int limit) 16 | { 17 | if (mLimit != limit) 18 | { 19 | mLimit = limit; 20 | emit this->limitChanged(); 21 | } 22 | this->invalidate(); 23 | } 24 | 25 | void OrnProxyModel::sort(Qt::SortOrder order) 26 | { 27 | QSortFilterProxyModel::sort(0, order); 28 | } 29 | 30 | void OrnProxyModel::reset() 31 | { 32 | static_cast(this->sourceModel())->reset(); 33 | } 34 | 35 | int OrnProxyModel::rowCount(const QModelIndex &parent) const 36 | { 37 | auto count = QSortFilterProxyModel::rowCount(parent); 38 | return mLimit > -1 ? std::min(count, mLimit) : count; 39 | } 40 | 41 | bool OrnProxyModel::canFetchMore(const QModelIndex &parent) const 42 | { 43 | auto canFetch = QSortFilterProxyModel::canFetchMore(parent); 44 | return mLimit > -1 ? 45 | QSortFilterProxyModel::rowCount(parent) < mLimit && canFetch : 46 | canFetch; 47 | } 48 | 49 | bool OrnProxyModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const 50 | { 51 | auto role = this->filterRole(); 52 | // The default value 53 | if (role == Qt::DisplayRole) 54 | { 55 | return true; 56 | } 57 | 58 | auto model = this->sourceModel(); 59 | auto idx = model->index(source_row, 0, source_parent); 60 | auto dat = model->data(idx, role); 61 | #ifdef QT_DEBUG 62 | if (dat.type() != QVariant::Bool) 63 | { 64 | qWarning("The filter role must return boolean!"); 65 | } 66 | #endif 67 | return dat.toBool(); 68 | } 69 | -------------------------------------------------------------------------------- /src/ornproxymodel.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | class OrnProxyModel : public QSortFilterProxyModel 6 | { 7 | Q_OBJECT 8 | Q_PROPERTY(int limit READ limit WRITE setLimit NOTIFY limitChanged) 9 | 10 | public: 11 | explicit OrnProxyModel(QObject *parent = nullptr); 12 | 13 | int limit() const; 14 | void setLimit(int limit); 15 | 16 | signals: 17 | void limitChanged(); 18 | 19 | public: 20 | // Why QSortFilterProxyModel has no sort slot? 21 | Q_INVOKABLE void sort(Qt::SortOrder order = Qt::AscendingOrder); 22 | 23 | public slots: 24 | void reset(); 25 | 26 | private: 27 | int mLimit{-1}; 28 | 29 | // QAbstractItemModel interface 30 | public: 31 | int rowCount(const QModelIndex &parent) const override; 32 | bool canFetchMore(const QModelIndex &parent) const override; 33 | 34 | // QSortFilterProxyModel interface 35 | protected: 36 | bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override; 37 | }; 38 | -------------------------------------------------------------------------------- /src/ornrepo.cpp: -------------------------------------------------------------------------------- 1 | #include "ornrepo.h" 2 | #include "ornpm.h" 3 | 4 | OrnRepo::OrnRepo(bool enabled, const QString &alias) 5 | : enabled{enabled} 6 | , alias{alias} 7 | , author{alias == OrnPm::storemanRepo ? 8 | //% "Storeman OBS Repository" 9 | qtTrId("orn-storeman-repo-name") : 10 | alias.mid(OrnPm::repoNamePrefix.size()) 11 | } 12 | { 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/ornrepo.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | struct OrnRepo 6 | { 7 | OrnRepo(bool enabled, const QString &alias); 8 | 9 | bool enabled; 10 | QString alias; 11 | QString author; 12 | }; 13 | 14 | using OrnRepoList = QList; 15 | -------------------------------------------------------------------------------- /src/ornrepomodel.cpp: -------------------------------------------------------------------------------- 1 | #include "ornrepomodel.h" 2 | #include "ornpm.h" 3 | 4 | #include 5 | #include 6 | 7 | #include 8 | 9 | OrnRepoModel::OrnRepoModel(QObject *parent) 10 | : QAbstractListModel(parent) 11 | { 12 | auto ornPm = OrnPm::instance(); 13 | 14 | connect(ornPm, &OrnPm::initialisedChanged, this, &OrnRepoModel::reset); 15 | connect(ornPm, &OrnPm::enableReposFinished, this, &OrnRepoModel::reset); 16 | connect(ornPm, &OrnPm::removeAllReposFinished, this, &OrnRepoModel::reset); 17 | connect(ornPm, &OrnPm::repoModified, this, &OrnRepoModel::onRepoModified); 18 | connect(this, &OrnRepoModel::modelReset, this, &OrnRepoModel::enabledReposChanged); 19 | 20 | // Delay reset to ensure that modelReset signal is received in qml 21 | QTimer::singleShot(500, this, &OrnRepoModel::reset); 22 | } 23 | 24 | bool OrnRepoModel::hasEnabledRepos() const 25 | { 26 | return mEnabledRepos; 27 | } 28 | 29 | bool OrnRepoModel::hasDisabledRepos() const 30 | { 31 | return mEnabledRepos != mData.size(); 32 | } 33 | 34 | void OrnRepoModel::reset() 35 | { 36 | qDebug() << "Resetting model"; 37 | this->beginResetModel(); 38 | 39 | mData = OrnPm::instance()->repoList(); 40 | 41 | const auto enabled = std::count_if(mData.cbegin(), mData.cend(), [](const auto &r) { 42 | return r.enabled; 43 | }); 44 | 45 | if (mEnabledRepos != enabled) 46 | { 47 | mEnabledRepos = enabled; 48 | emit this->enabledReposChanged(); 49 | } 50 | 51 | this->endResetModel(); 52 | } 53 | 54 | void OrnRepoModel::onRepoModified(const QString &alias, int action) 55 | { 56 | QModelIndex parentIndex; 57 | 58 | if (action == OrnPm::AddRepo) 59 | { 60 | auto row = mData.size(); 61 | this->beginInsertRows(parentIndex, row, row); 62 | mData << OrnRepo{true, alias}; 63 | ++mEnabledRepos; 64 | emit this->enabledReposChanged(); 65 | this->endInsertRows(); 66 | return; 67 | } 68 | 69 | int row = 0; 70 | for (auto &repo : mData) 71 | { 72 | if (repo.alias == alias) 73 | { 74 | switch (action) 75 | { 76 | case OrnPm::RemoveRepo: 77 | this->beginRemoveRows(parentIndex, row, row); 78 | mData.removeAt(row); 79 | --mEnabledRepos; 80 | emit this->enabledReposChanged(); 81 | this->endRemoveRows(); 82 | break; 83 | case OrnPm::DisableRepo: 84 | case OrnPm::EnableRepo: 85 | { 86 | bool enable = action == OrnPm::EnableRepo; 87 | if (repo.enabled != enable) 88 | { 89 | repo.enabled = enable; 90 | auto index = this->createIndex(row, 0); 91 | emit this->dataChanged(index, index, { RepoEnabledRole }); 92 | mEnabledRepos += enable ? 1 : -1; 93 | emit this->enabledReposChanged(); 94 | } 95 | } 96 | break; 97 | default: 98 | break; 99 | } 100 | return; 101 | } 102 | ++row; 103 | } 104 | } 105 | 106 | int OrnRepoModel::rowCount(const QModelIndex &parent) const 107 | { 108 | return !parent.isValid() ? mData.size() : 0; 109 | } 110 | 111 | QVariant OrnRepoModel::data(const QModelIndex &index, int role) const 112 | { 113 | if (!index.isValid()) 114 | { 115 | return QVariant(); 116 | } 117 | 118 | auto &repo = mData[index.row()]; 119 | switch (role) 120 | { 121 | case RepoAuthorRole: 122 | return repo.author; 123 | case RepoAliasRole: 124 | return repo.alias; 125 | case RepoEnabledRole: 126 | return repo.enabled; 127 | case SortRole: 128 | // Place enabled first and then sort by author 129 | return QString::number(!repo.enabled).append(repo.author); 130 | default: 131 | return QVariant(); 132 | } 133 | } 134 | 135 | QHash OrnRepoModel::roleNames() const 136 | { 137 | return { 138 | { RepoAuthorRole, "repoAuthor" }, 139 | { RepoAliasRole, "repoAlias" }, 140 | { RepoEnabledRole, "repoEnabled" } 141 | }; 142 | } 143 | -------------------------------------------------------------------------------- /src/ornrepomodel.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "ornrepo.h" 6 | 7 | class OrnRepoModel : public QAbstractListModel 8 | { 9 | Q_OBJECT 10 | Q_PROPERTY(bool hasEnabledRepos READ hasEnabledRepos NOTIFY enabledReposChanged) 11 | Q_PROPERTY(bool hasDisabledRepos READ hasDisabledRepos NOTIFY enabledReposChanged) 12 | 13 | public: 14 | 15 | enum Roles 16 | { 17 | RepoAuthorRole = Qt::UserRole + 1, 18 | RepoAliasRole, 19 | RepoEnabledRole, 20 | SortRole 21 | }; 22 | Q_ENUM(Roles) 23 | 24 | explicit OrnRepoModel(QObject *parent = nullptr); 25 | 26 | bool hasEnabledRepos() const; 27 | bool hasDisabledRepos() const; 28 | 29 | public slots: 30 | void reset(); 31 | 32 | signals: 33 | void enabledReposChanged(); 34 | 35 | private slots: 36 | void onRepoModified(const QString &alias, int action); 37 | 38 | private: 39 | int mEnabledRepos{0}; 40 | OrnRepoList mData; 41 | 42 | // QAbstractItemModel interface 43 | public: 44 | int rowCount(const QModelIndex &parent) const override; 45 | QVariant data(const QModelIndex &index, int role) const override; 46 | QHash roleNames() const override; 47 | }; 48 | -------------------------------------------------------------------------------- /src/ornsearchappsmodel.cpp: -------------------------------------------------------------------------------- 1 | #include "ornsearchappsmodel.h" 2 | 3 | #include 4 | 5 | 6 | OrnSearchAppsModel::OrnSearchAppsModel(QObject *parent) 7 | : OrnAppsModel(true, parent) 8 | { 9 | mCanFetchMore = false; 10 | } 11 | 12 | QString OrnSearchAppsModel::searchKey() const 13 | { 14 | return mSearchKey; 15 | } 16 | 17 | void OrnSearchAppsModel::setSearchKey(const QString &searchKey) 18 | { 19 | if (mSearchKey != searchKey) 20 | { 21 | mSearchKey = searchKey; 22 | emit this->searchKeyChanged(); 23 | this->reset(); 24 | mCanFetchMore = !mSearchKey.isEmpty(); 25 | } 26 | } 27 | 28 | void OrnSearchAppsModel::resetImpl() 29 | { 30 | mPrevReplyHash.clear(); 31 | OrnAbstractListModel::resetImpl(); 32 | } 33 | 34 | void OrnSearchAppsModel::fetchMore(const QModelIndex &parent) 35 | { 36 | if (parent.isValid()) 37 | { 38 | return; 39 | } 40 | if (mSearchKey.isEmpty()) 41 | { 42 | qWarning() << "Could not search with an empty search key"; 43 | return; 44 | } 45 | QUrlQuery query; 46 | query.addQueryItem(QStringLiteral("keys"), mSearchKey); 47 | OrnAbstractListModel::fetch(QStringLiteral("search/apps"), query); 48 | } 49 | 50 | void OrnSearchAppsModel::processReply(const QJsonDocument &jsonDoc) 51 | { 52 | // An ugly patch for repeating data 53 | auto replyHash = QCryptographicHash::hash(jsonDoc.toJson(), QCryptographicHash::Md5); 54 | if (mPrevReplyHash == replyHash) 55 | { 56 | qDebug() << "Current reply is equal to the previous one. " 57 | "Considering the model has fetched all data"; 58 | mCanFetchMore = false; 59 | return; 60 | } 61 | mPrevReplyHash = replyHash; 62 | OrnAbstractListModel::processReply(jsonDoc); 63 | } 64 | -------------------------------------------------------------------------------- /src/ornsearchappsmodel.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "ornappsmodel.h" 4 | 5 | class OrnSearchAppsModel : public OrnAppsModel 6 | { 7 | Q_OBJECT 8 | Q_PROPERTY(QString searchKey READ searchKey WRITE setSearchKey NOTIFY searchKeyChanged) 9 | 10 | public: 11 | explicit OrnSearchAppsModel(QObject *parent = nullptr); 12 | 13 | QString searchKey() const; 14 | void setSearchKey(const QString &searchKey); 15 | 16 | signals: 17 | void searchKeyChanged(); 18 | 19 | private: 20 | QString mSearchKey; 21 | QByteArray mPrevReplyHash; 22 | 23 | // OrnAbstractListModelBase interface 24 | protected: 25 | void resetImpl() override; 26 | 27 | // QAbstractItemModel interface 28 | public: 29 | void fetchMore(const QModelIndex &parent) override; 30 | void processReply(const QJsonDocument &jsonDoc) override; 31 | }; 32 | -------------------------------------------------------------------------------- /src/ornsecrets.cpp: -------------------------------------------------------------------------------- 1 | #include "ornsecrets_p.h" 2 | #include "ornsecrets.h" 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | #include 14 | 15 | using namespace Sailfish::Secrets; 16 | 17 | 18 | static const QString collectionName(QStringLiteral("storeman")); 19 | 20 | bool checkResult(const Request &req) 21 | { 22 | auto result = req.result(); 23 | auto success = result.errorCode() == Result::NoError; 24 | if (!success) { 25 | qDebug() << result.errorMessage(); 26 | } 27 | return success; 28 | } 29 | 30 | Secret::Identifier makeIdent(const QString &name) 31 | { 32 | return Secret::Identifier(name, collectionName, SecretManager::DefaultEncryptedStoragePluginName); 33 | } 34 | 35 | bool createCollection(SecretManager *manager) 36 | { 37 | CreateCollectionRequest ccr; 38 | ccr.setManager(manager); 39 | ccr.setCollectionName(collectionName); 40 | ccr.setAccessControlMode(SecretManager::OwnerOnlyMode); 41 | ccr.setCollectionLockType(CreateCollectionRequest::DeviceLock); 42 | ccr.setDeviceLockUnlockSemantic(SecretManager::DeviceLockKeepUnlocked); 43 | ccr.setStoragePluginName(SecretManager::DefaultEncryptedStoragePluginName); 44 | ccr.setEncryptionPluginName(SecretManager::DefaultEncryptedStoragePluginName); 45 | ccr.startRequest(); 46 | ccr.waitForFinished(); 47 | return checkResult(ccr); 48 | } 49 | 50 | OrnSecrets::OrnSecrets() 51 | : d_ptr{new OrnSecretsPrivate()} 52 | { 53 | CollectionNamesRequest cnr; 54 | cnr.setManager(d_ptr->secretManager.get()); 55 | cnr.setStoragePluginName(SecretManager::DefaultEncryptedStoragePluginName); 56 | cnr.startRequest(); 57 | cnr.waitForFinished(); 58 | d_ptr->valid = checkResult(cnr) && cnr.collectionNames().contains(collectionName); 59 | } 60 | 61 | OrnSecrets::~OrnSecrets() 62 | { 63 | 64 | } 65 | 66 | bool OrnSecrets::isValid() const 67 | { 68 | return d_ptr->valid; 69 | } 70 | 71 | bool OrnSecrets::storeData(const QString &name, const QByteArray &data) 72 | { 73 | if (!d_ptr->valid) { 74 | d_ptr->valid = createCollection(d_ptr->secretManager.get()); 75 | } 76 | 77 | Secret secret(makeIdent(name)); 78 | secret.setData(data); 79 | 80 | StoreSecretRequest ssr; 81 | ssr.setManager(d_ptr->secretManager.get()); 82 | ssr.setSecretStorageType(StoreSecretRequest::CollectionSecret); 83 | ssr.setUserInteractionMode(SecretManager::SystemInteraction); 84 | ssr.setSecret(secret); 85 | ssr.startRequest(); 86 | ssr.waitForFinished(); 87 | 88 | return checkResult(ssr); 89 | } 90 | 91 | QByteArray OrnSecrets::data(const QString &name) 92 | { 93 | if (!d_ptr->valid) { 94 | return QByteArray(); 95 | } 96 | 97 | StoredSecretRequest ssr; 98 | ssr.setManager(d_ptr->secretManager.get()); 99 | ssr.setUserInteractionMode(Sailfish::Secrets::SecretManager::SystemInteraction); 100 | ssr.setIdentifier(makeIdent(name));; 101 | ssr.startRequest(); 102 | ssr.waitForFinished(); 103 | 104 | auto success = checkResult(ssr); 105 | if (success) 106 | { 107 | return ssr.secret().data(); 108 | } 109 | 110 | return QByteArray(); 111 | } 112 | 113 | bool OrnSecrets::removeCollection() 114 | { 115 | if (!d_ptr->valid) { 116 | return false; 117 | } 118 | 119 | DeleteCollectionRequest dcr; 120 | dcr.setManager(d_ptr->secretManager.get()); 121 | dcr.setCollectionName(collectionName); 122 | dcr.setStoragePluginName(SecretManager::DefaultEncryptedStoragePluginName); 123 | dcr.setUserInteractionMode(Sailfish::Secrets::SecretManager::SystemInteraction); 124 | dcr.startRequest(); 125 | dcr.waitForFinished(); 126 | return checkResult(dcr); 127 | } 128 | -------------------------------------------------------------------------------- /src/ornsecrets.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | struct OrnSecretsPrivate; 7 | 8 | class OrnSecrets 9 | { 10 | public: 11 | OrnSecrets(); 12 | ~OrnSecrets(); 13 | 14 | bool isValid() const; 15 | 16 | bool storeData(const QString &name, const QByteArray &data); 17 | QByteArray data(const QString &name); 18 | bool removeCollection(); 19 | 20 | private: 21 | std::unique_ptr d_ptr; 22 | }; 23 | -------------------------------------------------------------------------------- /src/ornsecrets_p.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | struct OrnSecretsPrivate 7 | { 8 | using SecretManager = Sailfish::Secrets::SecretManager; 9 | 10 | std::unique_ptr secretManager{new SecretManager()}; 11 | bool valid{false}; 12 | }; 13 | -------------------------------------------------------------------------------- /src/ornssu.cpp: -------------------------------------------------------------------------------- 1 | #include "ornssu.h" 2 | 3 | #include 4 | #include 5 | 6 | OrnSsu::OrnSsu(QObject *parent) 7 | : QDBusAbstractInterface( 8 | QStringLiteral("org.nemo.ssu"), 9 | QStringLiteral("/org/nemo/ssu"), 10 | "org.nemo.ssu", 11 | QDBusConnection::systemBus(), 12 | parent 13 | ) 14 | { 15 | 16 | } 17 | 18 | void OrnSsu::addRepo(const QString &alias, const QString &url) { 19 | QString method{QStringLiteral("addRepo")}; 20 | qDebug().nospace() 21 | << "Calling " << this << "->" << method.toLatin1().data() 22 | << "(" << alias << ", " << url << ")"; 23 | callWithArgumentList(QDBus::BlockWithGui, method, QVariantList{alias, url}); 24 | } 25 | 26 | void OrnSsu::modifyRepo(int action, const QString &alias) { 27 | QString method{QStringLiteral("modifyRepo")}; 28 | qDebug().nospace() 29 | << "Calling " << this << "->" << method.toLatin1().data() 30 | << "(" << action << ", " << alias << ")"; 31 | callWithArgumentList(QDBus::BlockWithGui, method, QVariantList{action, alias}); 32 | } 33 | -------------------------------------------------------------------------------- /src/ornssu.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | class QDBusPendingCallWatcher; 6 | 7 | class OrnSsu : public QDBusAbstractInterface 8 | { 9 | public: 10 | explicit OrnSsu(QObject *parent = nullptr); 11 | 12 | void addRepo(const QString &alias, const QString &url); 13 | void modifyRepo(int action, const QString &alias); 14 | }; 15 | -------------------------------------------------------------------------------- /src/orntaglistitem.cpp: -------------------------------------------------------------------------------- 1 | #include "orntaglistitem.h" 2 | #include "ornutils.h" 3 | #include "ornconst.h" 4 | 5 | #include 6 | 7 | 8 | OrnTagListItem::OrnTagListItem(const QJsonObject &data) 9 | : tagId(OrnUtils::toUint(data[OrnConst::tid])) 10 | , appsCount(OrnUtils::toUint(data[OrnConst::appsCount])) 11 | , name(OrnUtils::toString(data[OrnConst::name])) 12 | {} 13 | -------------------------------------------------------------------------------- /src/orntaglistitem.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | class QJsonObject; 6 | 7 | struct OrnTagListItem 8 | { 9 | OrnTagListItem(const QJsonObject &data); 10 | 11 | quint32 tagId; 12 | quint32 appsCount; 13 | QString name; 14 | }; 15 | -------------------------------------------------------------------------------- /src/orntagsmodel.cpp: -------------------------------------------------------------------------------- 1 | #include "orntagsmodel.h" 2 | #include "ornclient.h" 3 | 4 | #include 5 | #include 6 | 7 | 8 | OrnTagsModel::OrnTagsModel(QObject *parent) 9 | : OrnAbstractListModel(false, parent) 10 | {} 11 | 12 | QStringList OrnTagsModel::tagIds() const 13 | { 14 | return mTagIds; 15 | } 16 | 17 | void OrnTagsModel::setTagIds(const QStringList &ids) 18 | { 19 | if (mTagIds != ids) 20 | { 21 | mTagIds = ids; 22 | emit this->tagIdsChanged(); 23 | this->reset(); 24 | } 25 | } 26 | 27 | QVariant OrnTagsModel::data(const QModelIndex &index, int role) const 28 | { 29 | if (!index.isValid()) 30 | { 31 | return QVariant(); 32 | } 33 | 34 | const auto &tag = mData[index.row()]; 35 | switch (role) 36 | { 37 | case TagIdRole: 38 | return tag.tagId; 39 | case AppsCountRole: 40 | return tag.appsCount; 41 | case NameRole: 42 | return tag.name; 43 | default: 44 | return QVariant(); 45 | } 46 | } 47 | 48 | QHash OrnTagsModel::roleNames() const 49 | { 50 | return { 51 | { TagIdRole, "tagId" }, 52 | { AppsCountRole, "appsCount" }, 53 | { NameRole, "name" } 54 | }; 55 | } 56 | 57 | void OrnTagsModel::fetchMore(const QModelIndex &parent) 58 | { 59 | if (parent.isValid()) 60 | { 61 | return; 62 | } 63 | 64 | mFetching = true; 65 | emit this->fetchingChanged(); 66 | 67 | auto client = OrnClient::instance(); 68 | auto size = mTagIds.size(); 69 | QString resourcePrefix(QStringLiteral("tags/")); 70 | 71 | const auto &ids = mTagIds; 72 | for (const auto &id : ids) 73 | { 74 | auto request = client->apiRequest(resourcePrefix + id); 75 | qDebug() << "Fetching data from" << request.url().toString(); 76 | auto reply = client->networkAccessManager()->get(request); 77 | connect(reply, &QNetworkReply::finished, [this, client, size, reply]() 78 | { 79 | auto doc = client->processReply(reply); 80 | if (doc.isObject()) 81 | { 82 | mFetchedTags.append(doc.object()); 83 | if (mFetchedTags.size() == size) 84 | { 85 | this->processReply(QJsonDocument(mFetchedTags)); 86 | mFetchedTags = QJsonArray(); 87 | mFetching = false; 88 | emit this->fetchingChanged(); 89 | } 90 | } 91 | }); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/orntagsmodel.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "ornabstractlistmodel.h" 4 | #include "orntaglistitem.h" 5 | 6 | class OrnTagsModel : public OrnAbstractListModel 7 | { 8 | Q_OBJECT 9 | Q_PROPERTY(QStringList tagIds READ tagIds WRITE setTagIds NOTIFY tagIdsChanged) 10 | 11 | public: 12 | enum Role 13 | { 14 | TagIdRole = Qt::UserRole, 15 | AppsCountRole, 16 | NameRole 17 | }; 18 | Q_ENUM(Role) 19 | 20 | explicit OrnTagsModel(QObject *parent = nullptr); 21 | 22 | QStringList tagIds() const; 23 | void setTagIds(const QStringList &ids); 24 | 25 | signals: 26 | void tagIdsChanged(); 27 | 28 | private: 29 | QStringList mTagIds; 30 | QJsonArray mFetchedTags; 31 | 32 | // QAbstractItemModel interface 33 | public: 34 | QVariant data(const QModelIndex &index, int role) const override; 35 | QHash roleNames() const override; 36 | void fetchMore(const QModelIndex &parent) override; 37 | }; 38 | -------------------------------------------------------------------------------- /src/ornutils.cpp: -------------------------------------------------------------------------------- 1 | #include "ornutils.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include 11 | 12 | namespace OrnUtils 13 | { 14 | 15 | QList toIntList(const QJsonValue &value) 16 | { 17 | auto array = value.toArray(); 18 | QString tidKey(QStringLiteral("tid")); 19 | QList list; 20 | for (const QJsonValueRef v : array) 21 | { 22 | list << toUint(v.toObject()[tidKey]); 23 | } 24 | return list; 25 | } 26 | 27 | QString locate(const QString &filename) 28 | { 29 | auto dir = QDir(QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation)); 30 | if (!dir.exists() && !dir.mkpath(QChar('.'))) 31 | { 32 | qWarning() << "Could not create local data dir" << dir.path(); 33 | return QString(); 34 | } 35 | return dir.absoluteFilePath(filename); 36 | } 37 | 38 | QVersionNumber systemVersion() 39 | { 40 | QSettings release(QStringLiteral("/etc/sailfish-release"), QSettings::IniFormat); 41 | return QVersionNumber::fromString(release.value(QStringLiteral("VERSION_ID")).toString()); 42 | } 43 | 44 | QString desktopFile(const QString &name) 45 | { 46 | return QStandardPaths::locate( 47 | QStandardPaths::ApplicationsLocation, 48 | QStringLiteral(".desktop").prepend(name) 49 | ); 50 | } 51 | 52 | } // namespace OrnUtils 53 | -------------------------------------------------------------------------------- /src/ornutils.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | class QVersionNumber; 7 | 8 | namespace OrnUtils 9 | { 10 | 11 | inline quint32 toUint(const QJsonValue &value) 12 | { 13 | return value.toString().remove(QChar(',')).toUInt(); 14 | } 15 | 16 | inline QString toString(const QJsonValue &value) 17 | { 18 | return value.toString().trimmed(); 19 | } 20 | 21 | inline QDateTime toDateTime(const QJsonValue &value) 22 | { 23 | return QDateTime::fromMSecsSinceEpoch(qint64(toUint(value)) * 1000); 24 | } 25 | 26 | QList toIntList(const QJsonValue &value); 27 | 28 | QString locate(const QString &filename); 29 | 30 | QString desktopFile(const QString &name); 31 | 32 | inline QString packageName(const QString &id) 33 | { 34 | return id.section(QChar(';'), 0, 0); 35 | } 36 | 37 | inline QString packageVersion(const QString &id) 38 | { 39 | return id.section(QChar(';'), 1, 1); 40 | } 41 | 42 | inline QString packageArch(const QString &id) 43 | { 44 | return id.section(QChar(';'), 2, 2); 45 | } 46 | 47 | inline QString packageRepo(const QString &id) 48 | { 49 | return id.section(QChar(';'), 3, 3); 50 | } 51 | 52 | QVersionNumber systemVersion(); 53 | 54 | inline QString stringify(bool value) 55 | { 56 | return value ? QStringLiteral("true") : QStringLiteral("false"); 57 | } 58 | 59 | } // namespace OrnUtils 60 | -------------------------------------------------------------------------------- /src/storeman.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | class QQmlEngine; 7 | class QJSEngine; 8 | 9 | class OrnApplication; 10 | class StoremanPrivate; 11 | 12 | class Storeman : public QObject 13 | { 14 | Q_OBJECT 15 | Q_DECLARE_PRIVATE(Storeman) 16 | 17 | Q_PROPERTY(bool showRecentOnStart READ showRecentOnStart WRITE setShowRecentOnStart NOTIFY showRecentOnStartChanged) 18 | Q_PROPERTY(QVariantList mainPageOrder READ mainPageOrder WRITE setMainPageOrder RESET resetMainPageOrder NOTIFY mainPageOrderChanged) 19 | Q_PROPERTY(int updateInterval READ updateInterval WRITE setUpdateInterval NOTIFY updateIntervalChanged) 20 | Q_PROPERTY(bool checkForUpdates READ checkForUpdates WRITE setCheckForUpdates NOTIFY checkForUpdatesChanged) 21 | Q_PROPERTY(bool smartUpdate READ smartUpdate WRITE setSmartUpdate NOTIFY smartUpdateChanged) 22 | Q_PROPERTY(bool showUpdatesNotification READ showUpdatesNotification WRITE setShowUpdatesNotification NOTIFY showUpdatesNotificationChanged) 23 | Q_PROPERTY(bool refreshOnSystemUpgrade READ refreshOnSystemUpgrade WRITE setRefreshOnSystemUpgrade NOTIFY refreshOnSystemUpgradeChanged) 24 | Q_PROPERTY(bool searchUnusedRepos READ searchUnusedRepos WRITE setSearchUnusedRepos NOTIFY searchUnusedReposChanged) 25 | 26 | public: 27 | enum Hint 28 | { 29 | CommentDelegateHint, 30 | CommentFieldHint, 31 | ApplicationRateAndBookmarkHint 32 | }; 33 | Q_ENUM(Hint) 34 | 35 | enum MainPageItem 36 | { 37 | RecentlyUpdated, 38 | Categories, 39 | Bookmarks, 40 | Repositories, 41 | MyRepository, 42 | InstalledApps, 43 | LocalRpms, 44 | }; 45 | Q_ENUM(MainPageItem) 46 | 47 | static QObject *qmlInstance(QQmlEngine *engine, QJSEngine *scriptEngine); 48 | 49 | bool showRecentOnStart() const; 50 | void setShowRecentOnStart(bool value); 51 | 52 | QVariantList mainPageOrder() const; 53 | void setMainPageOrder(const QVariantList &value); 54 | void resetMainPageOrder(); 55 | 56 | int updateInterval() const; 57 | void setUpdateInterval(int value); 58 | 59 | bool checkForUpdates() const; 60 | void setCheckForUpdates(bool value); 61 | 62 | bool smartUpdate() const; 63 | void setSmartUpdate(bool value); 64 | 65 | bool showUpdatesNotification() const; 66 | void setShowUpdatesNotification(bool value); 67 | 68 | bool refreshOnSystemUpgrade() const; 69 | void setRefreshOnSystemUpgrade(bool value); 70 | 71 | bool searchUnusedRepos() const; 72 | void setSearchUnusedRepos(bool value); 73 | 74 | Q_INVOKABLE static bool fileExists(const QString &filePath); 75 | Q_INVOKABLE static bool removeFile(const QString &filePath); 76 | 77 | Q_INVOKABLE bool showHint(Storeman::Hint hint); 78 | Q_INVOKABLE void setHintShowed(Storeman::Hint hint); 79 | 80 | Q_INVOKABLE OrnApplication *cachedApp(quint32 appId); 81 | 82 | public slots: 83 | void resetUpdatesTimer(); 84 | 85 | signals: 86 | void showRecentOnStartChanged(); 87 | void mainPageOrderChanged(); 88 | void updateIntervalChanged(); 89 | void checkForUpdatesChanged(); 90 | void smartUpdateChanged(); 91 | void showUpdatesNotificationChanged(); 92 | void refreshOnSystemUpgradeChanged(); 93 | void searchUnusedReposChanged(); 94 | void updatesNotification(bool show, quint32 replaceId); 95 | void recentAppsChanged(); 96 | 97 | private slots: 98 | void refreshRepos(); 99 | void onUpdatablePackagesChanged(); 100 | void startUpdatesTimer(); 101 | void checkSystemVersion(); 102 | 103 | private: 104 | explicit Storeman(QObject *parent = nullptr); 105 | }; 106 | -------------------------------------------------------------------------------- /src/storeman_p.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "storeman.h" 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | 11 | class StoremanPrivate : public QObjectPrivate 12 | { 13 | Q_DECLARE_PUBLIC(Storeman) 14 | 15 | void refreshRepos(); 16 | 17 | QSettings settings; 18 | QTimer updatesTimer; 19 | QCache appsCache; 20 | }; 21 | -------------------------------------------------------------------------------- /translations/README.md: -------------------------------------------------------------------------------- 1 | # Translations (l10n / i18n) 2 | 3 | You can help localising Storeman to your language using [Transifex](https://app.transifex.com/mentaljam/harbour-storeman) or [Qt Linguist](https://doc.qt.io/qt-5/qtlinguist-index.html). 4 | 5 | Note that for Storeman principally translations designated with a country code only (e.g. `pt`) shall be created and maintained. Only if a complete and well maintained translation for the sole country code exists, a country specific variant with a locale will be accepted (e.g. `nl_BE`). 6 | 7 | [Transifex](https://app.transifex.com/mentaljam/harbour-storeman) is the preferred way of submitting translations. Please do not send pull requests (PRs) with translations directly to GitHub, if you have a Transifex account. 8 | 9 | If you do not want to use Atlassian's Transifex, alternatives are [Qt Linguist](https://doc.qt.io/qt-5/linguist-translators.html) or to perform this manually, which is tedious and error prone, hence only suitable for small changes. The resulting changes must be submitted as a pull request, unfortunately. 10 | 11 | ### Testing translations 12 | 13 | Note that translations for Storeman are utilising *ID based* Qt `.ts` files. Hence, to compile a translation file for testing, the `lrelease` command must be executed with the option `-idbased` to convert the translation files (`.ts` files) into Qt message files (`.qm` files), either from [within Qt Linguist](https://doc.qt.io/qtcreator/creator-editor-external.html) or directly [at the command line](https://doc.qt.io/qt-5/linguist-manager.html): 14 | ``` 15 | lrelease -idbased harbour-storeman.ts 16 | ``` 17 | If you want to test your translation before publishing, you should compile it and copy the resulting `.qm` file(s) to (requires root privileges): 18 | ``` 19 | /usr/share/harbour-storeman/translations 20 | ``` 21 | Storeman tries to automatically load a translation file according to your system locale setting. You can also run the application with a selected locale from the terminal. For example, for the Swedish language the command is: 22 | ``` 23 | export LANG=sv; harbour-storeman 24 | ``` 25 | 26 | ### Updating the source `.ts` file with source strings from source code 27 | 28 | Developers and release managers can use the `lupdate` process, either from [within Qt Linguist](https://doc.qt.io/qtcreator/creator-editor-external.html) or directly [at the command line](https://doc.qt.io/qt-5/linguist-manager.html) (mind to [include all files with translatable strings](https://github.com/storeman-developers/harbour-storeman/pull/431#issuecomment-1659024529), e.g. by `lupdate qml/ src/ *.desktop -ts translations/harbour-storeman.ts`), or tediously perform this manually, which hence is only suitable for small changes. 29 | 30 | --------------------------------------------- 31 | 32 | **Note**: When updating this README, mind to also update [its counterpart for the SailfishOS:Chum GUI app](https://github.com/sailfishos-chum/sailfishos-chum-gui//blob/main/translations/README.md). 33 | -------------------------------------------------------------------------------- /translations/translations.pri: -------------------------------------------------------------------------------- 1 | TRANSLATIONS += \ 2 | translations/harbour-storeman.ts \ 3 | translations/harbour-storeman-cs.ts \ 4 | translations/harbour-storeman-da.ts \ 5 | translations/harbour-storeman-de.ts \ 6 | translations/harbour-storeman-el.ts \ 7 | translations/harbour-storeman-es.ts \ 8 | translations/harbour-storeman-et.ts \ 9 | translations/harbour-storeman-fi.ts \ 10 | translations/harbour-storeman-fr.ts \ 11 | translations/harbour-storeman-hu.ts \ 12 | translations/harbour-storeman-it.ts \ 13 | translations/harbour-storeman-nl.ts \ 14 | translations/harbour-storeman-nl_BE.ts \ 15 | translations/harbour-storeman-no.ts \ 16 | translations/harbour-storeman-pl.ts \ 17 | translations/harbour-storeman-pt.ts \ 18 | translations/harbour-storeman-ru.ts \ 19 | translations/harbour-storeman-sk.ts \ 20 | translations/harbour-storeman-sl.ts \ 21 | translations/harbour-storeman-sv.ts \ 22 | translations/harbour-storeman-tt.ts \ 23 | translations/harbour-storeman-zh.ts 24 | 25 | qm.input = TRANSLATIONS 26 | qm.output = translations/${QMAKE_FILE_BASE}.qm 27 | qm.commands = @echo "compiling ${QMAKE_FILE_NAME}"; \ 28 | lrelease -idbased -silent ${QMAKE_FILE_NAME} -qm ${QMAKE_FILE_OUT} 29 | qm.CONFIG = target_predeps no_link 30 | 31 | QMAKE_EXTRA_COMPILERS += qm 32 | 33 | translations.files = $$OUT_PWD/translations/*.qm 34 | translations.path = $$PREFIX/share/$$TARGET/translations 35 | translations.CONFIG += no_check_exist 36 | 37 | INSTALLS += translations 38 | --------------------------------------------------------------------------------