mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-07-17 16:37:43 +00:00
Feat/to me it looks like there are lots of 60dd04 (#7280)
* fix(issue): prevent crash from orphan issueProviderId (#7135) The Jira image-headers effect in task-detail-panel subscribed to selectIssueProviderById without an error handler, so a task with an issueProviderId pointing at a deleted provider (e.g. after sync convergence where taskIdsToUnlink didn't cover all local tasks) propagated the selector throw to Zone.js as a crash dialog. Wrap the inner selector observable in catchError that logs and falls back to of(null); the downstream jiraCfg?.isEnabled guard handles the fallback. Also drop IssueLog.log(issueProviderKey, issueProvider) from the throwing variant of the selector: providers may carry credentials (host, token, apiKey) and IssueLog history is exportable. * fix(focus-mode): sync tray countdown with in-app timer during breaks Tray title was rebuilt from a cached currentFocusSessionTime that only refreshed when CURRENT_TASK_UPDATED fired. addTimeSpent is gated on an active current task, so during focus-mode breaks or task-less focus sessions the cache froze while the in-app timer kept ticking. Add the tick action to taskChangeElectron$ so the cache refreshes every second whenever the focus timer is running. Fixes #7278 * fix(ci): restore GitHub Actions SHA pins undone by0e9218bd68Commit0e9218bd68silently reverted PR #7212 (github-actions-minor group bump) along with its stated sync/client-id work. This restores the 15 workflow files to their pre-revert state. Actions restored to newer pinned SHAs: - actions/upload-artifact v7.0.0 -> v7.0.1 - step-security/harden-runner v2.16.1 -> v2.17.0 - softprops/action-gh-release v2.6.1 -> v3.0.0 - signpath/github-action-submit-signing-request v2.0 -> v2.1 - anthropics/claude-code-action v1.0.89 -> v1.0.93 - docker/build-push-action v7.0.0 -> v7.1.0 - easingthemes/ssh-deploy v5.1.1 -> v6.0.3 * fix: restore i18n, UI, and docs work undone by0e9218bd68Commit0e9218bd68silently reverted the following work alongside its stated sync/client-id changes. Files where later master commits (fec7b25f23, etc.) already re-applied the reverted work are intentionally left untouched. Restored: - #7232 docs/long-term-plans/location-based-reminders.md (513 lines) - #7199 Romanian i18n phase 3 (ro.json + ro-md.json, ~1168 lines) - #7049 Polish translation improvements - #7143 planner component styling (4 scss files) - #7211 add-task-bar preserve time estimate when typing title - #7208 task.reducer roll-up estimates for added subtasks - #6767 focus-mode pomodoro reset button - #7205 plugin-dev github-issue-provider TOKEN description - #7231 mobile-bottom-nav FAB fix -a4fe03272iOS keyboard accessory bar (global-theme + dialog-fullscreen-markdown) -309670db3ShortSyntaxEffects undefined guard -667a7986fDropbox PKCE auth comment/behavior * fix(electron): restore electron + e2e work undone by0e9218bd68Commit0e9218bd68silently reverted the following electron/e2e work. Files already re-fixed by later master commits are left as-is: - e2e/tests/sync/supersync-archive-conflict.spec.ts (de33234976+191d129ff3) Restored: -e8a3e156ebfix(electron): Linux autostart IDB backing-store recovery (re-adds electron/clear-stale-idb-locks.ts + start-app.ts wiring) -5ce78a5b63fix(electron): macOS Cmd+Q / Dock > Quit hang (setIsQuiting + before-quit delegate to close-handler) -46e0fa2d01fix(sync): FILE_SYNC_LIST_FILES IPC contract (electronAPI.d.ts + local-file-sync + preload + ipc-events) -ea1ef16307fix(android): session-only SAF permissions on OEM devices -8865dc0a50test(e2e): supersync parallel-worker stampede guard (SUPERSYNC_SERVER_HEALTHY env-var fallback + goto retry loop) -af7c7687e2test(e2e): block WS-triggered downloads in non-WS specs -265b44db5dtest(e2e): premature waitForURL on daily-summary (this is literally the fix the bad commit's message claimed to add) Conflict resolutions: - e2e/utils/supersync-helpers.ts: kept the refined getDoneTaskElement checks fromd64014d086(later thanc558bcab5e) while restoring the goto retry loop from8865dc0a50. - electron/start-app.ts: unioned imports (setIsQuiting + clearStaleLevelDbLocks from theirs, fs from ours). * fix(sync): restore sync-core work undone by0e9218bd68Commit0e9218bd68silently reverted parts of several sync fixes. Most sync-core reverts have already been re-addressed differently on master by later commits (1f5184f6e7,05cd875dd6,09f5ced2c9,7df43358ab,d9158d6adb,32dbc95ed9,8c3b08e016,f89fe1ebc3) — those files are intentionally left untouched to avoid reverting master's newer work. This PR restores only the pieces that are genuinely still missing: -e8a3e156ebfix(electron): IDB backing-store autoreload (in-app piece) operation-log-hydrator.service.ts + .spec.ts (the electron/clear- stale-idb-locks.ts piece was restored in the prior commit) Plus three low-risk documentation/cleanup restorations: - operation-sync.util.ts — add "Nextcloud" to isFileBasedProvider JSDoc - dropbox.ts — restore improved _getRedirectUri JSDoc (667a7986fb) - dialog-get-and-enter-auth-code.component.ts — restore isNativePlatform comment explaining why manual code entry flow is used (667a7986fb) - file-adapter.interface.ts — remove stray "// NEW" comment (46e0fa2d01) Intentionally NOT restored (master's newer work covers or supersedes): - sync-trigger.service.ts / sync.effects.ts (05cd875dd6) - sync-wrapper.service.ts (1f5184f6e7) - sync-errors.ts (1f5184f6e7re-added LegacySyncFormatDetectedError) - file-based-sync-adapter.service.ts + spec (1f5184f6e7+d9158d6adb) - file-based-sync.types.ts (1f5184f6e7) - operation-log.const.ts (32dbc95ed9bumped IDB_OPEN_RETRIES to 5) - dialog-sync-initial-cfg.component.ts (f89fe1ebc3)
This commit is contained in:
parent
1d98d356be
commit
01e30b9c7e
59 changed files with 1719 additions and 684 deletions
|
|
@ -13,7 +13,7 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
|
||||
uses: step-security/harden-runner@f808768d1510423e83855289c910610ca9b43176 # v2.17.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
allowed-endpoints: >
|
||||
|
|
|
|||
2
.github/workflows/build-android.yml
vendored
2
.github/workflows/build-android.yml
vendored
|
|
@ -77,7 +77,7 @@ jobs:
|
|||
# APK is now signed automatically by Gradle using signingConfig
|
||||
|
||||
- name: 'Upload APK files'
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: sup-android-release
|
||||
path: android/app/build/outputs/apk/**/*.apk
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ jobs:
|
|||
github_token: ${{ secrets.github_token }}
|
||||
|
||||
- name: 'Upload Artifact'
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: WinStoreRelease
|
||||
path: .tmp/app-builds/*.appx
|
||||
|
|
|
|||
2
.github/workflows/build-ios.yml
vendored
2
.github/workflows/build-ios.yml
vendored
|
|
@ -200,7 +200,7 @@ jobs:
|
|||
run: ls -la "$RUNNER_TEMP/ipa-output"
|
||||
|
||||
- name: Upload IPA artifact
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: sup-ios-release
|
||||
path: ${{ runner.temp }}/ipa-output/*.ipa
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
|
||||
uses: step-security/harden-runner@f808768d1510423e83855289c910610ca9b43176 # v2.17.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
allowed-endpoints: >
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
|
||||
uses: step-security/harden-runner@f808768d1510423e83855289c910610ca9b43176 # v2.17.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
allowed-endpoints: >
|
||||
|
|
@ -69,7 +69,7 @@ jobs:
|
|||
run: npm run buildFrontend:prodWeb
|
||||
|
||||
- name: Deploy to Web Server
|
||||
uses: easingthemes/ssh-deploy@a1aa0b6cf96ce2406eef90faa35007a4a7bf0ac0 # v5.1.1
|
||||
uses: easingthemes/ssh-deploy@2cc5b27bf3029d0455dd5e09fe02633904031447 # v6.0.3
|
||||
env:
|
||||
SSH_PRIVATE_KEY: ${{ secrets.WEB_SERVER_SSH_KEY }}
|
||||
ARGS: '-rltgoDzvO --delete --exclude "news.json"'
|
||||
|
|
|
|||
10
.github/workflows/build.yml
vendored
10
.github/workflows/build.yml
vendored
|
|
@ -31,7 +31,7 @@ jobs:
|
|||
fi
|
||||
} > /tmp/release-body.md
|
||||
- name: Create Draft Release
|
||||
uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2.6.1
|
||||
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0
|
||||
with:
|
||||
tag_name: ${{ github.ref_name }}
|
||||
draft: true
|
||||
|
|
@ -102,7 +102,7 @@ jobs:
|
|||
|
||||
- name: 'Upload E2E results on failure'
|
||||
if: ${{ failure() }}
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: e2eResults
|
||||
path: .tmp/e2e-test-results/**/*.*
|
||||
|
|
@ -314,7 +314,7 @@ jobs:
|
|||
|
||||
- name: Upload unsigned executables for SignPath
|
||||
id: upload-unsigned
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: unsigned-windows-executables
|
||||
path: .tmp/app-builds/*.exe
|
||||
|
|
@ -322,7 +322,7 @@ jobs:
|
|||
retention-days: 1
|
||||
|
||||
- name: Sign Windows executables with SignPath
|
||||
uses: signpath/github-action-submit-signing-request@3f9250c56651ff692d6729a2fbb0603a42d7d322 # v2
|
||||
uses: signpath/github-action-submit-signing-request@bc66d86b015a46e9c6d9700de73143a82f9570ff # v2
|
||||
with:
|
||||
api-token: ${{ secrets.SIGNPATH_API_TOKEN }}
|
||||
organization-id: ${{ secrets.SIGNPATH_ORGANIZATION_ID }}
|
||||
|
|
@ -474,7 +474,7 @@ jobs:
|
|||
"
|
||||
|
||||
- name: Publish signed Windows binaries to GitHub Release
|
||||
uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2.6.1
|
||||
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
files: |
|
||||
|
|
|
|||
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
|
|
@ -120,7 +120,7 @@ jobs:
|
|||
run: npm run e2e:ci
|
||||
- name: 'Upload E2E results on failure'
|
||||
if: ${{ failure() }}
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: e2eResults
|
||||
path: .tmp/e2e-test-results/**/*.*
|
||||
|
|
|
|||
2
.github/workflows/claude.yml
vendored
2
.github/workflows/claude.yml
vendored
|
|
@ -32,7 +32,7 @@ jobs:
|
|||
|
||||
- name: Run Claude Code
|
||||
id: claude
|
||||
uses: anthropics/claude-code-action@6e2bd52842c65e914eba5c8badd17560bd26b5de # v1
|
||||
uses: anthropics/claude-code-action@b47fd721da662d48c5680e154ad16a73ed74d2e0 # v1
|
||||
with:
|
||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
|
|
|||
8
.github/workflows/e2e-scheduled.yml
vendored
8
.github/workflows/e2e-scheduled.yml
vendored
|
|
@ -50,7 +50,7 @@ jobs:
|
|||
test -f .tmp/angular-dist/browser/assets/bundled-plugins/api-test-plugin/manifest.json || (echo "Plugin assets missing from build" && exit 1)
|
||||
|
||||
- name: Upload Build Artifact
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: e2e-build-${{ github.run_id }}
|
||||
path: .tmp/angular-dist
|
||||
|
|
@ -86,7 +86,7 @@ jobs:
|
|||
|
||||
- name: Upload E2E Results on Failure
|
||||
if: ${{ failure() }}
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: e2e-results-regular-${{ github.run_id }}
|
||||
path: .tmp/e2e-test-results/**/*.*
|
||||
|
|
@ -143,7 +143,7 @@ jobs:
|
|||
|
||||
- name: Upload E2E Results on Failure
|
||||
if: ${{ failure() }}
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: e2e-results-webdav-${{ github.run_id }}
|
||||
path: .tmp/e2e-test-results/**/*.*
|
||||
|
|
@ -205,7 +205,7 @@ jobs:
|
|||
|
||||
- name: Upload E2E Results on Failure
|
||||
if: ${{ failure() }}
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: e2e-results-supersync-${{ matrix.shard }}-${{ github.run_id }}
|
||||
path: .tmp/e2e-test-results/**/*.*
|
||||
|
|
|
|||
4
.github/workflows/manual-build.yml
vendored
4
.github/workflows/manual-build.yml
vendored
|
|
@ -62,7 +62,7 @@ jobs:
|
|||
release: false
|
||||
|
||||
- name: 'Upload Artifact'
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: WinBuildStuff
|
||||
path: .tmp/app-builds/*.exe
|
||||
|
|
@ -162,7 +162,7 @@ jobs:
|
|||
# if: always()
|
||||
# run: ls -la && cat notarization-error.log
|
||||
- name: 'Upload Artifact'
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: dmg
|
||||
path: .tmp/app-builds/*.dmg
|
||||
|
|
|
|||
2
.github/workflows/pr-preview-build.yml
vendored
2
.github/workflows/pr-preview-build.yml
vendored
|
|
@ -41,7 +41,7 @@ jobs:
|
|||
run: npm run buildFrontend:prodWeb
|
||||
|
||||
- name: Upload build artifact
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: pr-preview-build
|
||||
path: dist/browser
|
||||
|
|
|
|||
4
.github/workflows/publish-to-hub-docker.yml
vendored
4
.github/workflows/publish-to-hub-docker.yml
vendored
|
|
@ -13,7 +13,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
|
||||
uses: step-security/harden-runner@f808768d1510423e83855289c910610ca9b43176 # v2.17.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
allowed-endpoints: >
|
||||
|
|
@ -55,7 +55,7 @@ jobs:
|
|||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
|
||||
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
|
|
|
|||
4
.github/workflows/supersync-docker.yml
vendored
4
.github/workflows/supersync-docker.yml
vendored
|
|
@ -19,7 +19,7 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
|
||||
uses: step-security/harden-runner@f808768d1510423e83855289c910610ca9b43176 # v2.17.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
allowed-endpoints: >
|
||||
|
|
@ -59,7 +59,7 @@ jobs:
|
|||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
|
||||
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
|
||||
with:
|
||||
context: .
|
||||
file: ./packages/super-sync-server/Dockerfile
|
||||
|
|
|
|||
2
.github/workflows/test-mac-dmg-build.yml
vendored
2
.github/workflows/test-mac-dmg-build.yml
vendored
|
|
@ -117,7 +117,7 @@ jobs:
|
|||
fi
|
||||
|
||||
- name: Upload DMG artifact
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: mac-dmg-build
|
||||
path: .tmp/app-builds/*.dmg
|
||||
|
|
|
|||
|
|
@ -2,7 +2,9 @@ package com.superproductivity.superproductivity.plugins
|
|||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.Process
|
||||
import android.provider.DocumentsContract
|
||||
import androidx.activity.result.ActivityResult
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
|
|
@ -40,18 +42,26 @@ class SafBridgePlugin : Plugin() {
|
|||
@ActivityCallback
|
||||
private fun handleFolderSelectionResult(call: PluginCall, result: ActivityResult) {
|
||||
if (result.resultCode == Activity.RESULT_OK) {
|
||||
val uri = result.data?.data
|
||||
val resultData = result.data
|
||||
val uri = resultData?.data
|
||||
if (uri != null) {
|
||||
// Take persistable permission
|
||||
// Use only the flags that were actually granted by the result intent.
|
||||
// Applying flags not present in the result throws SecurityException on some devices.
|
||||
// resultData is Intent? but uri != null proves resultData is non-null here;
|
||||
// Kotlin cannot infer this transitively, so !! is required and safe.
|
||||
val grantedFlags = resultData!!.flags and
|
||||
(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
||||
try {
|
||||
val takeFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION or
|
||||
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
||||
context.contentResolver.takePersistableUriPermission(uri, takeFlags)
|
||||
context.contentResolver.takePersistableUriPermission(uri, grantedFlags)
|
||||
} catch (e: SecurityException) {
|
||||
// Some devices/Android versions fail to persist SAF permission
|
||||
android.util.Log.w("SafBridgePlugin", "Failed to persist URI permission: ${e.message}")
|
||||
call.reject("Failed to persist folder permission: ${e.message}")
|
||||
return
|
||||
// Some devices/OEM ROMs fail to persist SAF permission even when
|
||||
// FLAG_GRANT_PERSISTABLE_URI_PERMISSION was requested. Fall through and
|
||||
// still return the URI — it will work for this session even if it can't
|
||||
// be persisted across reboots (user will need to re-select next launch).
|
||||
android.util.Log.w(
|
||||
"SafBridgePlugin",
|
||||
"Could not persist URI permission (session-only access): ${e.message}",
|
||||
)
|
||||
}
|
||||
|
||||
val ret = JSObject()
|
||||
|
|
@ -208,8 +218,19 @@ class SafBridgePlugin : Plugin() {
|
|||
|
||||
try {
|
||||
val uri = Uri.parse(uriString)
|
||||
val persistedUris = context.contentResolver.persistedUriPermissions
|
||||
val hasPermission = persistedUris.any { it.uri == uri && it.isReadPermission && it.isWritePermission }
|
||||
// Check both persistent grants (survive reboots) and temporary session grants
|
||||
// (active for current app session only). This is required to support devices/OEM
|
||||
// ROMs where takePersistableUriPermission throws SecurityException — on those
|
||||
// devices the session grant is still valid even though it could not be persisted.
|
||||
val pid = Process.myPid()
|
||||
val uid = Process.myUid()
|
||||
val canRead = context.checkUriPermission(
|
||||
uri, pid, uid, Intent.FLAG_GRANT_READ_URI_PERMISSION,
|
||||
) == PackageManager.PERMISSION_GRANTED
|
||||
val canWrite = context.checkUriPermission(
|
||||
uri, pid, uid, Intent.FLAG_GRANT_WRITE_URI_PERMISSION,
|
||||
) == PackageManager.PERMISSION_GRANTED
|
||||
val hasPermission = canRead && canWrite
|
||||
|
||||
val ret = JSObject()
|
||||
ret.put("hasPermission", hasPermission)
|
||||
|
|
|
|||
513
docs/long-term-plans/location-based-reminders.md
Normal file
513
docs/long-term-plans/location-based-reminders.md
Normal file
|
|
@ -0,0 +1,513 @@
|
|||
# Location-Based Reminders — Design Document
|
||||
|
||||
> **Status: Planned (Brainstorm)**
|
||||
|
||||
## Overview
|
||||
|
||||
Add location-based reminders to Super Productivity. Users can attach a saved location to a task and receive a notification when they arrive at that place. This is primarily a mobile feature (Android/iOS via Capacitor), with passive location display on desktop/web.
|
||||
|
||||
## Decisions
|
||||
|
||||
| Decision | Choice | Rationale |
|
||||
|----------|--------|-----------|
|
||||
| Primary platform | Mobile (Android/iOS) | Geofencing requires GPS + background location. Desktop/web show location info but don't trigger. |
|
||||
| Trigger type | Arrive only (v1) | Simplest. Leave triggers can be added later. |
|
||||
| Location management | Saved locations entity | Users revisit the same places. Follows existing Tag entity pattern. |
|
||||
| Location picker (v1) | "Use current location" + label | No map needed for v1. Map picker is a v2 enhancement. |
|
||||
| Geofencing approach | Custom native Capacitor plugin | `@capacitor/geolocation` only does point-in-time reads, not geofencing. Need native `GeofencingClient` (Android) / `CLLocationManager` (iOS). |
|
||||
| Sync | Sync by default | Location data treated like any other entity. Each device manages its own geofences locally after sync. |
|
||||
| Feature toggle | `isLocationRemindersEnabled` in `AppFeaturesConfig` | Opt-in, default false. |
|
||||
|
||||
---
|
||||
|
||||
## 1. Data Model
|
||||
|
||||
### 1.1 SavedLocation Entity
|
||||
|
||||
New file: `src/app/features/saved-location/saved-location.model.ts`
|
||||
|
||||
```typescript
|
||||
import { EntityState } from '@ngrx/entity';
|
||||
|
||||
export interface SavedLocationCopy {
|
||||
id: string;
|
||||
title: string; // "Office", "Grocery Store", "Gym"
|
||||
lat: number; // latitude
|
||||
lng: number; // longitude
|
||||
radius: number; // geofence radius in meters (default 200)
|
||||
icon?: string | null; // material icon name, e.g. 'home', 'work', 'shopping_cart'
|
||||
created: number; // creation timestamp
|
||||
modified?: number; // last update timestamp
|
||||
}
|
||||
|
||||
export type SavedLocation = Readonly<SavedLocationCopy>;
|
||||
export type SavedLocationState = EntityState<SavedLocation>;
|
||||
```
|
||||
|
||||
### 1.2 Task Model Changes
|
||||
|
||||
File: `src/app/features/tasks/task.model.ts` — add to `TaskCopy`:
|
||||
|
||||
```typescript
|
||||
/** ID of a SavedLocation. When set, a geofence reminder is active for this task. */
|
||||
locationReminderId?: string | null;
|
||||
```
|
||||
|
||||
### 1.3 Config Changes
|
||||
|
||||
File: `src/app/features/config/global-config.model.ts` — add to `AppFeaturesConfig`:
|
||||
|
||||
```typescript
|
||||
isLocationRemindersEnabled: boolean; // default false
|
||||
```
|
||||
|
||||
File: `src/app/features/config/default-global-config.const.ts` — set default:
|
||||
|
||||
```typescript
|
||||
isLocationRemindersEnabled: false,
|
||||
```
|
||||
|
||||
### 1.4 Platform Capabilities
|
||||
|
||||
File: `src/app/core/platform/platform-capabilities.model.ts` — add:
|
||||
|
||||
```typescript
|
||||
/** Whether the platform supports native geofencing (background location monitoring). */
|
||||
readonly geofencing: boolean;
|
||||
```
|
||||
|
||||
| Platform | `geofencing` |
|
||||
|----------|-------------|
|
||||
| Android (`ANDROID_CAPABILITIES`) | `true` |
|
||||
| iOS (`IOS_CAPABILITIES`) | `true` |
|
||||
| Electron (`ELECTRON_CAPABILITIES`) | `false` |
|
||||
| Web (`WEB_CAPABILITIES`) | `false` |
|
||||
|
||||
---
|
||||
|
||||
## 2. Sync & Persistence Registration
|
||||
|
||||
All registration steps required for the new entity, following existing patterns:
|
||||
|
||||
### 2.1 Entity Type
|
||||
|
||||
File: `packages/shared-schema/src/entity-types.ts`
|
||||
|
||||
Add `'SAVED_LOCATION'` to the `ENTITY_TYPES` array.
|
||||
|
||||
### 2.2 Action Types Enum
|
||||
|
||||
File: `src/app/op-log/core/action-types.enum.ts`
|
||||
|
||||
```typescript
|
||||
// SavedLocation actions
|
||||
SAVED_LOCATION_ADD = '[SavedLocation] Add SavedLocation',
|
||||
SAVED_LOCATION_UPDATE = '[SavedLocation] Update SavedLocation',
|
||||
SAVED_LOCATION_DELETE = '[SavedLocation] Delete SavedLocation',
|
||||
```
|
||||
|
||||
These string values are **immutable** once deployed — they are used for encoding/decoding operations in IndexedDB and sync between clients.
|
||||
|
||||
### 2.3 Entity Registry
|
||||
|
||||
File: `src/app/op-log/core/entity-registry.ts`
|
||||
|
||||
Add to `ENTITY_CONFIGS`:
|
||||
|
||||
```typescript
|
||||
SAVED_LOCATION: {
|
||||
storagePattern: 'adapter',
|
||||
featureName: SAVED_LOCATION_FEATURE_NAME,
|
||||
payloadKey: 'savedLocation',
|
||||
adapter: savedLocationAdapter,
|
||||
selectEntities: createSelector(
|
||||
selectSavedLocationFeatureState,
|
||||
selectSavedLocationEntitiesFromAdapter,
|
||||
),
|
||||
selectById: selectSavedLocationById,
|
||||
},
|
||||
```
|
||||
|
||||
### 2.4 Model Config
|
||||
|
||||
File: `src/app/op-log/model/model-config.ts`
|
||||
|
||||
Add to `AllModelConfig` type:
|
||||
|
||||
```typescript
|
||||
savedLocation: ModelCfg<SavedLocationState>;
|
||||
```
|
||||
|
||||
Add to `MODEL_CONFIGS`:
|
||||
|
||||
```typescript
|
||||
savedLocation: {
|
||||
defaultData: initialSavedLocationState,
|
||||
isMainFileModel: true,
|
||||
repair: fixEntityStateConsistency,
|
||||
},
|
||||
```
|
||||
|
||||
### 2.5 Root State
|
||||
|
||||
File: `src/app/root-store/root-state.ts`
|
||||
|
||||
```typescript
|
||||
[SAVED_LOCATION_FEATURE_NAME]: SavedLocationState;
|
||||
```
|
||||
|
||||
### 2.6 Feature Store Registration
|
||||
|
||||
File: `src/app/root-store/feature-stores.module.ts`
|
||||
|
||||
```typescript
|
||||
StoreModule.forFeature(SAVED_LOCATION_FEATURE_NAME, savedLocationReducer),
|
||||
```
|
||||
|
||||
### 2.7 Cascading Delete Meta-Reducer
|
||||
|
||||
File: `src/app/root-store/meta/task-shared-meta-reducers/saved-location-shared.reducer.ts`
|
||||
|
||||
When a SavedLocation is deleted, clear `locationReminderId` on all tasks that reference it. This must be a meta-reducer (not an effect) to ensure atomicity — one operation in the sync log.
|
||||
|
||||
Register in `src/app/root-store/meta/meta-reducer-registry.ts` in **Phase 5** (Entity-Specific Cascades), alongside `tagSharedMetaReducer`, `projectSharedMetaReducer`, etc.
|
||||
|
||||
---
|
||||
|
||||
## 3. NgRx Store
|
||||
|
||||
### 3.1 Actions
|
||||
|
||||
File: `src/app/features/saved-location/store/saved-location.actions.ts`
|
||||
|
||||
Following the Tag action pattern with `PersistentActionMeta`:
|
||||
|
||||
| Action | OpType | Payload |
|
||||
|--------|--------|---------|
|
||||
| `addSavedLocation` | `Create` | `{ savedLocation: SavedLocation }` |
|
||||
| `updateSavedLocation` | `Update` | `{ savedLocation: Update<SavedLocation> }` |
|
||||
| `deleteSavedLocation` | `Delete` | `{ id: string }` |
|
||||
|
||||
All include `meta: { isPersistent: true, entityType: 'SAVED_LOCATION', entityId, opType } satisfies PersistentActionMeta`.
|
||||
|
||||
### 3.2 Reducer
|
||||
|
||||
File: `src/app/features/saved-location/store/saved-location.reducer.ts`
|
||||
|
||||
Standard `@ngrx/entity` adapter:
|
||||
|
||||
```typescript
|
||||
export const SAVED_LOCATION_FEATURE_NAME = 'savedLocation';
|
||||
export const savedLocationAdapter = createEntityAdapter<SavedLocation>({
|
||||
sortComparer: (a, b) => a.title.localeCompare(b.title),
|
||||
});
|
||||
export const initialSavedLocationState = savedLocationAdapter.getInitialState();
|
||||
```
|
||||
|
||||
### 3.3 Selectors
|
||||
|
||||
File: `src/app/features/saved-location/store/saved-location.selectors.ts`
|
||||
|
||||
| Selector | Returns |
|
||||
|----------|---------|
|
||||
| `selectSavedLocationFeatureState` | Feature state |
|
||||
| `selectAllSavedLocations` | `SavedLocation[]` |
|
||||
| `selectSavedLocationById` | `SavedLocation \| undefined` |
|
||||
| `selectSavedLocationEntities` | `Dictionary<SavedLocation>` |
|
||||
|
||||
Task-side selector (in task selectors or a cross-feature selector):
|
||||
|
||||
| Selector | Returns |
|
||||
|----------|---------|
|
||||
| `selectTasksWithLocationReminder` | All undone tasks that have `locationReminderId` set |
|
||||
|
||||
### 3.4 Service
|
||||
|
||||
File: `src/app/features/saved-location/saved-location.service.ts`
|
||||
|
||||
Thin wrapper dispatching actions to the store. Methods: `addSavedLocation()`, `updateSavedLocation()`, `deleteSavedLocation()`, `getById$()`.
|
||||
|
||||
---
|
||||
|
||||
## 4. Geofencing Service
|
||||
|
||||
### 4.1 Architecture
|
||||
|
||||
New file: `src/app/features/saved-location/geofence.service.ts`
|
||||
|
||||
```
|
||||
Store (undone tasks with locationReminderId)
|
||||
→ GeofenceService watches selector (distinctUntilChanged)
|
||||
→ Computes which locations need active geofences
|
||||
→ Registers/unregisters via custom Capacitor plugin
|
||||
→ Receives geofence enter events
|
||||
→ Emits to ReminderService for dialog/notification
|
||||
```
|
||||
|
||||
### 4.2 Why a Custom Capacitor Plugin
|
||||
|
||||
`@capacitor/geolocation` only provides one-time and continuous position reads — it does **not** support geofencing. Native geofencing requires:
|
||||
|
||||
- **Android:** `com.google.android.gms.location.GeofencingClient` (Google Play Services). Supports up to 100 geofences. Fires `BroadcastReceiver` on enter/exit.
|
||||
- **iOS:** `CLLocationManager.startMonitoring(for: CLCircularRegion)`. Supports up to 20 monitored regions. Fires delegate callbacks on enter/exit.
|
||||
|
||||
Implementation: Create `GeofencePlugin.kt` (Android) and `GeofencePlugin.swift` (iOS) extending Capacitor's `Plugin` class. Register in `CapacitorMainActivity.onCreate()` following the pattern of `SafBridgePlugin`, `WebDavHttpPlugin`, etc.
|
||||
|
||||
### 4.3 Lifecycle Rules
|
||||
|
||||
**Register geofence when:**
|
||||
- Feature is enabled + a task gets `locationReminderId` assigned
|
||||
- App starts with existing location-reminded tasks
|
||||
|
||||
**Unregister geofence when:**
|
||||
- Task is completed, deleted, or `locationReminderId` cleared
|
||||
- SavedLocation is deleted
|
||||
- No more undone tasks reference that location
|
||||
- Feature is disabled
|
||||
|
||||
**iOS 20-region limit:** Only register geofences for the 20 locations with the most active tasks. Re-evaluate when tasks change.
|
||||
|
||||
### 4.4 Effects
|
||||
|
||||
Effects MUST use `inject(LOCAL_ACTIONS)` — geofence registration should never happen during remote sync replay. Each device manages its own geofences based on local state after sync.
|
||||
|
||||
### 4.5 Background Behavior
|
||||
|
||||
When geofence fires while app is in background:
|
||||
- **Android:** `BroadcastReceiver` shows native notification directly (same pattern as `ReminderAlarmReceiver`). Tapping opens app + reminder dialog.
|
||||
- **iOS:** `CLLocationManager` delegate fires local notification. Tapping opens app + reminder dialog.
|
||||
|
||||
### 4.6 Permission Flow
|
||||
|
||||
On first use:
|
||||
1. Check `CapacitorPlatformService.hasCapability('geofencing')`
|
||||
2. Request `ACCESS_FINE_LOCATION` + `ACCESS_BACKGROUND_LOCATION` (Android) or "Always" location access (iOS)
|
||||
3. If denied: feature degrades to display-only, show explanation
|
||||
|
||||
---
|
||||
|
||||
## 5. Reminder Integration
|
||||
|
||||
### 5.1 Integration Point
|
||||
|
||||
The existing `ReminderService.onRemindersActive$` is a derived observable — external code cannot emit to it directly. Two approaches:
|
||||
|
||||
**Option A (recommended):** Add a new public subject on `ReminderService`:
|
||||
|
||||
```typescript
|
||||
// In ReminderService
|
||||
private _onLocationReminders$ = new Subject<TaskWithReminderData[]>();
|
||||
locationReminders$ = this._onLocationReminders$.asObservable();
|
||||
```
|
||||
|
||||
Then merge in `ReminderModule`:
|
||||
|
||||
```typescript
|
||||
merge(
|
||||
this._reminderService.onRemindersActive$,
|
||||
this._reminderService.locationReminders$,
|
||||
).subscribe(reminders => /* existing dialog handling */);
|
||||
```
|
||||
|
||||
**Option B:** Add a public method `emitLocationReminders()` that pushes to the private `_onRemindersActive$` subject.
|
||||
|
||||
### 5.2 Data Shape
|
||||
|
||||
Location-triggered reminders must satisfy `TaskWithReminderData`:
|
||||
|
||||
```typescript
|
||||
interface TaskWithReminderData extends Task {
|
||||
readonly reminderData: { remindAt: number }; // use trigger timestamp
|
||||
readonly parentData?: Task;
|
||||
readonly isDeadlineReminder?: boolean; // false for location
|
||||
}
|
||||
```
|
||||
|
||||
Consider adding `isLocationReminder?: boolean` for UI differentiation.
|
||||
|
||||
### 5.3 Dialog UX Changes
|
||||
|
||||
The existing `DialogViewTaskRemindersComponent` works as-is with minor additions:
|
||||
- Show location name/icon in header (e.g., "At: Grocery Store")
|
||||
- **Snooze** = suppress this location reminder for 1 hour (temporary geofence pause)
|
||||
- **Dismiss** = clear `locationReminderId` on the task
|
||||
- **Done** = mark task complete (same as today)
|
||||
- Hide "Edit Reminder" time picker for location-triggered reminders
|
||||
|
||||
### 5.4 Native Notifications (Background)
|
||||
|
||||
Reuse `CapacitorReminderService` patterns:
|
||||
- Android: native notification via `ReminderNotificationHelper` triggered from geofence `BroadcastReceiver`
|
||||
- iOS: `LocalNotifications.schedule()` triggered from `CLLocationManager` delegate
|
||||
- Both support Done/Snooze action buttons
|
||||
|
||||
---
|
||||
|
||||
## 6. UI Components
|
||||
|
||||
### 6.1 New Components
|
||||
|
||||
| Component | Path | Purpose |
|
||||
|-----------|------|---------|
|
||||
| `saved-location-settings` | `src/app/features/saved-location/saved-location-settings/` | CRUD list in settings page |
|
||||
| `location-picker-dialog` | `src/app/features/saved-location/location-picker-dialog/` | Assign location to task (dropdown + "Use current location") |
|
||||
|
||||
### 6.2 Modified Components
|
||||
|
||||
| Component | Change |
|
||||
|-----------|--------|
|
||||
| Task detail panel | Add "Location" field showing assigned location |
|
||||
| Task schedule dialog | Add location option alongside time-based reminder |
|
||||
| Settings page | Add "Locations" section |
|
||||
| Reminder dialog | Show location name when triggered by geofence |
|
||||
|
||||
### 6.3 Location Picker UX
|
||||
|
||||
```
|
||||
┌─────────────────────────────────┐
|
||||
│ Set Location Reminder │
|
||||
├─────────────────────────────────┤
|
||||
│ │
|
||||
│ 📍 Use Current Location │ ← gets GPS, prompts for label
|
||||
│ │
|
||||
│ ─── Saved Locations ────────── │
|
||||
│ │
|
||||
│ 🏠 Home │
|
||||
│ 🏢 Office │
|
||||
│ 🛒 Grocery Store │
|
||||
│ │
|
||||
│ [ Remove Location ] [ Cancel ] │
|
||||
└─────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 6.4 Settings Section
|
||||
|
||||
```
|
||||
┌─────────────────────────────────┐
|
||||
│ Location Reminders │
|
||||
├─────────────────────────────────┤
|
||||
│ [Toggle] Enable location │
|
||||
│ reminders │
|
||||
│ │
|
||||
│ Saved Locations: │
|
||||
│ ┌─────────────────────────┐ │
|
||||
│ │ 🏠 Home 200m [✏️][🗑]│ │
|
||||
│ │ 🏢 Office 150m [✏️][🗑]│ │
|
||||
│ │ 🛒 Grocery 200m [✏️][🗑]│ │
|
||||
│ └─────────────────────────┘ │
|
||||
│ [ + Add Location ] │
|
||||
└─────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Cross-Platform Behavior
|
||||
|
||||
| Platform | Geofencing | Location Display | Notifications |
|
||||
|----------|-----------|-----------------|---------------|
|
||||
| **Android** | Native `GeofencingClient`, up to 100 fences, background | Yes | Native via `BroadcastReceiver` |
|
||||
| **iOS** | Native `CLLocationManager`, up to 20 regions, background | Yes | Native via `LocalNotifications` |
|
||||
| **Electron** | None | Yes (label only) | None |
|
||||
| **Web** | None | Yes (label only) | None |
|
||||
|
||||
---
|
||||
|
||||
## 8. File Structure
|
||||
|
||||
```
|
||||
src/app/features/saved-location/
|
||||
├── saved-location.model.ts
|
||||
├── saved-location.const.ts # DEFAULT_RADIUS = 200
|
||||
├── saved-location.service.ts
|
||||
├── geofence.service.ts # Capacitor geofencing bridge
|
||||
├── store/
|
||||
│ ├── saved-location.actions.ts
|
||||
│ ├── saved-location.reducer.ts
|
||||
│ └── saved-location.selectors.ts
|
||||
├── saved-location-settings/
|
||||
│ ├── saved-location-settings.component.ts
|
||||
│ └── saved-location-settings.component.html
|
||||
└── location-picker-dialog/
|
||||
├── location-picker-dialog.component.ts
|
||||
└── location-picker-dialog.component.html
|
||||
|
||||
src/app/root-store/meta/task-shared-meta-reducers/
|
||||
└── saved-location-shared.reducer.ts # Cascading delete
|
||||
|
||||
android/app/src/main/java/.../
|
||||
└── GeofencePlugin.kt # Native Android geofencing
|
||||
GeofenceBroadcastReceiver.kt # Handles fence enter events
|
||||
|
||||
ios/App/App/
|
||||
└── GeofencePlugin.swift # Native iOS geofencing
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. All Files to Touch
|
||||
|
||||
### New Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `src/app/features/saved-location/saved-location.model.ts` | Entity interface |
|
||||
| `src/app/features/saved-location/saved-location.const.ts` | Defaults |
|
||||
| `src/app/features/saved-location/saved-location.service.ts` | Service |
|
||||
| `src/app/features/saved-location/geofence.service.ts` | Capacitor bridge |
|
||||
| `src/app/features/saved-location/store/saved-location.actions.ts` | Actions |
|
||||
| `src/app/features/saved-location/store/saved-location.reducer.ts` | Reducer + adapter |
|
||||
| `src/app/features/saved-location/store/saved-location.selectors.ts` | Selectors |
|
||||
| `src/app/features/saved-location/saved-location-settings/*` | Settings UI |
|
||||
| `src/app/features/saved-location/location-picker-dialog/*` | Picker dialog |
|
||||
| `src/app/root-store/meta/task-shared-meta-reducers/saved-location-shared.reducer.ts` | Cascading deletes |
|
||||
| `android/.../GeofencePlugin.kt` | Android native geofencing |
|
||||
| `android/.../GeofenceBroadcastReceiver.kt` | Android fence event handler |
|
||||
| `ios/App/App/GeofencePlugin.swift` | iOS native geofencing |
|
||||
|
||||
### Modified Files
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| `packages/shared-schema/src/entity-types.ts` | Add `'SAVED_LOCATION'` |
|
||||
| `src/app/op-log/core/action-types.enum.ts` | Add `SAVED_LOCATION_ADD/UPDATE/DELETE` |
|
||||
| `src/app/op-log/core/entity-registry.ts` | Add `SAVED_LOCATION` config |
|
||||
| `src/app/op-log/model/model-config.ts` | Add to `AllModelConfig` + `MODEL_CONFIGS` |
|
||||
| `src/app/root-store/root-state.ts` | Add to `RootState` |
|
||||
| `src/app/root-store/feature-stores.module.ts` | Register feature store |
|
||||
| `src/app/root-store/meta/meta-reducer-registry.ts` | Register cascading delete in Phase 5 |
|
||||
| `src/app/features/tasks/task.model.ts` | Add `locationReminderId` field |
|
||||
| `src/app/features/config/global-config.model.ts` | Add `isLocationRemindersEnabled` |
|
||||
| `src/app/features/config/default-global-config.const.ts` | Set default `false` |
|
||||
| `src/app/core/platform/platform-capabilities.model.ts` | Add `geofencing` capability |
|
||||
| `src/app/features/reminder/reminder.service.ts` | Add `locationReminders$` subject |
|
||||
| `src/app/features/reminder/reminder.module.ts` | Merge location reminders into dialog flow |
|
||||
| `android/app/src/main/AndroidManifest.xml` | Add location permissions |
|
||||
| `android/.../CapacitorMainActivity.kt` | Register `GeofencePlugin` |
|
||||
|
||||
---
|
||||
|
||||
## 10. Future Enhancements (Out of Scope)
|
||||
|
||||
- Map picker with OpenStreetMap tiles
|
||||
- Address search / geocoding (Nominatim)
|
||||
- Leave triggers ("remind when I leave the office")
|
||||
- Time + location combos ("at the store, but only after 5 PM")
|
||||
- Project default locations
|
||||
- Location-based task views ("show tasks for where I am now")
|
||||
- WiFi-based triggers (alternative to GPS, works indoors)
|
||||
- Bluetooth beacon triggers
|
||||
|
||||
---
|
||||
|
||||
## 11. Verification Plan
|
||||
|
||||
1. **Unit tests:** SavedLocation reducer, selectors, service, cascading delete meta-reducer
|
||||
2. **Manual mobile testing:**
|
||||
- Create location from "Use current location"
|
||||
- Assign to a task, verify geofence registration
|
||||
- Move in/out of geofence, verify notification fires
|
||||
- Complete task, verify geofence unregistered
|
||||
- Delete location, verify `locationReminderId` cleared on tasks
|
||||
3. **Desktop/web:** Verify location label displays on tasks, no geofencing attempted
|
||||
4. **Sync:** Create location on device A, verify it appears on device B
|
||||
5. **Lint/format:** `npm run lint`, `npm run prettier`, `npm test`
|
||||
|
|
@ -47,9 +47,18 @@ export const test = base.extend<SuperSyncFixtures>({
|
|||
* Also checks server health and skips test if server unavailable.
|
||||
*/
|
||||
testRunId: async ({}, use, testInfo) => {
|
||||
// Check server health once per worker
|
||||
// Check server health once per worker.
|
||||
// Prefer the result pre-computed in globalSetup (stored as an env var before workers
|
||||
// were forked) to avoid a stampede of concurrent HTTP requests from all workers
|
||||
// simultaneously — which can overload the server and produce false negatives.
|
||||
if (serverHealthyCache === null) {
|
||||
serverHealthyCache = await isServerHealthy();
|
||||
if (process.env.SUPERSYNC_SERVER_HEALTHY !== undefined) {
|
||||
serverHealthyCache = process.env.SUPERSYNC_SERVER_HEALTHY === 'true';
|
||||
} else {
|
||||
// Fallback for when tests are run directly without going through globalSetup
|
||||
// (e.g., `npx playwright test --config ... e2e/tests/sync/foo.spec.ts`)
|
||||
serverHealthyCache = await isServerHealthy();
|
||||
}
|
||||
if (!serverHealthyCache) {
|
||||
console.warn(
|
||||
'SuperSync server not healthy at http://localhost:1901 - skipping tests',
|
||||
|
|
@ -69,9 +78,13 @@ export const test = base.extend<SuperSyncFixtures>({
|
|||
* Tests are automatically skipped if the server is not healthy.
|
||||
*/
|
||||
serverHealthy: async ({}, use, testInfo) => {
|
||||
// Only check once per worker
|
||||
// Only check once per worker — use globalSetup env var if available (see testRunId above)
|
||||
if (serverHealthyCache === null) {
|
||||
serverHealthyCache = await isServerHealthy();
|
||||
if (process.env.SUPERSYNC_SERVER_HEALTHY !== undefined) {
|
||||
serverHealthyCache = process.env.SUPERSYNC_SERVER_HEALTHY === 'true';
|
||||
} else {
|
||||
serverHealthyCache = await isServerHealthy();
|
||||
}
|
||||
if (!serverHealthyCache) {
|
||||
console.warn(
|
||||
'SuperSync server not healthy at http://localhost:1901 - skipping tests',
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { FullConfig } from '@playwright/test';
|
|||
import { execSync } from 'child_process';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { isServerHealthy } from './utils/supersync-helpers';
|
||||
|
||||
/**
|
||||
* Warm up the dev server by fetching the app once before any tests start.
|
||||
|
|
@ -69,6 +70,20 @@ const globalSetup = async (config: FullConfig): Promise<void> => {
|
|||
config.projects[0]?.use?.baseURL ||
|
||||
'http://localhost:4242';
|
||||
await warmUpDevServer(baseURL);
|
||||
|
||||
// Check SuperSync server health ONCE here, before workers start.
|
||||
// Without this, each worker checks independently on startup — with many workers
|
||||
// running simultaneously, the concurrent health-check requests can overload the
|
||||
// supersync server and cause false negatives, making workers skip all their tests.
|
||||
// By storing the result in an env var set before workers are forked, every worker
|
||||
// reads the cached result instantly instead of making HTTP requests.
|
||||
const healthy = await isServerHealthy().catch(() => false);
|
||||
process.env.SUPERSYNC_SERVER_HEALTHY = healthy ? 'true' : 'false';
|
||||
if (healthy) {
|
||||
console.log('SuperSync server healthy — supersync tests will run');
|
||||
} else {
|
||||
console.log('SuperSync server not available — supersync tests will be skipped');
|
||||
}
|
||||
};
|
||||
|
||||
export default globalSetup;
|
||||
|
|
|
|||
|
|
@ -191,8 +191,14 @@ export class SuperSyncPage extends BasePage {
|
|||
// piggybacked ops in the response, causing data to sync between clients
|
||||
// outside of explicit syncAndWait(). Set here (not in createSimulatedClient)
|
||||
// so tests using enableWebSocket:true still get immediate uploads.
|
||||
// 3. Block WsTriggeredDownloadService: Even when routeWebSocket() closes the
|
||||
// connection, a WS notification can slip through the moment the connection
|
||||
// opens (before it's closed). Setting __SP_E2E_BLOCK_WS_DOWNLOAD ensures
|
||||
// WsTriggeredDownloadService ignores any such notifications, preventing
|
||||
// uncontrolled background syncs that race with explicit syncAndWait() calls.
|
||||
await this.page.evaluate(() => {
|
||||
(globalThis as any).__SP_E2E_BLOCK_IMMEDIATE_UPLOAD = true;
|
||||
(globalThis as any).__SP_E2E_BLOCK_WS_DOWNLOAD = true;
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -672,8 +672,11 @@ test.describe('@supersync SuperSync E2E', () => {
|
|||
await saveAndGoHomeBtn.click();
|
||||
console.log('[Archive Test] Client B clicked Save and go home (archiving)');
|
||||
|
||||
// Wait for navigation back to work view
|
||||
await clientB.page.waitForURL(/tag\/TODAY/, { timeout: 10000 });
|
||||
// Wait for navigation back to work view.
|
||||
// Use negative lookahead to avoid matching /tag/TODAY/daily-summary prematurely.
|
||||
await clientB.page.waitForURL(/(tag\/TODAY(?!\/daily-summary))/, {
|
||||
timeout: 10000,
|
||||
});
|
||||
await clientB.page.waitForLoadState('networkidle');
|
||||
console.log('[Archive Test] Client B back on work view after archiving');
|
||||
|
||||
|
|
|
|||
|
|
@ -239,8 +239,34 @@ export const createSimulatedClient = async (
|
|||
}
|
||||
});
|
||||
|
||||
// Navigate to app and wait for ready
|
||||
await page.goto('/');
|
||||
// Navigate to app with retry for transient ERR_CONNECTION_REFUSED.
|
||||
// Under parallel load (many workers × 2-3 browser contexts each), the Angular
|
||||
// dev server can temporarily refuse connections. Retrying recovers from this
|
||||
// without failing the test outright.
|
||||
let lastGotoError: Error | null = null;
|
||||
for (let attempt = 0; attempt < 3; attempt++) {
|
||||
try {
|
||||
await page.goto('/');
|
||||
lastGotoError = null;
|
||||
break;
|
||||
} catch (e) {
|
||||
lastGotoError = e as Error;
|
||||
const isConnectionRefused = lastGotoError.message.includes(
|
||||
'ERR_CONNECTION_REFUSED',
|
||||
);
|
||||
if (attempt < 2 && isConnectionRefused) {
|
||||
const delay = 1000 * (attempt + 1);
|
||||
console.log(
|
||||
`[Client ${clientName}] page.goto('/') failed (attempt ${attempt + 1}/3): ERR_CONNECTION_REFUSED — retrying in ${delay}ms`,
|
||||
);
|
||||
await new Promise((r) => setTimeout(r, delay));
|
||||
} else {
|
||||
break; // Non-connection error or last attempt — let it throw below
|
||||
}
|
||||
}
|
||||
}
|
||||
if (lastGotoError) throw lastGotoError;
|
||||
|
||||
await waitForAppReady(page);
|
||||
|
||||
const workView = new WorkViewPage(page, `${clientName}-${testPrefix}`);
|
||||
|
|
|
|||
57
electron/clear-stale-idb-locks.ts
Normal file
57
electron/clear-stale-idb-locks.ts
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
import { join } from 'path';
|
||||
import { readdir, unlink } from 'fs/promises';
|
||||
import { log, warn } from 'electron-log/main';
|
||||
|
||||
/**
|
||||
* Deletes stale LevelDB LOCK files left in the IndexedDB directory.
|
||||
*
|
||||
* When Electron (Chromium) exits uncleanly (e.g., session logout with autostart),
|
||||
* the LevelDB LOCK files inside the IndexedDB backing stores are sometimes not
|
||||
* cleaned up. On the next launch these orphaned files block IndexedDB from opening,
|
||||
* producing the "Internal error opening backing store for indexedDB.open" error.
|
||||
*
|
||||
* This is safe to call because by the time it runs, `requestSingleInstanceLock()`
|
||||
* has already ensured we are the only running instance, so no legitimate process
|
||||
* can hold those locks.
|
||||
*
|
||||
* Only runs on Linux, where this startup race condition has been observed.
|
||||
*
|
||||
* @see https://github.com/electron/electron/issues/18263
|
||||
* @see https://github.com/super-productivity/super-productivity/issues/7191
|
||||
*/
|
||||
export const clearStaleLevelDbLocks = async (userDataPath: string): Promise<void> => {
|
||||
if (process.platform !== 'linux') {
|
||||
return;
|
||||
}
|
||||
|
||||
const idbDir = join(userDataPath, 'IndexedDB');
|
||||
|
||||
let entries: string[];
|
||||
try {
|
||||
entries = await readdir(idbDir);
|
||||
} catch (e: unknown) {
|
||||
// Directory doesn't exist yet (fresh install) — nothing to clean up.
|
||||
// Any other error (e.g., EACCES, EPERM) is unexpected and worth logging.
|
||||
if ((e as NodeJS.ErrnoException).code !== 'ENOENT') {
|
||||
warn(`[clearStaleLevelDbLocks] Could not read IndexedDB directory:`, e);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const leveldbDirs = entries.filter((e) => e.endsWith('.leveldb'));
|
||||
|
||||
await Promise.all(
|
||||
leveldbDirs.map(async (dir) => {
|
||||
const lockPath = join(idbDir, dir, 'LOCK');
|
||||
try {
|
||||
await unlink(lockPath);
|
||||
log(`[clearStaleLevelDbLocks] Removed stale lock: ${lockPath}`);
|
||||
} catch (e: unknown) {
|
||||
// LOCK file doesn't exist or is already released — this is the normal case
|
||||
if ((e as NodeJS.ErrnoException).code !== 'ENOENT') {
|
||||
warn(`[clearStaleLevelDbLocks] Could not remove ${lockPath}:`, e);
|
||||
}
|
||||
}
|
||||
}),
|
||||
);
|
||||
};
|
||||
2
electron/electronAPI.d.ts
vendored
2
electron/electronAPI.d.ts
vendored
|
|
@ -48,7 +48,7 @@ export interface ElectronAPI {
|
|||
|
||||
fileSyncRemove(args: { filePath: string }): Promise<unknown | Error>;
|
||||
|
||||
fileSyncListFiles(args: { dirPath: string }): Promise<string[] | Error>; // NEW
|
||||
fileSyncListFiles(args: { dirPath: string }): Promise<string[] | Error>;
|
||||
|
||||
checkDirExists(args: { dirPath: string }): Promise<true | Error>;
|
||||
|
||||
|
|
|
|||
|
|
@ -112,6 +112,36 @@ export const initLocalFileSyncAdapter = (): void => {
|
|||
return true;
|
||||
} catch (e) {
|
||||
log('ERR: error while checking dir ' + dirPath);
|
||||
if ((e as NodeJS.ErrnoException).code === 'EACCES') {
|
||||
log(
|
||||
'ERR: Permission denied. If running as a snap, ensure the "home" or "removable-media" interface is connected.',
|
||||
);
|
||||
}
|
||||
error(e);
|
||||
return e instanceof Error ? e : new Error(String(e));
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
ipcMain.handle(
|
||||
IPC.FILE_SYNC_LIST_FILES,
|
||||
(
|
||||
ev,
|
||||
{
|
||||
dirPath,
|
||||
}: {
|
||||
dirPath: string;
|
||||
},
|
||||
): string[] | Error => {
|
||||
try {
|
||||
return readdirSync(dirPath);
|
||||
} catch (e) {
|
||||
log('ERR: Sync error while listing files in ' + dirPath);
|
||||
if ((e as NodeJS.ErrnoException).code === 'EACCES') {
|
||||
log(
|
||||
'ERR: Permission denied. If running as a snap, ensure the "home" or "removable-media" interface is connected.',
|
||||
);
|
||||
}
|
||||
error(e);
|
||||
return e instanceof Error ? e : new Error(String(e));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -459,7 +459,7 @@ const appCloseHandler = (app: App): void => {
|
|||
|
||||
mainWin.on('close', (event) => {
|
||||
// NOTE: this might not work if we run a second instance of the app
|
||||
log('close, isQuiting:', getIsQuiting());
|
||||
log('close event: isQuiting=', getIsQuiting(), 'pendingBeforeCloseIds=', ids);
|
||||
if (!getIsQuiting()) {
|
||||
if (getIsMinimizeToTray()) {
|
||||
const indicator = ensureIndicator();
|
||||
|
|
|
|||
|
|
@ -68,8 +68,8 @@ const ea: ElectronAPI = {
|
|||
dataStr: string | undefined;
|
||||
}>,
|
||||
fileSyncRemove: (filePath) => _invoke('FILE_SYNC_REMOVE', filePath) as Promise<void>,
|
||||
fileSyncListFiles: ({ dirPath }) =>
|
||||
_invoke('FILE_SYNC_LIST_FILES', dirPath) as Promise<string[] | Error>,
|
||||
fileSyncListFiles: (args) =>
|
||||
_invoke('FILE_SYNC_LIST_FILES', args) as Promise<string[] | Error>,
|
||||
checkDirExists: (dirPath) =>
|
||||
_invoke('CHECK_DIR_EXISTS', dirPath) as Promise<true | Error>,
|
||||
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ export enum IPC {
|
|||
FILE_SYNC_LOAD = 'FILE_SYNC_LOAD',
|
||||
FILE_SYNC_SAVE = 'FILE_SYNC_SAVE',
|
||||
FILE_SYNC_REMOVE = 'FILE_SYNC_REMOVE',
|
||||
FILE_SYNC_LIST_FILES = 'FILE_SYNC_LIST_FILES', // NEW
|
||||
FILE_SYNC_LIST_FILES = 'FILE_SYNC_LIST_FILES',
|
||||
FILE_SYNC_GET_REV_AND_CLIENT_UPDATE = 'FILE_SYNC_GET_REV_AND_CLIENT_UPDATE',
|
||||
|
||||
CHECK_DIR_EXISTS = 'CHECK_DIR_EXISTS',
|
||||
|
|
|
|||
|
|
@ -13,14 +13,15 @@ import { CONFIG } from './CONFIG';
|
|||
import { lazySetInterval } from './shared-with-frontend/lazy-set-interval';
|
||||
import { initIndicator } from './indicator';
|
||||
import { quitApp, showOrFocus } from './various-shared';
|
||||
import { createWindow } from './main-window';
|
||||
import { createWindow, getWin } from './main-window';
|
||||
import { IdleTimeHandler } from './idle-time-handler';
|
||||
import { destroyTaskWidget } from './task-widget/task-widget';
|
||||
import {
|
||||
initializeProtocolHandling,
|
||||
processPendingProtocolUrls,
|
||||
} from './protocol-handler';
|
||||
import { getIsQuiting, setIsLocked } from './shared-state';
|
||||
import { getIsQuiting, setIsQuiting, setIsLocked } from './shared-state';
|
||||
import { clearStaleLevelDbLocks } from './clear-stale-idb-locks';
|
||||
import * as fs from 'fs';
|
||||
|
||||
const ICONS_FOLDER = __dirname + '/assets/icons/';
|
||||
|
|
@ -356,18 +357,32 @@ export const startApp = (): void => {
|
|||
appIN.on('will-quit', () => {
|
||||
// un-register all shortcuts.
|
||||
globalShortcut.unregisterAll();
|
||||
// Safe to remove IPC listeners here: all windows are closed and before-close
|
||||
// IPC flows (sync, finish-day) are guaranteed to have completed.
|
||||
ipcMain.removeAllListeners();
|
||||
});
|
||||
|
||||
appIN.on('before-quit', () => {
|
||||
log('App before-quit: cleaning up resources');
|
||||
|
||||
// Clean up task widget before quitting
|
||||
appIN.on('before-quit', (event) => {
|
||||
log('App before-quit: isQuiting=', getIsQuiting());
|
||||
if (!getIsQuiting()) {
|
||||
// Native quit path (Cmd+Q, Dock > Quit on macOS): app.quit() was called
|
||||
// by the OS without going through quitApp(), so isQuiting was never set.
|
||||
// Prevent the immediate quit and delegate to the window close handler,
|
||||
// which manages the before-close callback flow (sync, finish-day, etc.)
|
||||
// and sets isQuiting=true before re-quitting.
|
||||
event.preventDefault();
|
||||
const win = getWin();
|
||||
if (win && !win.isDestroyed()) {
|
||||
win.close();
|
||||
} else {
|
||||
// No window to close — set flag and re-trigger quit directly.
|
||||
setIsQuiting(true);
|
||||
app.quit();
|
||||
}
|
||||
return;
|
||||
}
|
||||
// isQuiting=true: all before-close IPC work is complete — safe to clean up.
|
||||
destroyTaskWidget();
|
||||
|
||||
// Remove all IPC listeners to prevent memory leaks
|
||||
ipcMain.removeAllListeners();
|
||||
|
||||
// Clear any pending timeouts/intervals
|
||||
if (global.gc) {
|
||||
global.gc();
|
||||
}
|
||||
|
|
@ -425,6 +440,10 @@ export const startApp = (): void => {
|
|||
|
||||
// eslint-disable-next-line prefer-arrow/prefer-arrow-functions
|
||||
async function createMainWin(): Promise<void> {
|
||||
// Remove stale LevelDB LOCK files before the renderer opens IndexedDB.
|
||||
// Orphaned locks from unclean session shutdowns block the backing store open.
|
||||
await clearStaleLevelDbLocks(app.getPath('userData'));
|
||||
|
||||
mainWin = await createWindow({
|
||||
app,
|
||||
IS_DEV,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"CFG": {
|
||||
"REPO": "Repository (owner/repo)",
|
||||
"TOKEN": "Personal Access Token (optional, for private repos)",
|
||||
"TOKEN": "Personal Access Token (required for private repos/optional for public repos)",
|
||||
"FILTER_USERNAME": "Filter username (ignore own comments for update detection)",
|
||||
"BACKLOG_QUERY": "Backlog query (default: \"sort:updated state:open assignee:@me\")",
|
||||
"HOW_TO_GET_TOKEN": "How to get a token"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<nav
|
||||
class="mobile-bottom-nav"
|
||||
[class.entrance]="isEntrance()"
|
||||
(animationend)="onEntranceAnimationEnd($event)"
|
||||
>
|
||||
<button
|
||||
mat-button
|
||||
|
|
@ -25,7 +26,7 @@
|
|||
mat-fab
|
||||
class="add-task-button"
|
||||
[class.smaller]="hasProjectBacklog()"
|
||||
[class.hide-for-native]="isAndroid && isEntrance()"
|
||||
[class.hide-for-native]="isEntranceAnimating()"
|
||||
color="primary"
|
||||
(click)="showAddTaskBar()"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -105,7 +105,7 @@
|
|||
height: 28px;
|
||||
}
|
||||
|
||||
// On Android, hide during entrance animation (native add-task button is visible).
|
||||
// On Android, hide during the slide-up entrance animation.
|
||||
// var(--transition-standard) on .add-task-button already transitions opacity,
|
||||
// so removing the class fades in automatically.
|
||||
&.hide-for-native {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,12 @@
|
|||
import { ChangeDetectionStrategy, Component, inject, input, output } from '@angular/core';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
effect,
|
||||
inject,
|
||||
input,
|
||||
output,
|
||||
signal,
|
||||
} from '@angular/core';
|
||||
import { NavigationEnd, Router, RouterModule } from '@angular/router';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
|
|
@ -48,6 +56,35 @@ export class MobileBottomNavComponent {
|
|||
isEntrance = input(false);
|
||||
readonly isAndroid = IS_ANDROID_NATIVE;
|
||||
|
||||
// Tracks whether the slide-up entrance animation is actively running.
|
||||
// On Android, the web FAB is hidden only during this animation (not the full
|
||||
// entrance duration) so that the button appears as soon as the nav settles —
|
||||
// even if the native startup overlay was not shown (e.g. process-death recreation).
|
||||
readonly isEntranceAnimating = signal(false);
|
||||
private _entranceAnimationTimeout: ReturnType<typeof setTimeout> | undefined;
|
||||
|
||||
constructor() {
|
||||
effect((onCleanup) => {
|
||||
if (this.isEntrance() && this.isAndroid) {
|
||||
this.isEntranceAnimating.set(true);
|
||||
// Safety fallback: clear if animationend doesn't fire
|
||||
// (e.g. prefers-reduced-motion: reduce → animation: none).
|
||||
// Timeout = 250ms delay + 400ms duration + 50ms buffer = 700ms
|
||||
this._entranceAnimationTimeout = setTimeout(() => {
|
||||
this.isEntranceAnimating.set(false);
|
||||
}, 700);
|
||||
onCleanup(() => clearTimeout(this._entranceAnimationTimeout));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onEntranceAnimationEnd(event: AnimationEvent): void {
|
||||
if (event.animationName === 'slide-up-from-bottom') {
|
||||
this.isEntranceAnimating.set(false);
|
||||
clearTimeout(this._entranceAnimationTimeout);
|
||||
}
|
||||
}
|
||||
|
||||
readonly T = T;
|
||||
readonly TODAY_TAG = TODAY_TAG;
|
||||
readonly todayTagId = TODAY_TAG.id;
|
||||
|
|
|
|||
|
|
@ -475,6 +475,9 @@ export class GlobalThemeService {
|
|||
* Adds/removes CSS classes when keyboard shows/hides.
|
||||
*/
|
||||
private _initIOSKeyboardHandling(): void {
|
||||
// Show the native iOS accessory bar ("Done" button) above the keyboard
|
||||
Keyboard.setAccessoryBarVisible({ isVisible: true });
|
||||
|
||||
Keyboard.addListener('keyboardWillShow', (info: KeyboardInfo) => {
|
||||
Log.log('iOS keyboard will show', info);
|
||||
this.document.body.classList.add(BodyClass.isKeyboardVisible);
|
||||
|
|
|
|||
|
|
@ -207,6 +207,18 @@
|
|||
<mat-icon>done_all</mat-icon>
|
||||
</button>
|
||||
}
|
||||
|
||||
<!-- Reset cycles button (In Progress state, Pomodoro only) -->
|
||||
@if (isShowCompleteSessionButton() && isPomodoro()) {
|
||||
<button
|
||||
mat-icon-button
|
||||
[matTooltip]="T.F.FOCUS_MODE.RESET_CYCLES | translate"
|
||||
(click)="resetCycles()"
|
||||
class="reset-cycles-btn"
|
||||
>
|
||||
<mat-icon>restart_alt</mat-icon>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import {
|
|||
completeTask,
|
||||
focusModeLoaded,
|
||||
pauseFocusSession,
|
||||
resetCycles,
|
||||
selectFocusTask,
|
||||
setFocusModeMode,
|
||||
setFocusSessionDuration,
|
||||
|
|
@ -181,6 +182,7 @@ export class FocusModeMainComponent {
|
|||
isShowTimeAdjustButtons = computed(
|
||||
() => this._isInProgress() && this.mode() !== FocusModeMode.Flowtime,
|
||||
);
|
||||
isPomodoro = computed(() => this.mode() === FocusModeMode.Pomodoro);
|
||||
|
||||
// Play button should be disabled when sync with tracking is enabled but no task is selected
|
||||
isPlayButtonDisabled = computed(() => {
|
||||
|
|
@ -402,6 +404,10 @@ export class FocusModeMainComponent {
|
|||
this._store.dispatch(unPauseFocusSession());
|
||||
}
|
||||
|
||||
resetCycles(): void {
|
||||
this._store.dispatch(resetCycles());
|
||||
}
|
||||
|
||||
selectMode(mode: FocusModeMode | string | number): void {
|
||||
if (!Object.values(FocusModeMode).includes(mode as FocusModeMode)) {
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ import {
|
|||
IssueProviderState,
|
||||
} from '../issue.model';
|
||||
import { ICAL_TYPE } from '../issue.const';
|
||||
import { IssueLog } from '../../../core/log';
|
||||
|
||||
export const selectIssueProviderState = createFeatureSelector<IssueProviderState>(
|
||||
ISSUE_PROVIDER_FEATURE_KEY,
|
||||
|
|
@ -46,10 +45,11 @@ export const selectIssueProviderById = <T extends IssueProvider>(
|
|||
createSelector(selectIssueProviderState, ({ entities }) => {
|
||||
const issueProvider = entities[id];
|
||||
if (!issueProvider) {
|
||||
// Do not include the entity in the error — IssueLog history is exportable
|
||||
// and providers may carry credentials.
|
||||
throw new Error(`No issueProvider found for id ${id}`);
|
||||
}
|
||||
if (issueProviderKey && issueProvider.issueProviderKey !== issueProviderKey) {
|
||||
IssueLog.log(issueProviderKey, issueProvider);
|
||||
throw new Error(
|
||||
`IssueProvider found for id ${id} is not of type ${issueProviderKey} but ${issueProvider.issueProviderKey}`,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -56,7 +56,8 @@
|
|||
.title {
|
||||
flex-shrink: 1;
|
||||
min-width: 0;
|
||||
@include truncateText();
|
||||
overflow: hidden;
|
||||
word-break: break-word;
|
||||
font-size: var(--planner-font-size-mobile);
|
||||
|
||||
@include mq(xs) {
|
||||
|
|
|
|||
|
|
@ -40,7 +40,8 @@
|
|||
font-size: var(--planner-font-size-mobile);
|
||||
flex-shrink: 1;
|
||||
min-width: 0;
|
||||
@include truncateText();
|
||||
overflow: hidden;
|
||||
word-break: break-word;
|
||||
|
||||
@include mq(xs) {
|
||||
font-size: var(--planner-font-size);
|
||||
|
|
|
|||
|
|
@ -40,7 +40,8 @@
|
|||
font-size: var(--planner-font-size-mobile);
|
||||
flex-shrink: 1;
|
||||
min-width: 0;
|
||||
@include truncateText();
|
||||
overflow: hidden;
|
||||
word-break: break-word;
|
||||
|
||||
@include mq(xs) {
|
||||
font-size: var(--planner-font-size);
|
||||
|
|
|
|||
|
|
@ -105,7 +105,8 @@ done-toggle {
|
|||
|
||||
line-height: 1.5;
|
||||
position: relative;
|
||||
@include truncateText();
|
||||
overflow: hidden;
|
||||
word-break: break-word;
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
|
||||
|
|
@ -119,7 +120,8 @@ done-toggle {
|
|||
padding-right: var(--s-half);
|
||||
padding-top: var(--s-half);
|
||||
padding-bottom: var(--s-half);
|
||||
@include truncateText();
|
||||
overflow: hidden;
|
||||
word-break: break-word;
|
||||
line-height: 1.1;
|
||||
font-size: var(--planner-font-size-smaller-mobile);
|
||||
|
||||
|
|
|
|||
|
|
@ -281,9 +281,9 @@ describe('AddTaskBarParserService', () => {
|
|||
});
|
||||
|
||||
describe('Parsing Integration', () => {
|
||||
it('should call updateEstimate when parsing text', async () => {
|
||||
it('should call updateEstimate when text contains estimate syntax', async () => {
|
||||
await service.parseAndUpdateText(
|
||||
'Task with potential time estimate',
|
||||
'Task with estimate 30m',
|
||||
mockConfig,
|
||||
mockProjects,
|
||||
mockTags,
|
||||
|
|
@ -293,7 +293,7 @@ describe('AddTaskBarParserService', () => {
|
|||
expect(mockStateService.updateEstimate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle null time estimates', async () => {
|
||||
it('should not call updateEstimate for text without estimate on first parse', async () => {
|
||||
await service.parseAndUpdateText(
|
||||
'Simple task',
|
||||
mockConfig,
|
||||
|
|
@ -302,7 +302,24 @@ describe('AddTaskBarParserService', () => {
|
|||
mockDefaultProject,
|
||||
);
|
||||
|
||||
expect(mockStateService.updateEstimate).toHaveBeenCalledWith(null);
|
||||
expect(mockStateService.updateEstimate).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not call updateEstimate when typing text without estimate syntax for the first time', async () => {
|
||||
// Simulates: user sets estimate via dropdown, then types task title.
|
||||
// The parser has no previous result (first parse after empty input).
|
||||
// Since the parsed estimate is null and there's no previous result to
|
||||
// diff against, updateEstimate should NOT be called to avoid wiping
|
||||
// out the dropdown-set value.
|
||||
await service.parseAndUpdateText(
|
||||
'My new task',
|
||||
mockConfig,
|
||||
mockProjects,
|
||||
mockTags,
|
||||
mockDefaultProject,
|
||||
);
|
||||
|
||||
expect(mockStateService.updateEstimate).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call updateSpent when parsing text', async () => {
|
||||
|
|
|
|||
|
|
@ -188,8 +188,9 @@ export class AddTaskBarParserService {
|
|||
}
|
||||
|
||||
if (
|
||||
!this._previousParseResult ||
|
||||
this._previousParseResult.timeEstimate !== currentResult.timeEstimate
|
||||
(!this._previousParseResult && currentResult.timeEstimate !== null) ||
|
||||
(this._previousParseResult &&
|
||||
this._previousParseResult.timeEstimate !== currentResult.timeEstimate)
|
||||
) {
|
||||
this._stateService.updateEstimate(currentResult.timeEstimate);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ import {
|
|||
import { GlobalConfigService } from '../../config/global-config.service';
|
||||
import { unique } from '../../../util/unique';
|
||||
import { TaskService } from '../task.service';
|
||||
import { from, of } from 'rxjs';
|
||||
import { EMPTY, from, of } from 'rxjs';
|
||||
import { ProjectService } from '../../project/project.service';
|
||||
import { TagService } from '../../tag/tag.service';
|
||||
import { shortSyntax } from '../short-syntax';
|
||||
|
|
@ -102,6 +102,12 @@ export class ShortSyntaxEffects {
|
|||
),
|
||||
),
|
||||
mergeMap(([{ task, originalAction }, tags, projects, defaultProjectId]) => {
|
||||
// Guard: task may have been archived/deleted while the effect was in flight
|
||||
// (e.g., a concurrent sync applied a moveToArchive op). Skip processing
|
||||
// to prevent "Cannot read properties of undefined" errors.
|
||||
if (!task) {
|
||||
return EMPTY;
|
||||
}
|
||||
const isReplaceTagIds = originalAction.type === TaskSharedActions.updateTask.type;
|
||||
return from(
|
||||
shortSyntax(
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import {
|
|||
pauseFocusSession,
|
||||
showFocusOverlay,
|
||||
startFocusSession,
|
||||
tick,
|
||||
unPauseFocusSession,
|
||||
} from '../../focus-mode/store/focus-mode.actions';
|
||||
import { IPC } from '../../../../../electron/shared-with-frontend/ipc-events.const';
|
||||
|
|
@ -109,6 +110,9 @@ export class TaskElectronEffects {
|
|||
pauseFocusSession,
|
||||
unPauseFocusSession,
|
||||
completeFocusSession,
|
||||
// Keep tray time in sync during focus-mode breaks and focus sessions
|
||||
// without an active task (addTimeSpent is gated on currentTask.id).
|
||||
tick,
|
||||
),
|
||||
|
||||
withLatestFrom(
|
||||
|
|
|
|||
|
|
@ -104,6 +104,29 @@ describe('Task Reducer', () => {
|
|||
expect(state.entities['task1']!.subTaskIds).toContain('subTask3');
|
||||
expect(state.entities['subTask3']).toEqual({ ...newSubTask, parentId: 'task1' });
|
||||
});
|
||||
|
||||
it('should roll up the parent time estimate when adding a subtask with an estimate', () => {
|
||||
const parent = createTask('parent');
|
||||
const subTask = createTask('subTask', { timeEstimate: 2.5 * 60 * 60 * 1000 });
|
||||
const state: TaskState = {
|
||||
...initialTaskState,
|
||||
ids: ['parent'],
|
||||
entities: {
|
||||
parent,
|
||||
},
|
||||
};
|
||||
|
||||
const result = taskReducer(
|
||||
state,
|
||||
fromActions.addSubTask({
|
||||
task: subTask,
|
||||
parentId: 'parent',
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.entities['parent']!.subTaskIds).toEqual(['subTask']);
|
||||
expect(result.entities['parent']!.timeEstimate).toBe(2.5 * 60 * 60 * 1000);
|
||||
});
|
||||
});
|
||||
|
||||
describe('moveSubTask (anchor-based)', () => {
|
||||
|
|
|
|||
|
|
@ -519,7 +519,7 @@ export const taskReducer = createReducer<TaskState>(
|
|||
state,
|
||||
);
|
||||
|
||||
return {
|
||||
const stateWithParentTask: TaskState = {
|
||||
...stateCopy,
|
||||
// update current task to new sub task if parent was current before
|
||||
...(state.currentTaskId === parentId ? { currentTaskId: task.id } : {}),
|
||||
|
|
@ -532,6 +532,8 @@ export const taskReducer = createReducer<TaskState>(
|
|||
},
|
||||
},
|
||||
};
|
||||
|
||||
return reCalcTimesForParentIfParent(parentId, stateWithParentTask);
|
||||
}),
|
||||
|
||||
on(toggleStart, (state) => {
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@ import { DialogScheduleTaskComponent } from '../../planner/dialog-schedule-task/
|
|||
import { DialogDeadlineComponent } from '../dialog-deadline/dialog-deadline.component';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { selectIssueProviderById } from '../../issue/store/issue-provider.selectors';
|
||||
import { IssueLog } from '../../../core/log';
|
||||
import { TaskTitleComponent } from '../../../ui/task-title/task-title.component';
|
||||
import { MatIcon } from '@angular/material/icon';
|
||||
import { TaskListComponent } from '../task-list/task-list.component';
|
||||
|
|
@ -365,9 +366,17 @@ export class TaskDetailPanelComponent implements OnInit, AfterViewInit, OnDestro
|
|||
distinctUntilChanged(),
|
||||
switchMap((issueProviderId) =>
|
||||
issueProviderId
|
||||
? this._store.select(
|
||||
selectIssueProviderById<IssueProviderJira>(issueProviderId, 'JIRA'),
|
||||
)
|
||||
? this._store
|
||||
.select(
|
||||
selectIssueProviderById<IssueProviderJira>(issueProviderId, 'JIRA'),
|
||||
)
|
||||
.pipe(
|
||||
// Orphan issueProviderId — see #7135.
|
||||
catchError((err: unknown) => {
|
||||
IssueLog.warn('Jira header setup skipped', err);
|
||||
return of(null);
|
||||
}),
|
||||
)
|
||||
: of(null),
|
||||
),
|
||||
takeUntilDestroyed(this._destroyRef),
|
||||
|
|
|
|||
|
|
@ -53,7 +53,10 @@ export class DialogGetAndEnterAuthCodeComponent implements OnDestroy {
|
|||
T: typeof T = T;
|
||||
token?: string;
|
||||
|
||||
// Always use manual code entry flow (show input field)
|
||||
// Always use manual code entry flow (show input field on all platforms).
|
||||
// The automatic deep-link redirect flow was reverted: it requires the
|
||||
// redirect URI to be registered in the Dropbox developer console, and
|
||||
// Android may kill the app during auth, losing the in-memory code verifier.
|
||||
readonly isNativePlatform = false;
|
||||
private _authCodeSub?: Subscription;
|
||||
|
||||
|
|
|
|||
|
|
@ -28,6 +28,8 @@ import { loadAllData } from '../../root-store/meta/load-all-data.action';
|
|||
import { bulkApplyHydrationOperations } from '../apply/bulk-hydration.action';
|
||||
import { CLIENT_ID_PROVIDER, ClientIdProvider } from '../util/client-id.provider';
|
||||
import { MAX_VECTOR_CLOCK_SIZE } from '@sp/shared-schema';
|
||||
import { IndexedDBOpenError } from '../core/errors/indexed-db-open.error';
|
||||
import { IDB_OPEN_ERROR_RELOAD_KEY } from './operation-log-hydrator.service';
|
||||
|
||||
describe('OperationLogHydratorService', () => {
|
||||
let service: OperationLogHydratorService;
|
||||
|
|
@ -1319,4 +1321,65 @@ describe('OperationLogHydratorService', () => {
|
|||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('IndexedDB open error handling', () => {
|
||||
let reloadSpy: jasmine.Spy;
|
||||
|
||||
beforeEach(() => {
|
||||
sessionStorage.removeItem(IDB_OPEN_ERROR_RELOAD_KEY);
|
||||
if (!jasmine.isSpy(window.alert)) {
|
||||
spyOn(window, 'alert');
|
||||
}
|
||||
(window.alert as jasmine.Spy).calls.reset();
|
||||
reloadSpy = spyOn(service as any, '_triggerReload');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
sessionStorage.removeItem(IDB_OPEN_ERROR_RELOAD_KEY);
|
||||
});
|
||||
|
||||
it('should silently auto-reload for backing store error on first occurrence', async () => {
|
||||
const err = new IndexedDBOpenError(
|
||||
new Error('Internal error opening backing store for indexedDB.open'),
|
||||
);
|
||||
mockRecoveryService.recoverPendingRemoteOps.and.rejectWith(err);
|
||||
|
||||
await expectAsync(service.hydrateStore()).toBeRejected();
|
||||
|
||||
expect(reloadSpy).toHaveBeenCalledTimes(1);
|
||||
expect(sessionStorage.getItem(IDB_OPEN_ERROR_RELOAD_KEY)).toBe('1');
|
||||
// No dialog on first attempt — autostart users aren't watching
|
||||
expect(window.alert).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should show recovery dialog and NOT auto-reload when already reloaded once', async () => {
|
||||
sessionStorage.setItem(IDB_OPEN_ERROR_RELOAD_KEY, '1');
|
||||
const err = new IndexedDBOpenError(
|
||||
new Error('Internal error opening backing store for indexedDB.open'),
|
||||
);
|
||||
mockRecoveryService.recoverPendingRemoteOps.and.rejectWith(err);
|
||||
|
||||
await expectAsync(service.hydrateStore()).toBeRejected();
|
||||
|
||||
expect(reloadSpy).not.toHaveBeenCalled();
|
||||
expect(window.alert).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should NOT auto-reload for non-backing-store IDB open errors', async () => {
|
||||
const err = new IndexedDBOpenError(new Error('QuotaExceededError'));
|
||||
mockRecoveryService.recoverPendingRemoteOps.and.rejectWith(err);
|
||||
|
||||
await expectAsync(service.hydrateStore()).toBeRejected();
|
||||
|
||||
expect(reloadSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should clear the reload key after successful hydration', async () => {
|
||||
sessionStorage.setItem(IDB_OPEN_ERROR_RELOAD_KEY, '1');
|
||||
// Successful hydration — no errors thrown
|
||||
await service.hydrateStore();
|
||||
|
||||
expect(sessionStorage.getItem(IDB_OPEN_ERROR_RELOAD_KEY)).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -27,6 +27,13 @@ import { MAX_CONFLICT_RETRY_ATTEMPTS } from '../core/operation-log.const';
|
|||
import { AppDataComplete } from '../model/model-config';
|
||||
import { CLIENT_ID_PROVIDER, ClientIdProvider } from '../util/client-id.provider';
|
||||
import { limitVectorClockSize } from '../../core/util/vector-clock';
|
||||
import { IS_ELECTRON } from '../../app.constants';
|
||||
|
||||
/**
|
||||
* sessionStorage key used to track auto-reload attempts after IndexedDB backing store errors.
|
||||
* Exported for use in tests.
|
||||
*/
|
||||
export const IDB_OPEN_ERROR_RELOAD_KEY = 'sp_idb_open_reload_attempt';
|
||||
|
||||
/**
|
||||
* Handles the hydration (loading) of the application state from the operation log
|
||||
|
|
@ -117,6 +124,7 @@ export class OperationLogHydratorService {
|
|||
'OperationLogHydratorService: Snapshot is invalid/corrupted. Attempting recovery...',
|
||||
);
|
||||
await this.recoveryService.attemptRecovery();
|
||||
sessionStorage.removeItem(IDB_OPEN_ERROR_RELOAD_KEY);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -280,6 +288,7 @@ export class OperationLogHydratorService {
|
|||
OpLog.normal(
|
||||
'OperationLogHydratorService: Fresh install detected. No data to load.',
|
||||
);
|
||||
sessionStorage.removeItem(IDB_OPEN_ERROR_RELOAD_KEY);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -362,6 +371,11 @@ export class OperationLogHydratorService {
|
|||
// Retry any failed remote ops from previous conflict resolution attempts
|
||||
// Now that state is fully hydrated, dependencies might be resolved
|
||||
await this.retryFailedRemoteOps();
|
||||
|
||||
// Clear the auto-reload guard so that a fresh backing-store error in the same
|
||||
// tab session gets the auto-reload treatment again rather than going straight
|
||||
// to the manual recovery dialog.
|
||||
sessionStorage.removeItem(IDB_OPEN_ERROR_RELOAD_KEY);
|
||||
} catch (e) {
|
||||
OpLog.err('OperationLogHydratorService: Error during hydration', e);
|
||||
|
||||
|
|
@ -658,6 +672,32 @@ export class OperationLogHydratorService {
|
|||
? error.originalError.message
|
||||
: String(error.originalError);
|
||||
|
||||
// Hoist platform detection — used in both branches below to avoid computing twice
|
||||
const isFlatpak = IS_ELECTRON && window.ea?.isFlatpak?.();
|
||||
const isSnap = !isFlatpak && IS_ELECTRON && window.ea?.isSnap?.();
|
||||
|
||||
// For backing-store errors (common during Linux session startup with autostart),
|
||||
// auto-reload once after the user dismisses the dialog. By the time the dialog
|
||||
// is dismissed the OS / Flatpak sandbox will usually have finished initializing.
|
||||
// A sessionStorage counter prevents an infinite reload loop on genuine errors.
|
||||
if (error.isBackingStoreError) {
|
||||
const reloadCount = +(sessionStorage.getItem(IDB_OPEN_ERROR_RELOAD_KEY) || '0');
|
||||
if (reloadCount === 0) {
|
||||
// Silent auto-reload on first occurrence — most likely a transient startup
|
||||
// timing issue (Flatpak sandbox not ready, stale LOCK file). The user is
|
||||
// typically not watching (autostart scenario), so a blocking dialog requiring
|
||||
// a click before the reload is unnecessary friction. If the reload fixes it,
|
||||
// the user never needs to know. If it fails again, the dialog below runs.
|
||||
OpLog.warn(
|
||||
'IndexedDB backing-store error on first attempt — triggering silent auto-reload',
|
||||
);
|
||||
sessionStorage.setItem(IDB_OPEN_ERROR_RELOAD_KEY, '1');
|
||||
this._triggerReload();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Second failure, or non-backing-store error: show full manual recovery instructions.
|
||||
let message =
|
||||
'Database Error - Cannot Load Data\n\n' +
|
||||
'Super Productivity cannot open its database. ' +
|
||||
|
|
@ -671,7 +711,11 @@ export class OperationLogHydratorService {
|
|||
'Recovery steps:\n' +
|
||||
'1. Close ALL browser tabs and windows\n' +
|
||||
'2. Restart the app\n' +
|
||||
'3. If using Linux Snap, try: snap set core experimental.refresh-app-awareness=true\n' +
|
||||
(isFlatpak
|
||||
? '3. If using Linux Flatpak with autostart, try disabling autostart and launching manually\n'
|
||||
: isSnap
|
||||
? '3. If using Linux Snap, try: snap set core experimental.refresh-app-awareness=true\n'
|
||||
: '3. If using Linux with autostart, try disabling autostart and launching manually\n') +
|
||||
'4. If issue persists, check available disk space\n\n';
|
||||
}
|
||||
|
||||
|
|
@ -682,4 +726,16 @@ export class OperationLogHydratorService {
|
|||
|
||||
alertDialog(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers an app reload. Uses Electron IPC in Electron context, browser reload otherwise.
|
||||
* Extracted as a method to allow spying in unit tests.
|
||||
*/
|
||||
private _triggerReload(): void {
|
||||
if (IS_ELECTRON) {
|
||||
window.ea.reloadMainWin();
|
||||
} else {
|
||||
window.location.reload();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -317,11 +317,12 @@ export class Dropbox implements SyncProviderServiceInterface<SyncProviderId.Drop
|
|||
/**
|
||||
* Gets the OAuth redirect URI.
|
||||
*
|
||||
* Always returns null to use Dropbox's manual code entry flow.
|
||||
* User must copy the code from Dropbox's page and paste it manually.
|
||||
* This works reliably across all platforms (web, Electron, Android, iOS).
|
||||
*
|
||||
* @returns null for manual code entry flow
|
||||
* Returns null on all platforms to use Dropbox's manual code entry flow.
|
||||
* User copies the code from Dropbox's page and pastes it manually.
|
||||
* This is the most reliable cross-platform approach — the automatic deep-link
|
||||
* redirect flow was reverted because the Dropbox developer console redirect URI
|
||||
* registration is uncertain and Android may kill the app during auth, losing
|
||||
* the in-memory code verifier.
|
||||
*/
|
||||
private _getRedirectUri(): string | null {
|
||||
return null;
|
||||
|
|
|
|||
|
|
@ -3,5 +3,5 @@ export interface FileAdapter {
|
|||
writeFile(filePath: string, dataStr: string): Promise<void>;
|
||||
deleteFile(filePath: string): Promise<void>;
|
||||
checkDirExists?(dirPath: string): Promise<boolean>;
|
||||
listFiles?(dirPath: string): Promise<string[]>; // NEW
|
||||
listFiles?(dirPath: string): Promise<string[]>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ export const isOperationSyncCapable = (
|
|||
|
||||
/**
|
||||
* Type guard to check if a provider uses file-based operation sync.
|
||||
* File-based providers (WebDAV, Dropbox, LocalFile) use file storage for sync.
|
||||
* File-based providers (WebDAV, Dropbox, LocalFile, Nextcloud) use file storage for sync.
|
||||
*/
|
||||
export const isFileBasedProvider = (
|
||||
provider: SyncProviderBase<SyncProviderId>,
|
||||
|
|
|
|||
|
|
@ -18,6 +18,13 @@
|
|||
padding-top: var(--mac-title-bar-padding);
|
||||
}
|
||||
|
||||
// On iOS, the virtual keyboard overlaps position:fixed elements since the CDK
|
||||
// overlay container does not shrink with Capacitor's resize:'native' mode.
|
||||
// Subtract the keyboard height so Save/Close buttons stay above the keyboard.
|
||||
:host-context(body.isNativeMobile.isKeyboardVisible) {
|
||||
height: calc(100% - var(--keyboard-height, 0px));
|
||||
}
|
||||
|
||||
.formatting-toolbar {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ const base64UrlEncode = (buffer: Uint8Array): string => {
|
|||
* Uses crypto.subtle when available, otherwise falls back to hash-wasm.
|
||||
*/
|
||||
const sha256 = async (data: Uint8Array): Promise<ArrayBuffer> => {
|
||||
if (typeof crypto !== 'undefined' && crypto.subtle !== undefined) {
|
||||
if (typeof crypto !== 'undefined' && crypto.subtle != null) {
|
||||
return crypto.subtle.digest('SHA-256', data as BufferSource);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@
|
|||
"DONATE_PAGE": {
|
||||
"BUTTON_TEXT": "Wesprzyj przez GitHub Sponsors",
|
||||
"INTRO_1": "Super Productivity jest finansowany w całości przez społeczność. Nie ma śledzenia, reklam ani zbierania danych — Twoje zadania pozostają na Twoim urządzeniu.",
|
||||
"INTRO_2": "Jeśli cenisz takie podejście i chcesz, aby projekt był zdrowy i się rozwijał, dobrowolna darowizna jest bardzo mile widziana."
|
||||
"INTRO_2": "Jeśli cenisz takie podejście i chcesz, aby projekt był utrzymywany w dobrej kondycji i się rozwijał, dobrowolna darowizna jest bardzo mile widziana."
|
||||
},
|
||||
"EXAMPLE_TASKS": {
|
||||
"LEARN_KEYBOARD_SHORTCUTS": {
|
||||
|
|
@ -167,8 +167,18 @@
|
|||
"EVENT_STR": "Wydarzenie",
|
||||
"EVENTS_STR": "Wydarzenia"
|
||||
},
|
||||
"CONTEXT_MENU": {
|
||||
"CREATE_TASK": "Utwórz jako zadanie",
|
||||
"OPEN_IN_CALENDAR": "Otwórz w kalendarzu",
|
||||
"RESCHEDULE": "Reschedule",
|
||||
"DELETE_EVENT": "Usuń zdarzenie"
|
||||
},
|
||||
"S": {
|
||||
"CAL_PROVIDER_ERROR": "Błąd dostawcy kalendarza: {{errTxt}}"
|
||||
"CAL_PROVIDER_ERROR": "Błąd dostawcy kalendarza: {{errTxt}}",
|
||||
"EVENT_HIDDEN": "Calendar event hidden",
|
||||
"EVENT_RESCHEDULED": "Event rescheduled",
|
||||
"EVENT_DELETED": "Event deleted",
|
||||
"TIME_BLOCK_ERROR": "Failed to sync time block: {{errTxt}}"
|
||||
}
|
||||
},
|
||||
"CLIPBOARD_IMAGE": {
|
||||
|
|
@ -265,7 +275,7 @@
|
|||
},
|
||||
"GITEA": {
|
||||
"FORM": {
|
||||
"HOST": "Host (np.: https://try.gitea.io)",
|
||||
"HOST": "Host (np. https://try.gitea.io)",
|
||||
"REPO_FULL_NAME": "Nazwa użytkownika lub nazwa organizacji/projektu",
|
||||
"REPO_FULL_NAME_DESCRIPTION": "Można go znaleźć jako część adresu URL podczas przeglądania projektu w przeglądarce.",
|
||||
"SCOPE": "Zakres",
|
||||
|
|
@ -422,7 +432,15 @@
|
|||
"ADVANCED_CONFIG": "Zaawansowana konfiguracja",
|
||||
"CONNECTED": "Połączone",
|
||||
"DELETE_CONFIRM": "Czy na pewno chcesz usunąć tego dostawcę zgłoszeń? Usunięcie oznacza, że <strong>wszystkie wcześniej zaimportowane zadania zgłoszeń stracą swoje powiązanie</strong>. Tej operacji nie można cofnąć!",
|
||||
"DISCONNECT": "Rozłączone"
|
||||
"DISCONNECT": "Rozłączone",
|
||||
"EDIT_TITLE": "Edit {{title}} Config",
|
||||
"LOAD_OPTIONS": "Load Options",
|
||||
"LOAD_OPTIONS_FAILED": "Failed to load options",
|
||||
"LOADING_OPTIONS": "Loading options...",
|
||||
"OPTIONS_LOADED": "Options loaded",
|
||||
"RELOAD_OPTIONS": "Reload Options",
|
||||
"SETUP_TITLE": "Setup {{title}} Config",
|
||||
"TEST_CONNECTION": "Test Connection"
|
||||
}
|
||||
},
|
||||
"ISSUE_PANEL": {
|
||||
|
|
@ -494,7 +512,7 @@
|
|||
},
|
||||
"FORM_CRED": {
|
||||
"ALLOW_SELF_SIGNED": "Zezwalaj na certyfikat self-signed",
|
||||
"HOST": "Host (np.: http://mój-host.pl:1234)",
|
||||
"HOST": "Host (np. http://moja-domena.pl:1234)",
|
||||
"PASSWORD": "Token / hasło",
|
||||
"USE_PAT": "Zamiast tego użyj osobistego tokena dostępu (LEGACY)",
|
||||
"USER_NAME": "E-mail / nazwa użytkownika",
|
||||
|
|
@ -692,13 +710,13 @@
|
|||
"STRIKETHROUGH": "Przekreślenie",
|
||||
"TASK_LIST": "Lista zadań"
|
||||
},
|
||||
"VIEW_PARSED": "Wyświetlanie jako przeanalizowany (nieedytowalny) markdown",
|
||||
"VIEW_SPLIT": "Wyświetlanie przeanalizowanego i nieprzeanalizowanego markdown w widoku podzielonym",
|
||||
"VIEW_TEXT_ONLY": "Wyświetlanie jako nieprzeanalizowany tekst"
|
||||
"VIEW_PARSED": "Wyświetl jako sparsowany (nieedytowalny) markdown",
|
||||
"VIEW_SPLIT": "Wyświetl jako sparsowany i niesformatowany Markdown w widoku podzielonym",
|
||||
"VIEW_TEXT_ONLY": "Wyświetl jako niesparsowany tekst"
|
||||
},
|
||||
"NOTE_CMP": {
|
||||
"DISABLE_PARSE": "Wyłącz markdown parsing",
|
||||
"ENABLE_PARSE": "Włącz markdown parse"
|
||||
"DISABLE_PARSE": "Wyłącz parsowanie Markdown przy podglądzie",
|
||||
"ENABLE_PARSE": "Włącz parsowanie Markdown"
|
||||
},
|
||||
"NOTES_CMP": {
|
||||
"ADD_BTN": "Dodaj nową notatkę",
|
||||
|
|
@ -931,7 +949,7 @@
|
|||
},
|
||||
"END": "Koniec pracy",
|
||||
"INSERT_BEFORE": "Przed",
|
||||
"LUNCH_BREAK": "Przerwa obiadowa",
|
||||
"LUNCH_BREAK": "Przerwa na lunch",
|
||||
"MONTH": "Miesiąc",
|
||||
"NO_TASKS": "Obecnie nie ma żadnych zadań. Dodaj zadania za pomocą przycisku plus (+) na górnym pasku.",
|
||||
"NOW": "Teraz",
|
||||
|
|
@ -951,16 +969,16 @@
|
|||
"OK": "Zrób to!"
|
||||
},
|
||||
"D_EDIT": {
|
||||
"CURRENT_STREAK": "Aktualna passa",
|
||||
"CURRENT_STREAK": "Obecna seria",
|
||||
"DAILY_GOAL": "Cel dzienny",
|
||||
"DAYS": "Dni",
|
||||
"L_COUNTER": "Licznik"
|
||||
"L_COUNTER": "Stan"
|
||||
},
|
||||
"FORM": {
|
||||
"ADD_NEW": "Dodaj prosty licznik",
|
||||
"HELP": "Tutaj możesz skonfigurować proste przyciski, które pojawią się w prawym górnym rogu. Mogą to być timery albo zwykły licznik, który zwiększa się po kliknięciu.",
|
||||
"L_COUNTDOWN_DURATION": "Czas trwania odliczania",
|
||||
"L_DAILY_GOAL": "Codzienny cel dla udanej passy",
|
||||
"L_DAILY_GOAL": "Dzienny cel utrzymania serii",
|
||||
"L_ICON": "Ikona",
|
||||
"L_ICON_ON": "Ikona po włączeniu",
|
||||
"L_IS_ENABLED": "Włączone",
|
||||
|
|
@ -971,11 +989,11 @@
|
|||
"L_TITLE": "Tytuł",
|
||||
"L_TRACK_STREAKS": "Śledź serie",
|
||||
"L_TYPE": "Typ",
|
||||
"L_WEEKDAYS": "Dni powszednie, aby sprawdzić smugę",
|
||||
"L_WEEKLY_FREQUENCY": "Liczba wykonań na tydzień",
|
||||
"L_WEEKDAYS": "Dni sprawdzania serii",
|
||||
"L_WEEKLY_FREQUENCY": "Wymagana liczba ukończeń w tygodniu",
|
||||
"TITLE": "Proste liczniki",
|
||||
"TYPE_CLICK_COUNTER": "Licznik kliknięć",
|
||||
"TYPE_REPEATED_COUNTDOWN": "Powtarzane odliczanie",
|
||||
"TYPE_REPEATED_COUNTDOWN": "Codzienne odliczanie",
|
||||
"TYPE_STOPWATCH": "Stoper"
|
||||
},
|
||||
"HABIT_TRACKER": {
|
||||
|
|
@ -987,7 +1005,7 @@
|
|||
},
|
||||
"S": {
|
||||
"GOAL_REACHED_1": "Osiągnąłeś swój dzisiejszy cel!",
|
||||
"GOAL_REACHED_2": "Aktualny czas trwania passy:"
|
||||
"GOAL_REACHED_2": "Czas trwania obecnej serii:"
|
||||
}
|
||||
},
|
||||
"SYNC": {
|
||||
|
|
@ -1537,7 +1555,7 @@
|
|||
"QA_NEXT_WEEK": "Następny tydzień",
|
||||
"QA_TODAY": "Dzisiaj",
|
||||
"QA_TOMORROW": "Jutro",
|
||||
"REMIND_AT": "Przypomnij o",
|
||||
"REMIND_AT": "Przypomnij",
|
||||
"REMOVE_DEADLINE": "Usuń termin",
|
||||
"RO_1H": "1 godzinę przed",
|
||||
"RO_5M": "5 minut przed",
|
||||
|
|
@ -1577,7 +1595,7 @@
|
|||
"QA_NEXT_WEEK": "Harmonogram na następny tydzień",
|
||||
"QA_TODAY": "Harmonogram na dziś",
|
||||
"QA_TOMORROW": "Harmonogram na jutro",
|
||||
"REMIND_AT": "Przypomnij o",
|
||||
"REMIND_AT": "Przypomnij",
|
||||
"RO_1H": "1 godzinę przed rozpoczęciem",
|
||||
"RO_5M": "5 minut przed rozpoczęciem",
|
||||
"RO_10M": "10 minut przed rozpoczęciem",
|
||||
|
|
@ -1594,7 +1612,7 @@
|
|||
"TITLE": "Wybierz datę i godzinę"
|
||||
},
|
||||
"D_TIME": {
|
||||
"ADD_FOR_OTHER_DAY": "Dodaj czas spędzony dla innego dnia",
|
||||
"ADD_FOR_OTHER_DAY": "Dodaj czas spędzony innego dnia",
|
||||
"DELETE_FOR": "Usuń wpis dla dnia",
|
||||
"ESTIMATE": "Szacowanie",
|
||||
"TIME_SPENT": "Czas spędzony",
|
||||
|
|
@ -1604,7 +1622,7 @@
|
|||
"D_TIME_FOR_DAY": {
|
||||
"ADD_ENTRY_FOR": "Dodaj nowy wpis dla {{date}}",
|
||||
"DATE": "Data nowego wpisu",
|
||||
"HELP": "Przykłady:<br> 30m => 30 minut<br> 2h => 2 godziny<br> 2h30m => 2 godziny i 30 minut<br> 1.5h => 1 godzina i 30 minut",
|
||||
"HELP": "Przykłady:<br> 30m => 30 minut<br> 2h => 2 godziny<br> 2h30m => 2 godziny i 30 minut<br> 1.5h => 1 godzina i 30 minut<br> 03:15 => 3 godziny i 15 minut",
|
||||
"TINE_SPENT": "Czas spędzony",
|
||||
"TITLE": "Dodaj dla dnia"
|
||||
},
|
||||
|
|
@ -1928,7 +1946,7 @@
|
|||
"DO_IT": "Zrób to!",
|
||||
"DONT_SHOW_AGAIN": "Nie pokazuj ponownie",
|
||||
"DUPLICATE": "Powiel",
|
||||
"DURATION_DESCRIPTION": "np. \"5h 23m\", oznacza 5 godzin i 23 minuty",
|
||||
"DURATION_DESCRIPTION": "np. \"5h 23m\" oznacza 5 godzin i 23 minuty",
|
||||
"EDIT": "Edytuj",
|
||||
"ENABLED": "Włączone",
|
||||
"EXAMPLE_VAL": "np. 32m",
|
||||
|
|
@ -1970,7 +1988,7 @@
|
|||
"HELP": "Włącz lub wyłącz konkretne funkcje aplikacji w całym interfejsie użytkownika.",
|
||||
"ISSUES_PANEL": "Panel zgłoszeń",
|
||||
"PLANNER": "Planer",
|
||||
"PROJECT_NOTES": "Notatki projektowe",
|
||||
"PROJECT_NOTES": "Notatki projektu",
|
||||
"SCHEDULE": "Harmonogram",
|
||||
"SCHEDULE_DAY_PANEL": "Panel dnia harmonogramu",
|
||||
"SEARCH": "Szukaj",
|
||||
|
|
@ -1979,7 +1997,9 @@
|
|||
"TITLE": "Funkcje aplikacji",
|
||||
"USER_PROFILES": "Profile użytkowników (eksperymentalne)",
|
||||
"USER_PROFILES_HINT": "Pozwala tworzyć i przełączać się między różnymi profilami użytkowników, z których każdy ma oddzielne ustawienia, zadania i konfiguracje synchronizacji. Przycisk zarządzania profilem pojawi się w prawym górnym rogu po włączeniu. Uwaga: Wyłączenie tej funkcji ukryje interfejs użytkownika, ale zachowa dane profilu (Funkcja eksperymentalna, brak gwarancji. Upewnij się, że masz kopię zapasową danych!).",
|
||||
"USER_PROFILES_WARNING": "<strong>Funkcja eksperymentalna:</strong> Ta funkcja jest wciąż w fazie rozwoju i może zostać usunięta lub znacząco zmieniona w przyszłej aktualizacji. Używasz na własne ryzyko. Upewnij się, że masz kopię zapasową danych."
|
||||
"USER_PROFILES_WARNING": "<strong>Funkcja eksperymentalna:</strong> Ta funkcja jest wciąż w fazie rozwoju i może zostać usunięta lub znacząco zmieniona w przyszłej aktualizacji. Używasz na własne ryzyko. Upewnij się, że masz kopię zapasową danych.",
|
||||
"EXPERIMENTAL_WARNING_TITLE": "Funkcja eksperymentalna",
|
||||
"EXPERIMENTAL_WARNING_MSG": "Ta funkcja jest wciąż w fazie rozwoju i może zostać usunięta lub znacząco zmieniona w przyszłej aktualizacji. Używasz na własne ryzyko. Upewnij się, że masz kopię zapasową danych.<br/><br/>Czy na pewno chcesz ją aktywować?"
|
||||
},
|
||||
"AUTO_BACKUPS": {
|
||||
"HELP": "Automatycznie zapisuj wszystkie dane w folderze aplikacji, aby mieć kopię na wypadek problemów.",
|
||||
|
|
@ -2215,8 +2235,8 @@
|
|||
"HELP": "Funkcja harmonogramu ma dać Ci lepszy podgląd tego, jak zaplanowane zadania układają się w czasie. Ustawienia znajdziesz w sekcji „Harmonogram”.",
|
||||
"L_IS_LUNCH_BREAK_ENABLED": "Włącz przerwę na lunch",
|
||||
"L_IS_WORK_START_END_ENABLED": "Ogranicz przepływ niezaplanowanych zadań do określonych godzin pracy",
|
||||
"L_LUNCH_BREAK_END": "Koniec przerwy obiadowej",
|
||||
"L_LUNCH_BREAK_START": "Początek przerwy obiadowej",
|
||||
"L_LUNCH_BREAK_END": "Koniec przerwy na lunch",
|
||||
"L_LUNCH_BREAK_START": "Początek przerwy na lunch",
|
||||
"L_WORK_END": "Koniec dnia pracy",
|
||||
"L_WORK_START": "Początek dnia pracy",
|
||||
"LUNCH_BREAK_START_END_DESCRIPTION": "np. 13:00",
|
||||
|
|
@ -2362,14 +2382,14 @@
|
|||
"HABITS": "Nawyki",
|
||||
"HELP": "Pomoc",
|
||||
"HM": {
|
||||
"CALENDARS": "Jak to zrobić: Połącz kalendarze",
|
||||
"CONTRIBUTE": "Wkład",
|
||||
"CALENDARS": "Poradnik: Łączenie się z kalendarzem",
|
||||
"CONTRIBUTE": "Współtwórz",
|
||||
"GET_HELP_ONLINE": "Uzyskaj pomoc online",
|
||||
"KEYBOARD": "Howto: Zaawansowana klawiatura",
|
||||
"KEYBOARD": "Poradnik: Zaawansowana klawiatura",
|
||||
"REDDIT_COMMUNITY": "Społeczność Reddit",
|
||||
"REPORT_A_PROBLEM": "Zgłoś problem",
|
||||
"START_WELCOME": "Rozpocznij wycieczkę powitalną",
|
||||
"SYNC": "Jak to zrobić: Konfiguracja synchronizacji"
|
||||
"START_WELCOME": "Rozpocznij samouczek powitalny",
|
||||
"SYNC": "Poradnik: Konfiguracja synchronizacji"
|
||||
},
|
||||
"METRICS": "Metryki",
|
||||
"NO_PROJECT_INFO": "Brak dostępnych projektów. Możesz utworzyć nowy projekt, klikając przycisk 'Utwórz projekt'.",
|
||||
|
|
@ -2393,9 +2413,9 @@
|
|||
"TAGS": "Tagi",
|
||||
"TASK_LIST": "Lista zadań",
|
||||
"TASKS": "Zadania",
|
||||
"TOGGLE_SHOW_BOOKMARKS": "Pokaż/ukryj zakładki",
|
||||
"TOGGLE_SHOW_BOOKMARKS": "Pokaż/Ukryj zakładki",
|
||||
"TOGGLE_SHOW_ISSUE_PANEL": "Pokaż/Ukryj panel zgłoszeń",
|
||||
"TOGGLE_SHOW_NOTES": "Pokaż/Ukryj Notatki Projektu",
|
||||
"TOGGLE_SHOW_NOTES": "Pokaż/Ukryj notatki projektu",
|
||||
"TOGGLE_TRACK_TIME": "Start/Stop zliczania czasu",
|
||||
"TRIGGER_SYNC": "Ręczne wyzwalanie synchronizacji",
|
||||
"UNPLAN_ALL_TASKS": "Odplanowano wszystkie zadania",
|
||||
|
|
@ -2679,7 +2699,7 @@
|
|||
"NO_PLANNED_TASK_ALL_DONE": "wszystkie zadania zakończone",
|
||||
"NO_PLANNED_TASKS": "Nie zaplanowano zadań",
|
||||
"RECURRING_TASKS": "Zadania cykliczne",
|
||||
"RESET_BREAK_TIMER": "Zresetuj bez czasu przerwy",
|
||||
"RESET_BREAK_TIMER": "Zresetuj czas bez przerwy",
|
||||
"TIME_ESTIMATED": "Zaplanowany czas:",
|
||||
"TODAY_REMAINING": "Dziś pozostało:",
|
||||
"WITHOUT_BREAK": "Bez przerwy:",
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@
|
|||
"BY": "după",
|
||||
"NO_RESULTS": "Nicio imagine găsită. Te rog încearcă un alt termen de căutare.",
|
||||
"SEARCH_LABEL": "Caută imagini",
|
||||
"SEARCH_PLACEHOLDER": "ex: natură, munți, abstract",
|
||||
"SEARCH_PLACEHOLDER": "d.e. natură, munți, abstract",
|
||||
"TITLE": "Alege o imagine de fundal din Unsplash"
|
||||
},
|
||||
"DONATE_PAGE": {
|
||||
|
|
@ -94,8 +94,8 @@
|
|||
"ADD_NEW_PANEL": "Adaugă un nou panou",
|
||||
"BACKLOG_TASK_FILTER_ALL": "Toate",
|
||||
"BACKLOG_TASK_FILTER_NO_BACKLOG": "Exclude",
|
||||
"BACKLOG_TASK_FILTER_ONLY_BACKLOG": "Doar restante",
|
||||
"BACKLOG_TASK_FILTER_TYPE": "Sarcini restante",
|
||||
"BACKLOG_TASK_FILTER_ONLY_BACKLOG": "Doar backlog",
|
||||
"BACKLOG_TASK_FILTER_TYPE": "Sarcini din backlog",
|
||||
"COLUMNS": "Coloane",
|
||||
"TAGS_EXCLUDED": "Etichete excluse",
|
||||
"TAGS_REQUIRED": "Etichete obligatorii",
|
||||
|
|
@ -167,8 +167,18 @@
|
|||
"EVENT_STR": "eveniment",
|
||||
"EVENTS_STR": "evenimente"
|
||||
},
|
||||
"CONTEXT_MENU": {
|
||||
"CREATE_TASK": "Creează ca sarcină",
|
||||
"OPEN_IN_CALENDAR": "Deschide în calendar",
|
||||
"RESCHEDULE": "Replanifică",
|
||||
"DELETE_EVENT": "Șterge evenimentul"
|
||||
},
|
||||
"S": {
|
||||
"CAL_PROVIDER_ERROR": "Eroare furnizor calendar: {{errTxt}}"
|
||||
"CAL_PROVIDER_ERROR": "Eroare furnizor calendar: {{errTxt}}",
|
||||
"EVENT_HIDDEN": "Eveniment calendar ascuns",
|
||||
"EVENT_RESCHEDULED": "Eveniment reprogramat",
|
||||
"EVENT_DELETED": "Eveniment șters",
|
||||
"TIME_BLOCK_ERROR": "Nu s-a putut sincroniza blocul de timp: {{errTxt}}"
|
||||
}
|
||||
},
|
||||
"CLIPBOARD_IMAGE": {
|
||||
|
|
@ -265,7 +275,7 @@
|
|||
},
|
||||
"GITEA": {
|
||||
"FORM": {
|
||||
"HOST": "Gazdă (ex: https://try.gitea.io)",
|
||||
"HOST": "Gazdă (d.e. https://try.gitea.io)",
|
||||
"REPO_FULL_NAME": "Nume utilizator sau nume organizație/proiect",
|
||||
"REPO_FULL_NAME_DESCRIPTION": "Poate fi găsit ca parte a URL-ului cînd vizualizezi proiectul într-un navigator web.",
|
||||
"SCOPE": "Domeniu de aplicare",
|
||||
|
|
@ -303,7 +313,7 @@
|
|||
"FILTER_USER": "Filtrează după numele de utilizator",
|
||||
"GITLAB_BASE_URL": "URL de bază personalizat GitLab (opțional)",
|
||||
"PROJECT": "nume utilizator/proiect",
|
||||
"PROJECT_HINT": "ex: super-productivity/super-productivity",
|
||||
"PROJECT_HINT": "d.e. super-productivity/super-productivity",
|
||||
"SCOPE": "Domeniu de aplicare",
|
||||
"SCOPE_ALL": "Toate",
|
||||
"SCOPE_ASSIGNED": "Atribuite mie",
|
||||
|
|
@ -421,8 +431,16 @@
|
|||
"DIALOG": {
|
||||
"ADVANCED_CONFIG": "Configurare avansată",
|
||||
"CONNECTED": "Conectat",
|
||||
"DELETE_CONFIRM": "Dorești să ștergi acest furnizor de probleme? Ștergerea înseamnă că <strong>toate sarcinile de probleme importate anterior vor pierde referința</strong>. Aceasta nu poate fi anulată!",
|
||||
"DISCONNECT": "Deconectează"
|
||||
"DELETE_CONFIRM": "Dorești să ștergi acest furnizor de probleme? Ștergerea înseamnă că <strong>toate sarcinile de probleme importate anterior vor pierde referința</strong>. Aceasta acțiune nu poate fi anulată!",
|
||||
"DISCONNECT": "Deconectează",
|
||||
"EDIT_TITLE": "Editează configurația {{title}}",
|
||||
"LOAD_OPTIONS": "Încarcă opțiunile",
|
||||
"LOAD_OPTIONS_FAILED": "Nu s-au putut încărca opțiunile",
|
||||
"LOADING_OPTIONS": "Se încarcă opțiunile...",
|
||||
"OPTIONS_LOADED": "Opțiuni încărcate",
|
||||
"RELOAD_OPTIONS": "Reîncarcă opțiunile",
|
||||
"SETUP_TITLE": "Editează configurația {{title}}",
|
||||
"TEST_CONNECTION": "Testează conexiunea"
|
||||
}
|
||||
},
|
||||
"ISSUE_PANEL": {
|
||||
|
|
@ -432,37 +450,37 @@
|
|||
},
|
||||
"JIRA": {
|
||||
"BANNER": {
|
||||
"BLOCK_ACCESS_MSG": "Jira: To prevent API lock out, access has been blocked by Super Productivity. You probably should check your Jira settings!",
|
||||
"BLOCK_ACCESS_UNBLOCK": "Unblock"
|
||||
"BLOCK_ACCESS_MSG": "Jira: Pentru a preveni blocarea API-ului, accesul a fost blocat de Super Productivity. Probabil ar trebui să verifici setările Jira!",
|
||||
"BLOCK_ACCESS_UNBLOCK": "Deblochează"
|
||||
},
|
||||
"CFG_CMP": {
|
||||
"ALWAYS_ASK": "Always open dialog",
|
||||
"DO_NOT": "Don't transition",
|
||||
"DONE": "Status for completing task",
|
||||
"ENABLE": "Enable Jira integration",
|
||||
"ENABLE_TRANSITIONS": "Enable Transition Handling",
|
||||
"IN_PROGRESS": "Status for starting task",
|
||||
"LOAD_SUGGESTIONS": "Load Suggestions",
|
||||
"MAP_CUSTOM_FIELDS": "Load Story Points",
|
||||
"MAP_CUSTOM_FIELDS_INFO": "Unfortunately some of Jira's data is saved under custom fields which are different for every installation. To include this data, you need to select the proper custom field for it. Currently there is only the story points field that needs to be mapped.",
|
||||
"OPEN": "Status for pausing task",
|
||||
"SELECT_ISSUE_FOR_TRANSITIONS": "Select issue to load available transitions",
|
||||
"STORY_POINTS": "Story Points Field Name",
|
||||
"TRANSITION": "Transition Handling"
|
||||
"ALWAYS_ASK": "Deschide mereu dialogul",
|
||||
"DO_NOT": "Nu tranziționa",
|
||||
"DONE": "Stare pentru finalizarea sarcinii",
|
||||
"ENABLE": "Activează integrarea Jira",
|
||||
"ENABLE_TRANSITIONS": "Activează gestionarea tranzițiilor",
|
||||
"IN_PROGRESS": "Stare pentru începerea sarcinii",
|
||||
"LOAD_SUGGESTIONS": "Încarcă sugestiile",
|
||||
"MAP_CUSTOM_FIELDS": "Încarcă Story Points",
|
||||
"MAP_CUSTOM_FIELDS_INFO": "Din păcate unele din datele Jira sînt salvate sub cîmpuri particulare (custom fields) care sînt diferite pentru fiecare instalare. Pentru a include aceste date, trebuie să selectezi cîmpul personalizat adecvat. Momentan, există doar cîmpul pentru Story Points care trebuie asociat.",
|
||||
"OPEN": "Stare pentru punerea pe pauză a sarcinii",
|
||||
"SELECT_ISSUE_FOR_TRANSITIONS": "Selectează problema pentru a încărca tranzițiile disponibile",
|
||||
"STORY_POINTS": "Nume cîmp Story Points",
|
||||
"TRANSITION": "Gestionarea tranzițiilor"
|
||||
},
|
||||
"DIALOG_CONFIRM_ASSIGNMENT": {
|
||||
"MSG": "<strong>{{summary}}</strong> is currently assigned to <strong>{{assignee}}</strong>. Do you want to assign it to yourself?",
|
||||
"OK": "Do it!"
|
||||
"MSG": "<strong>{{summary}}</strong> Este asociat curent cu <strong>{{assignee}}</strong>. Dorești să îl asociezi pentru tine?",
|
||||
"OK": "Fă-o!"
|
||||
},
|
||||
"DIALOG_INITIAL": {
|
||||
"TITLE": "Setup Jira for Project"
|
||||
"TITLE": "Configurează Jira pentru proiect"
|
||||
},
|
||||
"DIALOG_TRANSITION": {
|
||||
"CHOOSE_STATUS": "Choose status to assign",
|
||||
"CURRENT_ASSIGNEE": "Current Assignee:",
|
||||
"CURRENT_STATUS": "Current Status:",
|
||||
"TASK_NAME": "Task name:",
|
||||
"TITLE": "Jira: Update Status"
|
||||
"CHOOSE_STATUS": "Alege starea de atribuit",
|
||||
"CURRENT_ASSIGNEE": "Atribuit curent:",
|
||||
"CURRENT_STATUS": "Starea curentă:",
|
||||
"TASK_NAME": "Nume sarcină:",
|
||||
"TITLE": "Jira: Actualizează starea"
|
||||
},
|
||||
"DIALOG_WORKLOG": {
|
||||
"CHECKBOXES": {
|
||||
|
|
@ -929,32 +947,32 @@
|
|||
"TEXT": "<p>Orarul îți oferă o viziune de ansamblu despre cum se desfășoară sarcinile planificate în timp. Este generat automat din sarcinile tale și necesită doar <strong>estimări de timp</strong> pentru a funcționa.</p><p>Două lucruri sînt distinse: <strong>Sarcini programate</strong>, care sînt afișate la ora planificată și <strong>Sarcini obișnuite</strong>, care ar trebui să curgă în jurul acestor evenimente fixe.</p><p>Dacă setezi o oră de început și sfîrșit a lucrului (recomandat), sarcinile obișnuite nu vor fi niciodată plasate în afara acestor limite.</p>",
|
||||
"TITLE": "Orar"
|
||||
},
|
||||
"END": "Work End",
|
||||
"INSERT_BEFORE": "Before",
|
||||
"LUNCH_BREAK": "Lunch Break",
|
||||
"MONTH": "Month",
|
||||
"NO_TASKS": "Currently there are no tasks. Please add some tasks via the plus (+) button in the top bar.",
|
||||
"NOW": "Now",
|
||||
"PLAN_END_DAY": "End of {{date}}",
|
||||
"PLAN_START_DAY": "Start of {{date}}",
|
||||
"SHIFT_KEY_INFO": "Hold Shift to toggle day planning mode",
|
||||
"START": "Work Start"
|
||||
"END": "Finalul muncii",
|
||||
"INSERT_BEFORE": "Înainte",
|
||||
"LUNCH_BREAK": "Pauză de masă",
|
||||
"MONTH": "Lună",
|
||||
"NO_TASKS": "Momentan nu există sarcini. Adaugă cîteva sarcini prin butonul plus (+) din bara de sus.",
|
||||
"NOW": "Acum",
|
||||
"PLAN_END_DAY": "Final de {{date}}",
|
||||
"PLAN_START_DAY": "Început de {{date}}",
|
||||
"SHIFT_KEY_INFO": "Ține apăsat Shift pentru a comuta modul de planificare zilnică",
|
||||
"START": "Începerea muncii"
|
||||
},
|
||||
"SEARCH_BAR": {
|
||||
"INFO": "Start typing to search for tasks",
|
||||
"NO_RESULTS": "No tasks found matching your search",
|
||||
"PLACEHOLDER": "Search for task or task notes"
|
||||
"INFO": "Începe să tastezi pentru a căuta sarcini",
|
||||
"NO_RESULTS": "Nu s-au găsit sarcini care să se potrivească căutării",
|
||||
"PLACEHOLDER": "Caută sarcină sau notițe despre sarcină"
|
||||
},
|
||||
"SIMPLE_COUNTER": {
|
||||
"D_CONFIRM_REMOVE": {
|
||||
"MSG": "Deleting a simple counter, will also delete all past data tracked on it. Are you sure you want to proceed?",
|
||||
"OK": "Do it!"
|
||||
"MSG": "Ștergerea unui contor simplu va șterge și toate datele urmărite pe el. Ești sigur că vrei să continui?",
|
||||
"OK": "Fă-o!"
|
||||
},
|
||||
"D_EDIT": {
|
||||
"CURRENT_STREAK": "Current Streak",
|
||||
"DAILY_GOAL": "Daily Goal",
|
||||
"DAYS": "Days",
|
||||
"L_COUNTER": "Count"
|
||||
"CURRENT_STREAK": "Serie curentă",
|
||||
"DAILY_GOAL": "Obiectiv zilnic",
|
||||
"DAYS": "Zile",
|
||||
"L_COUNTER": "Număr"
|
||||
},
|
||||
"FORM": {
|
||||
"ADD_NEW": "Adaugă un contor simplu sau un obicei",
|
||||
|
|
@ -967,7 +985,7 @@
|
|||
"L_IS_HIDE_BUTTON": "Ascunde butonul din bara de unelte",
|
||||
"L_STREAK_MODE": "Mod urmărire serie",
|
||||
"L_STREAK_MODE_SPECIFIC_DAYS": "Zilele săptămînii",
|
||||
"L_STREAK_MODE_WEEKLY_FREQUENCY": "Frecvență săptămînală (ex: de 3 ori pe săptămînă)",
|
||||
"L_STREAK_MODE_WEEKLY_FREQUENCY": "Frecvență săptămînală (d.e. de 3 ori pe săptămînă)",
|
||||
"L_TITLE": "Titlu",
|
||||
"L_TRACK_STREAKS": "Urmărește seriile",
|
||||
"L_TYPE": "Tip",
|
||||
|
|
@ -1000,40 +1018,40 @@
|
|||
"FORCE_UPLOAD": "Forțarea încărcării datelor poate duce la pierderea acestora. Ești sigur că vrei să continui?"
|
||||
},
|
||||
"D_AUTH_CODE": {
|
||||
"FOLLOW_LINK": "Please open the following link and copy the auth code provided there into the input field below.",
|
||||
"FOLLOW_LINK_MOBILE": "Please tap the button below to authorize. You will be automatically redirected back to the app.",
|
||||
"GET_AUTH_CODE": "Get Authorization Code",
|
||||
"L_AUTH_CODE": "Enter Auth Code",
|
||||
"TITLE": "Login: {{provider}}",
|
||||
"WAITING_FOR_AUTH": "Waiting for authentication..."
|
||||
"FOLLOW_LINK": "Te rog deschide următoarea legătură și copiază codul de autorizare furnizat acolo în cîmpul de intrare de mai jos.",
|
||||
"FOLLOW_LINK_MOBILE": "Te rog apasă butonul de mai jos pentru a autoriza. Vei fi redirecționat automat înapoi la aplicație.",
|
||||
"GET_AUTH_CODE": "Obține codul de autorizare",
|
||||
"L_AUTH_CODE": "Întrodu codul de autorizare",
|
||||
"TITLE": "Autentificare: {{provider}}",
|
||||
"WAITING_FOR_AUTH": "Se așteaptă autentificarea..."
|
||||
},
|
||||
"D_CONFLICT": {
|
||||
"ADDITIONAL_INFO": "Additional Info",
|
||||
"CHANGES": "Changes",
|
||||
"CHANGES_SINCE_LAST_SYNC": "Changes since last sync",
|
||||
"COMPARISON_RESULT": "Comparison Result",
|
||||
"DATE": "Date",
|
||||
"LAST_SYNC": "Last Sync:",
|
||||
"LAST_SYNCED": "Last Synced",
|
||||
"LAST_WRITE": "Last Write",
|
||||
"ADDITIONAL_INFO": "Informații suplimentare",
|
||||
"CHANGES": "Modificări",
|
||||
"CHANGES_SINCE_LAST_SYNC": "Modificări de la ultima sincronizare",
|
||||
"COMPARISON_RESULT": "Rezultatul comparării",
|
||||
"DATE": "Dată",
|
||||
"LAST_SYNC": "Ultima Sincronizare:",
|
||||
"LAST_SYNCED": "Ultima sincronizare",
|
||||
"LAST_WRITE": "Ultima scriere",
|
||||
"LOCAL": "Local",
|
||||
"LOCAL_REMOTE": "Local -> Remote",
|
||||
"NEVER": "Never",
|
||||
"OVERWRITE_WARNING": "WARNING: The {{targetName}} data contains approximately {{targetChanges}} changes while the {{sourceName}} data only has {{sourceChanges}} changes. Are you sure you want to overwrite the {{targetName}} data with the {{sourceName}} version? This action cannot be undone.",
|
||||
"REMOTE": "Remote",
|
||||
"RESULT": "Result",
|
||||
"TEXT": "<p>Update from remote. Both local and remote data seem to be modified.</p>",
|
||||
"TIME": "Time",
|
||||
"TIMESTAMP": "Timestamp",
|
||||
"TITLE": "Sync: Conflicting Data",
|
||||
"USE_LOCAL": "Keep local",
|
||||
"USE_REMOTE": "Keep remote",
|
||||
"VECTOR_CLOCK": "Vector Clock",
|
||||
"VECTOR_CLOCK_HEADING": "Vector Clock",
|
||||
"VECTOR_COMPARISON_CONCURRENT": "Concurrent (True Conflict)",
|
||||
"VECTOR_COMPARISON_EQUAL": "Equal",
|
||||
"VECTOR_COMPARISON_LOCAL_GREATER": "Local > Remote",
|
||||
"VECTOR_COMPARISON_LOCAL_LESS": "Local < Remote"
|
||||
"LOCAL_REMOTE": "Local -> Distant",
|
||||
"NEVER": "Niciodată",
|
||||
"OVERWRITE_WARNING": "ATENȚIE: Datele {{targetName}} conțin aproximativ {{targetChanges}} modificări în timp ce datele {{sourceName}} au doar {{sourceChanges}} modificări. Ești sigur că vrei să înlocuiești datele {{targetName}} cu versiunea {{sourceName}}? Această acțiune nu poate fi anulată.",
|
||||
"REMOTE": "Distant",
|
||||
"RESULT": "Rezultat",
|
||||
"TEXT": "<p>Actualizează de la distanță. Atît datele locale cît și cele de pe server par modificate.</p>",
|
||||
"TIME": "Timp",
|
||||
"TIMESTAMP": "Marca temporală",
|
||||
"TITLE": "Sincronizare: Date conflictuale",
|
||||
"USE_LOCAL": "Păstrează local",
|
||||
"USE_REMOTE": "Păstrează distant",
|
||||
"VECTOR_CLOCK": "Ceas vectorial",
|
||||
"VECTOR_CLOCK_HEADING": "Ceas vectorial",
|
||||
"VECTOR_COMPARISON_CONCURRENT": "Concurent (conflict adevărat)",
|
||||
"VECTOR_COMPARISON_EQUAL": "Egal",
|
||||
"VECTOR_COMPARISON_LOCAL_GREATER": "Local > Distant",
|
||||
"VECTOR_COMPARISON_LOCAL_LESS": "Local < Distant"
|
||||
},
|
||||
"D_DATA_REPAIR_CONFIRM": {
|
||||
"MSG": "Datele tale par deteriorate. Dorești să încerci repararea automată? Aceasta poate duce la pierderea minoră de date.",
|
||||
|
|
@ -1045,28 +1063,28 @@
|
|||
"UNKNOWN_ERROR": "Eroare necunoscută de sincronizare: {{err}}"
|
||||
},
|
||||
"D_DECRYPT_ERROR": {
|
||||
"BTN_OVER_WRITE_REMOTE": "Use This Password & Overwrite Remote",
|
||||
"CHANGE_PW_AND_DECRYPT": "Retry Decrypt",
|
||||
"P1": "Your remote data is encrypted and decryption failed.",
|
||||
"P2_FORGOT_PW": "If you <strong>forgot your password</strong>, enter the correct password and click \"Retry Decrypt\".",
|
||||
"P3_PW_CHANGED": "If you <strong>changed your password on another device</strong>, enter your new password and click \"Overwrite Remote\". This will upload your local data to the server.",
|
||||
"PASSWORD": "Password",
|
||||
"TITLE": "Decryption Failed"
|
||||
"BTN_OVER_WRITE_REMOTE": "Folosește această parolă și suprascrie-o pe cea de la distanță",
|
||||
"CHANGE_PW_AND_DECRYPT": "Reîncearcă decriptarea",
|
||||
"P1": "Datele tale de la distanță sînt criptate și decriptarea a eșuat.",
|
||||
"P2_FORGOT_PW": "Dacă ai <strong>uitat parola</strong>, întrodu parola corectă și apasă \"Reîncearcă decriptarea\".",
|
||||
"P3_PW_CHANGED": "Dacă ai <strong>schimbat parola pe un alt dispozitiv</strong>, întrodu noua parolă și apasă \"Suprascrie datele de la distanță\". Aceasta va încărca datele locale criptate cu această parolă pe server.",
|
||||
"PASSWORD": "Parolă",
|
||||
"TITLE": "Decriptarea a eșuat"
|
||||
},
|
||||
"D_ENTER_PASSWORD": {
|
||||
"BTN_FORCE_OVERWRITE": "Use Local Data",
|
||||
"BTN_SAVE_AND_SYNC": "Save & Sync",
|
||||
"FORCE_OVERWRITE_CONFIRM": "This will <strong>delete all data on the server</strong> and upload your local data encrypted with this password. Continue?",
|
||||
"FORCE_OVERWRITE_INFO": "If you prefer to keep your local data, you can overwrite the server instead.",
|
||||
"FORCE_OVERWRITE_TITLE": "Overwrite Server Data?",
|
||||
"MESSAGE": "The sync server contains encrypted data, but no encryption password is configured on this device. Please enter your encryption password to decrypt and sync your data.",
|
||||
"PASSWORD_LABEL": "Encryption Password",
|
||||
"TITLE": "Encryption Password Required"
|
||||
"BTN_FORCE_OVERWRITE": "Folosește datele locale",
|
||||
"BTN_SAVE_AND_SYNC": "Salvează și sincronizează",
|
||||
"FORCE_OVERWRITE_CONFIRM": "Acest lucru va <strong>șterge toate datele de pe server</strong> și va încărca datele locale criptate cu această parolă. Continui?",
|
||||
"FORCE_OVERWRITE_INFO": "Dacă preferi să păstrezi datele locale, poți suprascrie serverul în schimb.",
|
||||
"FORCE_OVERWRITE_TITLE": "Suprascrii datele de pe server?",
|
||||
"MESSAGE": "Serverul de sincronizare conține date criptate, dar nu este configurată nicio parolă de criptare pe acest dispozitiv. Te rog să întroduci parola de criptare pentru a decripta și sincroniza datele.",
|
||||
"PASSWORD_LABEL": "Parolă de criptare",
|
||||
"TITLE": "Parolă de criptare necesară"
|
||||
},
|
||||
"D_FRESH_CLIENT_CONFIRM": {
|
||||
"MESSAGE": "This appears to be a fresh installation. Remote data with {{count}} changes was found. Do you want to download and overwrite your local data with it?",
|
||||
"OK": "Download Remote Data",
|
||||
"TITLE": "Initial Sync"
|
||||
"MESSAGE": "Aceasta pare să fie o instalare curată. Datele de la distanță cu {{count}} modificări au fost găsite. Dorești să descarci și să înlocuiești datele locale cu ele?",
|
||||
"OK": "Descarcă datele de la distanță",
|
||||
"TITLE": "Sincronizare inițială"
|
||||
},
|
||||
"D_INCOMPLETE_SYNC": {
|
||||
"BTN_CLOSE_APP": "Închide aplicația",
|
||||
|
|
@ -1271,13 +1289,17 @@
|
|||
"SETUP_ENCRYPTION_PASSWORD_WARNING": "Folosește aceeași parolă pe toate dispozitivele. O parolă uitată poate fi resetată prin suprascrierea datelor de la distanță.",
|
||||
"SETUP_ENCRYPTION_TITLE": "SuperSync: Setare parolă"
|
||||
},
|
||||
"NEXTCLOUD": {
|
||||
"L_SERVER_URL": "Nextcloud Server URL",
|
||||
"SERVER_URL_DESCRIPTION": "e.g. https://cloud.example.com or https://example.com/nextcloud",
|
||||
"L_APP_PASSWORD": "App Password",
|
||||
"APP_PASSWORD_DESCRIPTION": "Go to Nextcloud → Settings → Security → App passwords to generate one"
|
||||
},
|
||||
"TITLE": "Sync",
|
||||
"WEB_DAV": {
|
||||
"CONDITIONAL_HEADER_WARNING_MSG": "Your WebDAV server does not support conditional headers (If-Unmodified-Since). This means Super Productivity cannot detect concurrent changes from other devices during upload.<br><br>Sync will still work, but there is a small risk of data conflicts if you make changes on multiple devices simultaneously.",
|
||||
"CONDITIONAL_HEADER_WARNING_TITLE": "Reduced Sync Safety",
|
||||
"CORS_INFO": "<strong>Making it work in a web browser:</strong> Allow Super Productivity to make CORS requests to your WebDAV server. This can have negative security implications! For Nextcloud, please refer to <a href='https://github.com/nextcloud/server/issues/3131'>this GitHub thread</a> for more information. One approach to make this work on mobile is allowing \"https://app.super-productivity.com\" via the Nextcloud app <a href='https://apps.nextcloud.com/apps/webapppassword'>webapppassword<a>. Use at your own risk!</p>",
|
||||
"CORS_INFO": "<strong>Making it work in a web browser:</strong> You need to allow Super Productivity to make CORS requests to your server. For Nextcloud, one approach is allowing \"https://app.super-productivity.com\" via the <a href='https://apps.nextcloud.com/apps/webapppassword'>webapppassword</a> app. Refer to <a href='https://github.com/nextcloud/server/issues/3131'>this GitHub thread</a> for more information.",
|
||||
"D_SYNC_FOLDER_PATH": "Path relative to the WebDAV server root where sync files will be stored (e.g. '/super-productivity' or '/'). This is NOT your server's internal directory path.",
|
||||
"INFO": "<strong>Warning:</strong> WebDAV implementations differ wildly. Super Productivity is known to work well with Nextcloud, <strong>but it may not work with your provider</strong>. Test thoroughly before relying on it.",
|
||||
"INFO": "<strong>Warning:</strong> Generic WebDAV sync is provided <strong>as-is without support</strong>. If you are using Nextcloud, please use the Nextcloud sync option instead.",
|
||||
"L_BASE_URL": "Base URL",
|
||||
"L_PASSWORD": "Password",
|
||||
"L_SYNC_FOLDER_PATH": "Sync Folder Path",
|
||||
|
|
@ -1315,6 +1337,8 @@
|
|||
"ERROR_PERMISSION": "File access denied. Please check your filesystem permissions.",
|
||||
"ERROR_PERMISSION_FLATPAK": "File access denied. Grant filesystem permission via Flatseal or use a path inside ~/.var/app/",
|
||||
"ERROR_PERMISSION_SNAP": "File access denied. Run 'snap connect super-productivity:home' or use a path inside ~/snap/super-productivity/common/",
|
||||
"ERROR_REMOTE_FILE_EMPTY": "Remote sync file is empty. This can happen after an interrupted sync. Overwrite remote with your local data?",
|
||||
"ERROR_REMOTE_FILE_LOCKED": "Remote sync file is locked by the server. This may resolve on the next sync attempt.",
|
||||
"ERROR_UNABLE_TO_READ_REMOTE_DATA": "Error while syncing. Unable to read remote data. Maybe you enabled encryption and your local password does not match the one used to encrypt the remote data?",
|
||||
"FINISH_DAY_SYNC_ERROR": "Sync error while finishing day. Please try again.",
|
||||
"FRESH_CLIENT_SYNC_CANCELLED": "Sincronizare inițială anulată. Datele de la distanță nu au fost aplicate.",
|
||||
|
|
@ -1405,11 +1429,11 @@
|
|||
"TITLE": "Basic Settings"
|
||||
},
|
||||
"S": {
|
||||
"ARCHIVE_CLEANUP_FAILED": "Curățarea datelor arhivate pentru eticheta ștearsă a eșuat",
|
||||
"UPDATED": "Tag settings were updated"
|
||||
"ARCHIVE_CLEANUP_FAILED": "A eșuat curățarea datelor arhivate pentru eticheta ștearsă",
|
||||
"UPDATED": "Setările etichetei au fost actualizate"
|
||||
},
|
||||
"TTL": {
|
||||
"ADD_NEW_TAG": "Add new Tag"
|
||||
"ADD_NEW_TAG": "Adaugă etichetă nouă"
|
||||
}
|
||||
},
|
||||
"TAG_FOLDER": {
|
||||
|
|
@ -1489,47 +1513,47 @@
|
|||
"ADD_SUB_TASK": "Adaugă subsarcină",
|
||||
"ADD_TO_MY_DAY": "Adaugă la lista de azi",
|
||||
"ADD_TO_PROJECT": "Adaugă la proiect",
|
||||
"CONVERT_TO_PARENT_TASK": "Convert to parent task",
|
||||
"DELETE": "Delete task",
|
||||
"DELETE_REPEAT_INSTANCE": "Delete recurring task instance",
|
||||
"DROP_ATTACHMENT": "Drop here to attach to \"{{title}}\"",
|
||||
"DUPLICATE": "Duplicate",
|
||||
"CONVERT_TO_PARENT_TASK": "Convertește în sarcină părinte",
|
||||
"DELETE": "Șterge sarcina",
|
||||
"DELETE_REPEAT_INSTANCE": "Șterge instanța de sarcină recurentă",
|
||||
"DROP_ATTACHMENT": "Plasează aici pentru a atașa la \"{{title}}\"",
|
||||
"DUPLICATE": "Duplică",
|
||||
"EDIT_DEADLINE": "Editează termen limită",
|
||||
"EDIT_SCHEDULED": "Reschedule",
|
||||
"FOCUS_SESSION": "Start focus session",
|
||||
"MARK_DONE": "Mark as completed",
|
||||
"MOVE_TO_BACKLOG": "Move to backlog",
|
||||
"MOVE_TO_OTHER_PROJECT": "Move to project",
|
||||
"MOVE_TO_REGULAR": "Move to regular list",
|
||||
"MOVE_TO_TOP": "Move to top",
|
||||
"OPEN_ATTACH": "Attach file or link",
|
||||
"OPEN_ISSUE": "Open in browser",
|
||||
"OPEN_TIME": "Time tracking",
|
||||
"EDIT_SCHEDULED": "Planifică din nou",
|
||||
"FOCUS_SESSION": "Începe sesiune de concentrare",
|
||||
"MARK_DONE": "Marchează ca finalizată",
|
||||
"MOVE_TO_BACKLOG": "Mută în backlog",
|
||||
"MOVE_TO_OTHER_PROJECT": "Mută în proiect",
|
||||
"MOVE_TO_REGULAR": "Mută în lista obișnuită",
|
||||
"MOVE_TO_TOP": "Mută în partea de sus",
|
||||
"OPEN_ATTACH": "Atașează fișier sau legătură",
|
||||
"OPEN_ISSUE": "Deschide în navigatorul web",
|
||||
"OPEN_TIME": "Contorizare timp",
|
||||
"REMOVE_DEADLINE": "Șterge termen limită",
|
||||
"REMOVE_FROM_MY_DAY": "Elimină din lista de azi",
|
||||
"SCHEDULE": "Schedule task",
|
||||
"SCHEDULE": "Planifică sarcina",
|
||||
"SET_DEADLINE": "Setează termen limită",
|
||||
"TOGGLE_ATTACHMENTS": "Show/hide attachments",
|
||||
"TOGGLE_DETAIL_PANEL": "Show/hide additional info",
|
||||
"TOGGLE_DONE": "Toggle completion status",
|
||||
"TOGGLE_SUB_TASK_VISIBILITY": "Toggle subtask visibility",
|
||||
"TOGGLE_TAGS": "Toggle tags",
|
||||
"TRACK_TIME": "Start tracking time",
|
||||
"TRACK_TIME_STOP": "Pause tracking time",
|
||||
"UNSCHEDULE_TASK": "Unschedule task",
|
||||
"UPDATE_ISSUE_DATA": "Update issue data"
|
||||
"TOGGLE_ATTACHMENTS": "Arată/Ascunde atașamente",
|
||||
"TOGGLE_DETAIL_PANEL": "Arată/Ascunde informații suplimentare",
|
||||
"TOGGLE_DONE": "Comută starea de finalizare",
|
||||
"TOGGLE_SUB_TASK_VISIBILITY": "Comută vizibilitatea subsarcinilor",
|
||||
"TOGGLE_TAGS": "Comută etichetele",
|
||||
"TRACK_TIME": "Începe contorizarea timpului",
|
||||
"TRACK_TIME_STOP": "Pune pe pauză contorizarea timpului",
|
||||
"UNSCHEDULE_TASK": "Șterge planificarea sarcinii",
|
||||
"UPDATE_ISSUE_DATA": "Actualizează datele problemei"
|
||||
},
|
||||
"D_CONFIRM_DELETE": {
|
||||
"MSG": "Dorești să ștergi sarcina \"{{title}}\"?",
|
||||
"OK": "Delete"
|
||||
"OK": "Șterge"
|
||||
},
|
||||
"D_CONFIRM_SHORT_SYNTAX_NEW_TAG": {
|
||||
"MSG": "Do you want to create the new tag {{tagsTxt}}?",
|
||||
"OK": "Create tag"
|
||||
"MSG": "DDorești să creezi eticheta nouă {{tagsTxt}}?",
|
||||
"OK": "Creează etichetă"
|
||||
},
|
||||
"D_CONFIRM_SHORT_SYNTAX_NEW_TAGS": {
|
||||
"MSG": "Do you want to create the new tags {{tagsTxt}}?",
|
||||
"OK": "Create tags"
|
||||
"MSG": "Dorești să creezi etichetele noi {{tagsTxt}}?",
|
||||
"OK": "Creează etichete"
|
||||
},
|
||||
"D_DEADLINE": {
|
||||
"ADD_TIME": "Adaugă ora",
|
||||
|
|
@ -1549,28 +1573,28 @@
|
|||
"SET_DEADLINE": "Setează termen limită"
|
||||
},
|
||||
"D_REMINDER_VIEW": {
|
||||
"ADD_ALL_TO_TODAY": "Add all to Today",
|
||||
"ADD_TO_TODAY": "Add to Today",
|
||||
"ADD_ALL_TO_TODAY": "Adaugă-le pe toate la Astăzi",
|
||||
"ADD_TO_TODAY": "Adaugă la Astăzi",
|
||||
"CLEAR_ALL_REMINDERS": "Șterge toate memento-urile",
|
||||
"CLEAR_REMINDER": "Șterge memento",
|
||||
"COMPLETE": "Complete",
|
||||
"COMPLETE_ALL": "Complete all",
|
||||
"COMPLETE": "Finalizează",
|
||||
"COMPLETE_ALL": "Finalizează-le pe toate",
|
||||
"DEADLINE_REMINDER": "Memento termen limită",
|
||||
"DEADLINE_SECTION": "Termene limită",
|
||||
"DISMISS_ALL_REMINDERS_KEEP_TODAY": "Dismiss All Reminders (Keep in Today)",
|
||||
"DISMISS_REMINDER_KEEP_TODAY": "Dismiss Reminder (Keep in Today)",
|
||||
"DONE": "Done",
|
||||
"DISMISS_ALL_REMINDERS_KEEP_TODAY": "Înlătură toate mementourile (păstrează în Astăzi)",
|
||||
"DISMISS_REMINDER_KEEP_TODAY": "Înlătură memento (păstrează în Astăzi)",
|
||||
"DONE": "Realizat",
|
||||
"DUE_SECTION": "Planificate",
|
||||
"DUE_TASK": "Reminder for planned task",
|
||||
"DUE_TASKS": "Reminder for planned tasks",
|
||||
"RESCHEDULE_EDIT": "Edit (reschedule)",
|
||||
"RESCHEDULE_UNTIL_TOMORROW": "Reschedule for tomorrow",
|
||||
"SNOOZE": "Snooze",
|
||||
"SNOOZE_ALL": "Snooze all",
|
||||
"START": "Start",
|
||||
"TASK_REMINDERS": "Memento-uri sarcini",
|
||||
"UNSCHEDULE": "Unschedule",
|
||||
"UNSCHEDULE_ALL": "Unschedule all"
|
||||
"DUE_TASK": "Memento sarcină planificată",
|
||||
"DUE_TASKS": "Memento sarcini planificate",
|
||||
"RESCHEDULE_EDIT": "Editează (planifică din nou)",
|
||||
"RESCHEDULE_UNTIL_TOMORROW": "Planifică din nou pentru mîine",
|
||||
"SNOOZE": "Amînă",
|
||||
"SNOOZE_ALL": "Amînă-le pe toate",
|
||||
"START": "Începe",
|
||||
"TASK_REMINDERS": "Mementouri sarcini",
|
||||
"UNSCHEDULE": "Șterge planificarea",
|
||||
"UNSCHEDULE_ALL": "Șterge planificarea tuturor"
|
||||
},
|
||||
"D_SCHEDULE_TASK": {
|
||||
"QA_NEXT_MONTH": "Schedule next month",
|
||||
|
|
@ -1589,9 +1613,9 @@
|
|||
"UNSCHEDULE": "Unschedule"
|
||||
},
|
||||
"D_SELECT_DATE_AND_TIME": {
|
||||
"DATE": "Date",
|
||||
"TIME": "Time",
|
||||
"TITLE": "Select Date and Time"
|
||||
"DATE": "Dată",
|
||||
"TIME": "Oră",
|
||||
"TITLE": "Alege data și ora"
|
||||
},
|
||||
"D_TIME": {
|
||||
"ADD_FOR_OTHER_DAY": "Add time spent for other day",
|
||||
|
|
@ -1671,18 +1695,18 @@
|
|||
"OK": "Remove completely"
|
||||
},
|
||||
"D_CONFIRM_UPDATE_INSTANCES": {
|
||||
"CANCEL": "Only future tasks",
|
||||
"MSG": "There are {{tasksNr}} instances created for this recurring task. Do you want to update all of them with the new defaults or just future tasks?",
|
||||
"OK": "Update all instances"
|
||||
"CANCEL": "Doar sarcinile viitoare",
|
||||
"MSG": "Există {{tasksNr}} instanțe create pentru această sarcină recurentă. Dorești să le actualizezi pe toate cu noile valori implicite sau doar să actualizezi sarcinile viitoare?",
|
||||
"OK": "Actualizează toate instanțele"
|
||||
},
|
||||
"D_SKIP_INSTANCE": {
|
||||
"MSG": "Skip the recurring task on {{date}}? This will prevent the task from being created on this date only.",
|
||||
"MSG": "Omiți sarcina recurentă pe {{date}}? Acest lucru va preveni crearea sarcinii doar pe această dată.",
|
||||
"OK": "Omite"
|
||||
},
|
||||
"D_EDIT": {
|
||||
"ADD": "Add Recurring Task Config",
|
||||
"ADVANCED_CFG": "Advanced configuration",
|
||||
"EDIT": "Edit Recurring Task Config",
|
||||
"ADD": "Adaugă configurație sarcină recurentă",
|
||||
"ADVANCED_CFG": "Configurare avansată",
|
||||
"EDIT": "Editează configurația sarcinii recurente",
|
||||
"HEATMAP_LABEL": "Activitate",
|
||||
"TAG_LABEL": "Etichete",
|
||||
"TIME_SPENT_THIS_MONTH": "Luna aceasta",
|
||||
|
|
@ -1690,105 +1714,105 @@
|
|||
"TIME_SPENT_TOTAL": "Total"
|
||||
},
|
||||
"F": {
|
||||
"C_DAY": "Day",
|
||||
"C_MONTH": "Month",
|
||||
"C_WEEK": "Week",
|
||||
"C_YEAR": "Year",
|
||||
"DEFAULT_ESTIMATE": "Default Estimate",
|
||||
"DISABLE_AUTO_UPDATE_SUBTASKS": "Disable auto-updating subtasks",
|
||||
"DISABLE_AUTO_UPDATE_SUBTASKS_DESCRIPTION": "Do not update inherited subtasks automatically when the newest instance changes",
|
||||
"FRIDAY": "Friday",
|
||||
"INHERIT_SUBTASKS": "Inherit subtasks",
|
||||
"INHERIT_SUBTASKS_DESCRIPTION": "When enabled, subtasks from the most recent task instance will be recreated with the recurring task",
|
||||
"MONDAY": "Monday",
|
||||
"NOTES": "Default notes",
|
||||
"Q_CUSTOM": "Custom recurring config",
|
||||
"Q_DAILY": "Every day",
|
||||
"Q_MONDAY_TO_FRIDAY": "Every Monday through Friday",
|
||||
"Q_MONTHLY_CURRENT_DATE": "Every month on the day {{dateDayStr}}",
|
||||
"C_DAY": "Zi",
|
||||
"C_MONTH": "Lună",
|
||||
"C_WEEK": "Săptămînă",
|
||||
"C_YEAR": "An",
|
||||
"DEFAULT_ESTIMATE": "Estimare implicită",
|
||||
"DISABLE_AUTO_UPDATE_SUBTASKS": "Dezactivează auto-actualizarea subsarcinilor",
|
||||
"DISABLE_AUTO_UPDATE_SUBTASKS_DESCRIPTION": "Nu actualiza automat subsarcinile moștenate cînd instanța cea mai nouă se modifică",
|
||||
"FRIDAY": "vineri",
|
||||
"INHERIT_SUBTASKS": "Moștenește subsarcini",
|
||||
"INHERIT_SUBTASKS_DESCRIPTION": "Cînd este activat, subsarcinile din instanța cea mai recentă a sarcinii vor fi recreate cu sarcina recurentă",
|
||||
"MONDAY": "luni",
|
||||
"NOTES": "Notițe implicite",
|
||||
"Q_CUSTOM": "Configurare recurentă personalizată",
|
||||
"Q_DAILY": "În fiecare zi",
|
||||
"Q_MONDAY_TO_FRIDAY": "În fiecare zi de luni pînă vineri",
|
||||
"Q_MONTHLY_CURRENT_DATE": "În fiecare lună în ziua {{dateDayStr}}",
|
||||
"Q_MONTHLY_FIRST_DAY": "În fiecare lună în prima zi",
|
||||
"Q_MONTHLY_LAST_DAY": "În fiecare lună în ultima zi",
|
||||
"Q_WEEKLY_CURRENT_WEEKDAY": "Every week on {{weekdayStr}}",
|
||||
"Q_YEARLY_CURRENT_DATE": "Every year on the {{dayAndMonthStr}}",
|
||||
"QUICK_SETTING": "Recurring Config",
|
||||
"REMIND_AT": "Remind at",
|
||||
"REMIND_AT_PLACEHOLDER": "Select when to remind",
|
||||
"SKIP_FOR_DATE": "Skip for {{date}}",
|
||||
"SKIP_INSTANCE": "Skip for today",
|
||||
"REPEAT_CYCLE": "Recur cycle",
|
||||
"REPEAT_EVERY": "Recur every",
|
||||
"SATURDAY": "Saturday",
|
||||
"SCHEDULE_TYPE_LABEL": "Schedule type",
|
||||
"Q_WEEKLY_CURRENT_WEEKDAY": "În fiecare săptămînă {{weekdayStr}}",
|
||||
"Q_YEARLY_CURRENT_DATE": "În fiecare an pe {{dayAndMonthStr}}",
|
||||
"QUICK_SETTING": "Configurație recurentă",
|
||||
"REMIND_AT": "Amintește la",
|
||||
"REMIND_AT_PLACEHOLDER": "Selectează cînd să fie amintit",
|
||||
"SKIP_FOR_DATE": "Omite pentru {{date}}",
|
||||
"SKIP_INSTANCE": "Omite pentru astăzi",
|
||||
"REPEAT_CYCLE": "Ciclu de repetare",
|
||||
"REPEAT_EVERY": "Repetă la fiecare",
|
||||
"SATURDAY": "sîmbătă",
|
||||
"SCHEDULE_TYPE_LABEL": "Tip programare",
|
||||
"SKIP_OVERDUE": "Omite instanțele întîrziate",
|
||||
"SKIP_OVERDUE_DESCRIPTION": "Cînd este activat, dacă aplicația este deschisă după ce a fost omisă o apariție programată (de ex. o sarcină săptămînală în care azi nu este o zi programată), instanța ratată va fi omisă silențios în loc să fie creată ca sarcină întîrziată",
|
||||
"START_DATE": "Start date",
|
||||
"START_TIME": "Scheduled start time",
|
||||
"START_TIME_DESCRIPTION": "E.g. 15:00. Leave blank for an all day task",
|
||||
"SUNDAY": "Sunday",
|
||||
"THURSDAY": "Thursday",
|
||||
"TITLE": "Task title",
|
||||
"TUESDAY": "Tuesday",
|
||||
"WEDNESDAY": "Wednesday"
|
||||
"START_DATE": "Dată de început",
|
||||
"START_TIME": "Timpul programat de început",
|
||||
"START_TIME_DESCRIPTION": "De ex. 15:00. Lasă necompletat pentru o sarcină pe toată ziua",
|
||||
"SUNDAY": "duminică",
|
||||
"THURSDAY": "joi",
|
||||
"TITLE": "Nume sarcină",
|
||||
"TUESDAY": "marți",
|
||||
"WEDNESDAY": "miercuri"
|
||||
},
|
||||
"SNACK_REPEAT_DIALOG_FAIL": "Nu s-a putut deschide dialogul de configurare repetare. Puteți configura din meniul sarcinii."
|
||||
},
|
||||
"TASK_VIEW": {
|
||||
"CUSTOMIZER": {
|
||||
"DEADLINE_NEXT_MONTH": "Due Next Month",
|
||||
"DEADLINE_NEXT_WEEK": "Due Next Week",
|
||||
"DEADLINE_THIS_MONTH": "Due This Month",
|
||||
"DEADLINE_THIS_WEEK": "Due This Week",
|
||||
"DEADLINE_TODAY": "Due Today",
|
||||
"DEADLINE_TOMORROW": "Due Tomorrow",
|
||||
"ENTER_PROJECT": "Filter Projects",
|
||||
"ENTER_TAG": "Filter Tag",
|
||||
"ESTIMATED_TIME": "Estimated Time",
|
||||
"FILTER_BY": "Filter By",
|
||||
"FILTER_DEADLINE": "Deadline",
|
||||
"FILTER_DEFAULT": "No Filter",
|
||||
"FILTER_ESTIMATED_TIME": "Estimated Time",
|
||||
"FILTER_NOT_SPECIFIED": "Not specified",
|
||||
"FILTER_PROJECT": "Project",
|
||||
"FILTER_SCHEDULED_DATE": "Scheduled Date",
|
||||
"FILTER_TAG": "Tag",
|
||||
"FILTER_TIME_SPENT": "Time Spent",
|
||||
"GROUP_BY": "Group By",
|
||||
"GROUP_DEADLINE": "Deadline",
|
||||
"GROUP_DEADLINE_NONE": "No deadline",
|
||||
"GROUP_DEFAULT": "No Group",
|
||||
"GROUP_PROJECT": "Project",
|
||||
"GROUP_SCHEDULED_DATE": "Scheduled Date",
|
||||
"GROUP_TAG": "Tag",
|
||||
"RESET_ALL": "Reset All",
|
||||
"DEADLINE_NEXT_MONTH": "Scadente luna viitoare",
|
||||
"DEADLINE_NEXT_WEEK": "Scadente săptămîna viitoare",
|
||||
"DEADLINE_THIS_MONTH": "Scadente luna aceasta",
|
||||
"DEADLINE_THIS_WEEK": "Scadente săptămîna aceasta",
|
||||
"DEADLINE_TODAY": "Scadente astăzi",
|
||||
"DEADLINE_TOMORROW": "Scadente mîine",
|
||||
"ENTER_PROJECT": "Filtrează proiecte",
|
||||
"ENTER_TAG": "Filtrează etichete",
|
||||
"ESTIMATED_TIME": "Timp estimat",
|
||||
"FILTER_BY": "Filtrează după",
|
||||
"FILTER_DEADLINE": "Scadență",
|
||||
"FILTER_DEFAULT": "Fără filtru",
|
||||
"FILTER_ESTIMATED_TIME": "Timp estimat",
|
||||
"FILTER_NOT_SPECIFIED": "Nu este specificat",
|
||||
"FILTER_PROJECT": "Proiect",
|
||||
"FILTER_SCHEDULED_DATE": "Dată programată",
|
||||
"FILTER_TAG": "Etichetă",
|
||||
"FILTER_TIME_SPENT": "Timp petrecut",
|
||||
"GROUP_BY": "Grupare după",
|
||||
"GROUP_DEADLINE": "Scadență",
|
||||
"GROUP_DEADLINE_NONE": "Fără scadență",
|
||||
"GROUP_DEFAULT": "Fără grup",
|
||||
"GROUP_PROJECT": "Proiect",
|
||||
"GROUP_SCHEDULED_DATE": "Dată programată",
|
||||
"GROUP_TAG": "Etichetă",
|
||||
"RESET_ALL": "Resetează tot",
|
||||
"SAVE_SORT": "Salvează ca implicit",
|
||||
"SCHEDULED_NEXT_MONTH": "Next Month",
|
||||
"SCHEDULED_NEXT_WEEK": "Next Week",
|
||||
"SCHEDULED_THIS_MONTH": "This Month",
|
||||
"SCHEDULED_THIS_WEEK": "This Week",
|
||||
"SCHEDULED_NEXT_MONTH": "Luna viitoare",
|
||||
"SCHEDULED_NEXT_WEEK": "Săptămîna viitoare",
|
||||
"SCHEDULED_THIS_MONTH": "Luna aceasta",
|
||||
"SCHEDULED_THIS_WEEK": "Săptămîna aceasta",
|
||||
"SCHEDULED_TODAY": "Astăzi",
|
||||
"SCHEDULED_TOMORROW": "Mîine",
|
||||
"SORT": "Sortare",
|
||||
"SORT_CREATION_DATE": "Creation Date",
|
||||
"SORT_DEADLINE": "Deadline",
|
||||
"SORT_DEFAULT": "Default",
|
||||
"SORT_NAME": "Name",
|
||||
"SORT_SCHEDULED_DATE": "Scheduled Date",
|
||||
"TIME_1HOUR": "> 1 Hour",
|
||||
"TIME_2HOUR": "> 2 Hours",
|
||||
"TIME_10MIN": "> 10 Minutes",
|
||||
"TIME_30MIN": "> 30 Minutes",
|
||||
"TIME_SPENT": "Time Spent"
|
||||
"SORT_CREATION_DATE": "Dată de creare",
|
||||
"SORT_DEADLINE": "Scadență",
|
||||
"SORT_DEFAULT": "Implicit",
|
||||
"SORT_NAME": "Nume",
|
||||
"SORT_SCHEDULED_DATE": "Dată programată",
|
||||
"TIME_1HOUR": "> 1 oră",
|
||||
"TIME_2HOUR": "> 2 ore",
|
||||
"TIME_10MIN": "> 10 minute",
|
||||
"TIME_30MIN": "> 30 minute",
|
||||
"TIME_SPENT": "Timp petrecut"
|
||||
}
|
||||
},
|
||||
"TIME_TRACKING": {
|
||||
"B": {
|
||||
"ALREADY_DID": "I already did",
|
||||
"SNOOZE": "Snooze {{time}}"
|
||||
"ALREADY_DID": "Deja am făcut",
|
||||
"SNOOZE": "Amînă {{time}}"
|
||||
},
|
||||
"B_TTR": {
|
||||
"ADD_TO_TASK": "Add to Task",
|
||||
"MSG": "You have not been tracking time for {{time}}",
|
||||
"MSG_WITHOUT_TIME": "You have not been tracking time"
|
||||
"ADD_TO_TASK": "Adaugă la sarcină",
|
||||
"MSG": "Nu ai ținut evidența timpului pentru {{time}}",
|
||||
"MSG_WITHOUT_TIME": "Nu ai ținut evidența timpului"
|
||||
},
|
||||
"D_ARCHIVE_COMPRESS": {
|
||||
"BTN_COMPRESS": "Comprimă arhiva",
|
||||
|
|
@ -1928,10 +1952,10 @@
|
|||
"DO_IT": "Fă-l!",
|
||||
"DONT_SHOW_AGAIN": "Nu mai afișa din nou",
|
||||
"DUPLICATE": "Duplică",
|
||||
"DURATION_DESCRIPTION": "ex: \"5h 23m\" care rezultă în 5 ore și 23 minute",
|
||||
"DURATION_DESCRIPTION": "d.e. \"5h 23m\" care rezultă în 5 ore și 23 minute",
|
||||
"EDIT": "Editează",
|
||||
"ENABLED": "Activat",
|
||||
"EXAMPLE_VAL": "ex: 32m",
|
||||
"EXAMPLE_VAL": "d.e. 32m",
|
||||
"EXTENSION_INFO": "Te rog <a target=\"_blank\" href=\"https://chrome.google.com/webstore/detail/super-productivity/ljkbjodfmekklcoibdnhahlaalhihmlb\"> descarcă extensia Chrome</a> pentru a permite comunicarea cu API-ul Jira și tratarea timpului mort. Acest lucru nu funcționează pe mobil. <strong>Fără extensie, aceste funcții nu vor funcționa!</strong>",
|
||||
"HIDE": "Ascunde",
|
||||
"ICON_INP_DESCRIPTION": "Toate emoji-urile UTF-8 sînt de asemenea suportate!",
|
||||
|
|
@ -1942,7 +1966,7 @@
|
|||
"NEXT": "Următorul",
|
||||
"NO_CON": "Momentan ești offline. Te rog reconectează-te la internet.",
|
||||
"NONE": "Niciunul",
|
||||
"OK": "Ok",
|
||||
"OK": "OK",
|
||||
"OPEN_IN_BROWSER": "Deschide în navigatorul web",
|
||||
"OVERDUE": "Restante",
|
||||
"PASSWORD_STRENGTH_FAIR": "Acceptabilă",
|
||||
|
|
@ -1962,25 +1986,27 @@
|
|||
},
|
||||
"GCF": {
|
||||
"APP_FEATURES": {
|
||||
"BOARDS": "Boards",
|
||||
"DONATE_PAGE": "Donate page",
|
||||
"BOARDS": "Table",
|
||||
"DONATE_PAGE": "Pagină de donații",
|
||||
"FINISH_DAY": "Încheie ziua",
|
||||
"FOCUS_MODE": "Focus mode",
|
||||
"FOCUS_MODE": "Mod concentrare",
|
||||
"HABITS": "Obiceiuri",
|
||||
"HELP": "Enable or disable specific app features across the UI.",
|
||||
"ISSUES_PANEL": "Issues panel",
|
||||
"PLANNER": "Planner",
|
||||
"PROJECT_NOTES": "Project notes",
|
||||
"HELP": "Activează sau dezactivează anumite funcții ale aplicației din interfață.",
|
||||
"ISSUES_PANEL": "Panou probleme",
|
||||
"PLANNER": "Planificator",
|
||||
"PROJECT_NOTES": "Notițe proiect",
|
||||
"SCHEDULE": "Orar",
|
||||
"SCHEDULE_DAY_PANEL": "Schedule day panel",
|
||||
"SCHEDULE_DAY_PANEL": "Panou programare zi",
|
||||
"SEARCH": "Caută",
|
||||
"SYNC_BUTTON": "Sync button",
|
||||
"TIME_TRACKING": "Stopwatch time tracking",
|
||||
"TITLE": "App Features",
|
||||
"USER_PROFILES": "User profiles (experimental)",
|
||||
"USER_PROFILES_HINT": "Allows you to create and switch between different user profiles, each with separate settings, tasks, and sync configurations. The profile management button will appear in the top-right corner when enabled. Note: Disabling this feature will hide the UI but preserve your profile data. (Experimental feature. No guarantees. Make sure to have a backup!)",
|
||||
"USER_PROFILES_WARNING": "<strong>Funcție experimentală:</strong> Această funcție este încă în dezvoltare și poate fi eliminată sau modificată semnificativ într-o actualizare viitoare. Folosește pe propriul risc și asigură-te că ai backup-uri ale datelor."
|
||||
},
|
||||
"SYNC_BUTTON": "Buton sincronizare",
|
||||
"TIME_TRACKING": "Cronometru urmărire timp",
|
||||
"TITLE": "Funcții aplicație",
|
||||
"USER_PROFILES": "Profiluri utilizator (experimental — ar putea fi eliminate)",
|
||||
"USER_PROFILES_HINT": "Îți permite să creezi și să comuti între diferite profiluri de utilizator, fiecare cu setări, sarcini și configurații de sincronizare separate. Butonul de gestionare a profilurilor va apărea în colțul din dreapta sus cînd este activat. Notă: Dezactivarea acestei funcții va ascunde interfața dar va păstra datele profilului. (Funcție experimentală. Fără garanții. Asigură-te că ai o copie de rezervă!)",
|
||||
"USER_PROFILES_WARNING": "<strong>Funcție experimentală:</strong> Această funcție este încă în dezvoltare și ar putea fi eliminată sau modificată semnificativ într-o actualizare viitoare. Folosește cu grijă și asigură-te că ai copii de rezervă ale datelor tale.",
|
||||
"EXPERIMENTAL_WARNING_TITLE": "Feature Experimental",
|
||||
"EXPERIMENTAL_WARNING_MSG": "Această funcție este experimentală. Poate conține erori, poate fi modificată semnificativ sau poate fi eliminată într-o actualizare viitoare. Asigură-te că ai copii de rezervă ale datelor tale.<br/><br/>Vrei să o activezi?"
|
||||
},
|
||||
"AUTO_BACKUPS": {
|
||||
"HELP": "Automatically save all data to your app folder to have it ready if something goes wrong.",
|
||||
"LABEL_IS_ENABLED": "Enable automatic backups",
|
||||
|
|
@ -1995,8 +2021,8 @@
|
|||
"SHOW_BANNER_THRESHOLD": "Show a notification X before the event (blank for disabled)"
|
||||
},
|
||||
"CLIPBOARD_IMAGES": {
|
||||
"DELETE": "Delete image",
|
||||
"DELETE_ALL": "Delete All",
|
||||
"DELETE": "Șterge imaginea",
|
||||
"DELETE_ALL": "Șterge-le pe toate",
|
||||
"DELETE_ALL_SUCCESS": "Toate imaginile din clipboard șterse cu succes",
|
||||
"DELETE_SUCCESS": "Imagine din clipboard ștearsă cu succes",
|
||||
"ERROR_DELETING": "Ștergerea imaginii din clipboard a eșuat",
|
||||
|
|
@ -2013,14 +2039,14 @@
|
|||
"PATH_LABEL": "Cale stocare imagini",
|
||||
"PATH_PLACEHOLDER": "Lasă gol pentru locația implicită",
|
||||
"PATH_TITLE": "Locație stocare",
|
||||
"SELECT_PATH": "Select Folder",
|
||||
"SELECT_PATH": "Selectează dosarul",
|
||||
"SELECT_PATH_TITLE": "Selectează dosarul pentru imagini clipboard",
|
||||
"SORT_BY": "Sort by",
|
||||
"SORT_LARGEST": "Largest first",
|
||||
"SORT_NEWEST": "Newest first",
|
||||
"SORT_OLDEST": "Oldest first",
|
||||
"SORT_SMALLEST": "Smallest first",
|
||||
"TITLE": "Imagini clipboard",
|
||||
"SORT_BY": "Sortează după",
|
||||
"SORT_LARGEST": "Cele mai mari primele",
|
||||
"SORT_NEWEST": "Cele mai noi primele",
|
||||
"SORT_OLDEST": "Cele mai vechi primele",
|
||||
"SORT_SMALLEST": "Cele mai mici primele",
|
||||
"TITLE": "Imagini Clipboard",
|
||||
"TOTAL_IMAGES": "Total imagini",
|
||||
"TOTAL_SIZE": "Dimensiune totală"
|
||||
},
|
||||
|
|
@ -2149,6 +2175,7 @@
|
|||
"TIME_LOCALE_IT_IT": "Italienesc: 24 de ore, DD/MM/YYYY",
|
||||
"TIME_LOCALE_JA_JP": "Japonez: 24 de ore, YYYY/MM/DD",
|
||||
"TIME_LOCALE_KO_KR": "Korean: 12 ore AM/PM, YYYY. MM. DD",
|
||||
"TIME_LOCALE_PL_PL": "Polonez: 24 de ore, DD.MM.YYYY",
|
||||
"TIME_LOCALE_PT_BR": "Portughez (Brazilia): 24 de ore, DD/MM/YYYY",
|
||||
"TIME_LOCALE_RO_MD": "Românesc (Moldova): 24 de ore, DD.MM.YYYY",
|
||||
"TIME_LOCALE_RO_RO": "Românesc: 24 de ore, DD.MM.YYYY",
|
||||
|
|
@ -2218,10 +2245,10 @@
|
|||
"L_LUNCH_BREAK_START": "Lunch break start",
|
||||
"L_WORK_END": "Work Day End",
|
||||
"L_WORK_START": "Work Day Start",
|
||||
"LUNCH_BREAK_START_END_DESCRIPTION": "ex: 13:00",
|
||||
"LUNCH_BREAK_START_END_DESCRIPTION": "d.e. 13:00",
|
||||
"MONTH": "Lună",
|
||||
"TITLE": "Programare",
|
||||
"WORK_START_END_DESCRIPTION": "ex: 17:00"
|
||||
"WORK_START_END_DESCRIPTION": "d.e. 17:00"
|
||||
},
|
||||
"SHORT_SYNTAX": {
|
||||
"HELP": "<p>Here you can control short syntax options when creating a task</p>",
|
||||
|
|
@ -2235,9 +2262,9 @@
|
|||
"URL_BEHAVIOR_LABEL": "URL behavior when adding tasks"
|
||||
},
|
||||
"SOUND": {
|
||||
"BREAK_REMINDER_SOUND": "Take a break reminder sound",
|
||||
"DONE_SOUND": "Task completed sound",
|
||||
"IS_INCREASE_DONE_PITCH": "Increase pitch for every task completed",
|
||||
"BREAK_REMINDER_SOUND": "Sunet memento de luat o pauză",
|
||||
"DONE_SOUND": "Sunet sarcină finalizată",
|
||||
"IS_INCREASE_DONE_PITCH": "Crește tonul pentru fiecare sarcină finalizată",
|
||||
"TITLE": "Sunet",
|
||||
"TRACK_TIME_SOUND": "Sunet memento de urmărire a timpului",
|
||||
"VOLUME": "Volum"
|
||||
|
|
@ -2646,18 +2673,19 @@
|
|||
"CREATE_NEW_PROFILE": "Creează un nou profil",
|
||||
"CREATED": "Creat:",
|
||||
"DELETE_PROFILE": "Șterge profilul",
|
||||
"DEPRECATION_WARNING": "Funcția experimentală Profile utilizator va fi eliminată într-o actualizare viitoare. Te rog să îți exporti datele de profil ca backup.",
|
||||
"DIALOG_TITLE": "Administrează profilurile de utilizator",
|
||||
"EXISTING_PROFILES": "Profiluri existente",
|
||||
"EXPORT_PROFILE": "Exportă profilul",
|
||||
"MANAGE_PROFILES": "Gestionează profilurile...",
|
||||
"PROFILE_NAME": "Nume profil",
|
||||
"PROFILE_NAME_PLACEHOLDER": "ex: Muncă, Personal",
|
||||
"PROFILE_NAME_PLACEHOLDER": "d.e. Muncă, Personal",
|
||||
"RENAME": "Redenumește",
|
||||
"SAVE": "Salvează"
|
||||
},
|
||||
"V": {
|
||||
"E_DATETIME": "Valoare întrodusă nu este o dată și oră!",
|
||||
"E_DURATION": "Te rog întrodu o durată validă (ex: 1h)",
|
||||
"E_DURATION": "Te rog întrodu o durată validă (d.e. 1h)",
|
||||
"E_MAX": "Nu trebuie să fie mai mare decît {{val}}",
|
||||
"E_MAX_LENGTH": "Trebuie să aibă cel mult {{val}} caractere",
|
||||
"E_MIN": "Nu trebuie să fie mai mică decît {{val}}",
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@
|
|||
"BY": "după",
|
||||
"NO_RESULTS": "Nicio imagine găsită. Te rog încearcă un alt termen de căutare.",
|
||||
"SEARCH_LABEL": "Caută imagini",
|
||||
"SEARCH_PLACEHOLDER": "ex: natură, munți, abstract",
|
||||
"SEARCH_PLACEHOLDER": "d.e. natură, munți, abstract",
|
||||
"TITLE": "Alege o imagine de fundal din Unsplash"
|
||||
},
|
||||
"DONATE_PAGE": {
|
||||
|
|
@ -94,8 +94,8 @@
|
|||
"ADD_NEW_PANEL": "Adaugă un nou panou",
|
||||
"BACKLOG_TASK_FILTER_ALL": "Toate",
|
||||
"BACKLOG_TASK_FILTER_NO_BACKLOG": "Exclude",
|
||||
"BACKLOG_TASK_FILTER_ONLY_BACKLOG": "Doar restante",
|
||||
"BACKLOG_TASK_FILTER_TYPE": "Sarcini restante",
|
||||
"BACKLOG_TASK_FILTER_ONLY_BACKLOG": "Doar backlog",
|
||||
"BACKLOG_TASK_FILTER_TYPE": "Sarcini din backlog",
|
||||
"COLUMNS": "Coloane",
|
||||
"TAGS_EXCLUDED": "Etichete excluse",
|
||||
"TAGS_REQUIRED": "Etichete obligatorii",
|
||||
|
|
@ -167,8 +167,18 @@
|
|||
"EVENT_STR": "eveniment",
|
||||
"EVENTS_STR": "evenimente"
|
||||
},
|
||||
"CONTEXT_MENU": {
|
||||
"CREATE_TASK": "Creează ca sarcină",
|
||||
"OPEN_IN_CALENDAR": "Deschide în calendar",
|
||||
"RESCHEDULE": "Replanifică",
|
||||
"DELETE_EVENT": "Șterge evenimentul"
|
||||
},
|
||||
"S": {
|
||||
"CAL_PROVIDER_ERROR": "Eroare furnizor calendar: {{errTxt}}"
|
||||
"CAL_PROVIDER_ERROR": "Eroare furnizor calendar: {{errTxt}}",
|
||||
"EVENT_HIDDEN": "Eveniment calendar ascuns",
|
||||
"EVENT_RESCHEDULED": "Eveniment reprogramat",
|
||||
"EVENT_DELETED": "Eveniment șters",
|
||||
"TIME_BLOCK_ERROR": "Nu s-a putut sincroniza blocul de timp: {{errTxt}}"
|
||||
}
|
||||
},
|
||||
"CLIPBOARD_IMAGE": {
|
||||
|
|
@ -265,7 +275,7 @@
|
|||
},
|
||||
"GITEA": {
|
||||
"FORM": {
|
||||
"HOST": "Gazdă (ex: https://try.gitea.io)",
|
||||
"HOST": "Gazdă (d.e. https://try.gitea.io)",
|
||||
"REPO_FULL_NAME": "Nume utilizator sau nume organizație/proiect",
|
||||
"REPO_FULL_NAME_DESCRIPTION": "Poate fi găsit ca parte a URL-ului când vizualizezi proiectul într-un navigator web.",
|
||||
"SCOPE": "Domeniu de aplicare",
|
||||
|
|
@ -303,7 +313,7 @@
|
|||
"FILTER_USER": "Filtrează după numele de utilizator",
|
||||
"GITLAB_BASE_URL": "URL de bază personalizat GitLab (opțional)",
|
||||
"PROJECT": "nume utilizator/proiect",
|
||||
"PROJECT_HINT": "ex: super-productivity/super-productivity",
|
||||
"PROJECT_HINT": "d.e. super-productivity/super-productivity",
|
||||
"SCOPE": "Domeniu de aplicare",
|
||||
"SCOPE_ALL": "Toate",
|
||||
"SCOPE_ASSIGNED": "Atribuite mie",
|
||||
|
|
@ -421,8 +431,16 @@
|
|||
"DIALOG": {
|
||||
"ADVANCED_CONFIG": "Configurare avansată",
|
||||
"CONNECTED": "Conectat",
|
||||
"DELETE_CONFIRM": "Dorești să ștergi acest furnizor de probleme? Ștergerea înseamnă că <strong>toate sarcinile de probleme importate anterior vor pierde referința</strong>. Aceasta nu poate fi anulată!",
|
||||
"DISCONNECT": "Deconectează"
|
||||
"DELETE_CONFIRM": "Dorești să ștergi acest furnizor de probleme? Ștergerea înseamnă că <strong>toate sarcinile de probleme importate anterior vor pierde referința</strong>. Aceasta acțiune nu poate fi anulată!",
|
||||
"DISCONNECT": "Deconectează",
|
||||
"EDIT_TITLE": "Editează configurația {{title}}",
|
||||
"LOAD_OPTIONS": "Încarcă opțiunile",
|
||||
"LOAD_OPTIONS_FAILED": "Nu s-au putut încărca opțiunile",
|
||||
"LOADING_OPTIONS": "Se încarcă opțiunile...",
|
||||
"OPTIONS_LOADED": "Opțiuni încărcate",
|
||||
"RELOAD_OPTIONS": "Reîncarcă opțiunile",
|
||||
"SETUP_TITLE": "Editează configurația {{title}}",
|
||||
"TEST_CONNECTION": "Testează conexiunea"
|
||||
}
|
||||
},
|
||||
"ISSUE_PANEL": {
|
||||
|
|
@ -432,37 +450,37 @@
|
|||
},
|
||||
"JIRA": {
|
||||
"BANNER": {
|
||||
"BLOCK_ACCESS_MSG": "Jira: To prevent API lock out, access has been blocked by Super Productivity. You probably should check your Jira settings!",
|
||||
"BLOCK_ACCESS_UNBLOCK": "Unblock"
|
||||
"BLOCK_ACCESS_MSG": "Jira: Pentru a preveni blocarea API-ului, accesul a fost blocat de Super Productivity. Probabil ar trebui să verifici setările Jira!",
|
||||
"BLOCK_ACCESS_UNBLOCK": "Deblochează"
|
||||
},
|
||||
"CFG_CMP": {
|
||||
"ALWAYS_ASK": "Always open dialog",
|
||||
"DO_NOT": "Don't transition",
|
||||
"DONE": "Status for completing task",
|
||||
"ENABLE": "Enable Jira integration",
|
||||
"ENABLE_TRANSITIONS": "Enable Transition Handling",
|
||||
"IN_PROGRESS": "Status for starting task",
|
||||
"LOAD_SUGGESTIONS": "Load Suggestions",
|
||||
"MAP_CUSTOM_FIELDS": "Load Story Points",
|
||||
"MAP_CUSTOM_FIELDS_INFO": "Unfortunately some of Jira's data is saved under custom fields which are different for every installation. To include this data, you need to select the proper custom field for it. Currently there is only the story points field that needs to be mapped.",
|
||||
"OPEN": "Status for pausing task",
|
||||
"SELECT_ISSUE_FOR_TRANSITIONS": "Select issue to load available transitions",
|
||||
"STORY_POINTS": "Story Points Field Name",
|
||||
"TRANSITION": "Transition Handling"
|
||||
"ALWAYS_ASK": "Deschide mereu dialogul",
|
||||
"DO_NOT": "Nu tranziționa",
|
||||
"DONE": "Stare pentru finalizarea sarcinii",
|
||||
"ENABLE": "Activează integrarea Jira",
|
||||
"ENABLE_TRANSITIONS": "Activează gestionarea tranzițiilor",
|
||||
"IN_PROGRESS": "Stare pentru începerea sarcinii",
|
||||
"LOAD_SUGGESTIONS": "Încarcă sugestiile",
|
||||
"MAP_CUSTOM_FIELDS": "Încarcă Story Points",
|
||||
"MAP_CUSTOM_FIELDS_INFO": "Din păcate unele din datele Jira sunt salvate sub câmpuri particulare (custom fields) care sunt diferite pentru fiecare instalare. Pentru a include aceste date, trebuie să selectezi câmpul personalizat adecvat. Momentan, există doar câmpul pentru Story Points care trebuie asociat.",
|
||||
"OPEN": "Stare pentru punerea pe pauză a sarcinii",
|
||||
"SELECT_ISSUE_FOR_TRANSITIONS": "Selectează problema pentru a încărca tranzițiile disponibile",
|
||||
"STORY_POINTS": "Nume câmp Story Points",
|
||||
"TRANSITION": "Gestionarea tranzițiilor"
|
||||
},
|
||||
"DIALOG_CONFIRM_ASSIGNMENT": {
|
||||
"MSG": "<strong>{{summary}}</strong> is currently assigned to <strong>{{assignee}}</strong>. Do you want to assign it to yourself?",
|
||||
"OK": "Do it!"
|
||||
"MSG": "<strong>{{summary}}</strong> Este asociat curent cu <strong>{{assignee}}</strong>. Dorești să îl asociezi pentru tine?",
|
||||
"OK": "Fă-o!"
|
||||
},
|
||||
"DIALOG_INITIAL": {
|
||||
"TITLE": "Setup Jira for Project"
|
||||
"TITLE": "Configurează Jira pentru proiect"
|
||||
},
|
||||
"DIALOG_TRANSITION": {
|
||||
"CHOOSE_STATUS": "Choose status to assign",
|
||||
"CURRENT_ASSIGNEE": "Current Assignee:",
|
||||
"CURRENT_STATUS": "Current Status:",
|
||||
"TASK_NAME": "Task name:",
|
||||
"TITLE": "Jira: Update Status"
|
||||
"CHOOSE_STATUS": "Alege starea de atribuit",
|
||||
"CURRENT_ASSIGNEE": "Atribuit curent:",
|
||||
"CURRENT_STATUS": "Starea curentă:",
|
||||
"TASK_NAME": "Nume sarcină:",
|
||||
"TITLE": "Jira: Actualizează starea"
|
||||
},
|
||||
"DIALOG_WORKLOG": {
|
||||
"CHECKBOXES": {
|
||||
|
|
@ -929,32 +947,32 @@
|
|||
"TEXT": "<p>Orarul îți oferă o viziune de ansamblu despre cum se desfășoară sarcinile planificate în timp. Este generat automat din sarcinile tale și necesită doar <strong>estimări de timp</strong> pentru a funcționa.</p><p>Două lucruri sunt distinse: <strong>Sarcini programate</strong>, care sunt afișate la ora planificată și <strong>Sarcini obișnuite</strong>, care ar trebui să curgă în jurul acestor evenimente fixe.</p><p>Dacă setezi o oră de început și sfârșit a lucrului (recomandat), sarcinile obișnuite nu vor fi niciodată plasate în afara acestor limite.</p>",
|
||||
"TITLE": "Orar"
|
||||
},
|
||||
"END": "Work End",
|
||||
"INSERT_BEFORE": "Before",
|
||||
"LUNCH_BREAK": "Lunch Break",
|
||||
"MONTH": "Month",
|
||||
"NO_TASKS": "Currently there are no tasks. Please add some tasks via the plus (+) button in the top bar.",
|
||||
"NOW": "Now",
|
||||
"PLAN_END_DAY": "End of {{date}}",
|
||||
"PLAN_START_DAY": "Start of {{date}}",
|
||||
"SHIFT_KEY_INFO": "Hold Shift to toggle day planning mode",
|
||||
"START": "Work Start"
|
||||
"END": "Finalul muncii",
|
||||
"INSERT_BEFORE": "Înainte",
|
||||
"LUNCH_BREAK": "Pauză de masă",
|
||||
"MONTH": "Lună",
|
||||
"NO_TASKS": "Momentan nu există sarcini. Adaugă câteva sarcini prin butonul plus (+) din bara de sus.",
|
||||
"NOW": "Acum",
|
||||
"PLAN_END_DAY": "Final de {{date}}",
|
||||
"PLAN_START_DAY": "Început de {{date}}",
|
||||
"SHIFT_KEY_INFO": "Ține apăsat Shift pentru a comuta modul de planificare zilnică",
|
||||
"START": "Începerea muncii"
|
||||
},
|
||||
"SEARCH_BAR": {
|
||||
"INFO": "Start typing to search for tasks",
|
||||
"NO_RESULTS": "No tasks found matching your search",
|
||||
"PLACEHOLDER": "Search for task or task notes"
|
||||
"INFO": "Începe să tastezi pentru a căuta sarcini",
|
||||
"NO_RESULTS": "Nu s-au găsit sarcini care să se potrivească căutării",
|
||||
"PLACEHOLDER": "Caută sarcină sau notițe despre sarcină"
|
||||
},
|
||||
"SIMPLE_COUNTER": {
|
||||
"D_CONFIRM_REMOVE": {
|
||||
"MSG": "Deleting a simple counter, will also delete all past data tracked on it. Are you sure you want to proceed?",
|
||||
"OK": "Do it!"
|
||||
"MSG": "Ștergerea unui contor simplu va șterge și toate datele urmărite pe el. Ești sigur că vrei să continui?",
|
||||
"OK": "Fă-o!"
|
||||
},
|
||||
"D_EDIT": {
|
||||
"CURRENT_STREAK": "Current Streak",
|
||||
"DAILY_GOAL": "Daily Goal",
|
||||
"DAYS": "Days",
|
||||
"L_COUNTER": "Count"
|
||||
"CURRENT_STREAK": "Serie curentă",
|
||||
"DAILY_GOAL": "Obiectiv zilnic",
|
||||
"DAYS": "Zile",
|
||||
"L_COUNTER": "Număr"
|
||||
},
|
||||
"FORM": {
|
||||
"ADD_NEW": "Adaugă un contor simplu sau un obicei",
|
||||
|
|
@ -967,7 +985,7 @@
|
|||
"L_IS_HIDE_BUTTON": "Ascunde butonul din bara de unelte",
|
||||
"L_STREAK_MODE": "Mod urmărire serie",
|
||||
"L_STREAK_MODE_SPECIFIC_DAYS": "Zilele săptămânii",
|
||||
"L_STREAK_MODE_WEEKLY_FREQUENCY": "Frecvență săptămânală (ex: de 3 ori pe săptămână)",
|
||||
"L_STREAK_MODE_WEEKLY_FREQUENCY": "Frecvență săptămânală (d.e. de 3 ori pe săptămână)",
|
||||
"L_TITLE": "Titlu",
|
||||
"L_TRACK_STREAKS": "Urmărește seriile",
|
||||
"L_TYPE": "Tip",
|
||||
|
|
@ -1000,40 +1018,40 @@
|
|||
"FORCE_UPLOAD": "Forțarea încărcării datelor poate duce la pierderea acestora. Ești sigur că vrei să continui?"
|
||||
},
|
||||
"D_AUTH_CODE": {
|
||||
"FOLLOW_LINK": "Please open the following link and copy the auth code provided there into the input field below.",
|
||||
"FOLLOW_LINK_MOBILE": "Please tap the button below to authorize. You will be automatically redirected back to the app.",
|
||||
"GET_AUTH_CODE": "Get Authorization Code",
|
||||
"L_AUTH_CODE": "Enter Auth Code",
|
||||
"TITLE": "Login: {{provider}}",
|
||||
"WAITING_FOR_AUTH": "Waiting for authentication..."
|
||||
"FOLLOW_LINK": "Te rog deschide următoarea legătură și copiază codul de autorizare furnizat acolo în câmpul de intrare de mai jos.",
|
||||
"FOLLOW_LINK_MOBILE": "Te rog apasă butonul de mai jos pentru a autoriza. Vei fi redirecționat automat înapoi la aplicație.",
|
||||
"GET_AUTH_CODE": "Obține codul de autorizare",
|
||||
"L_AUTH_CODE": "Introdu codul de autorizare",
|
||||
"TITLE": "Autentificare: {{provider}}",
|
||||
"WAITING_FOR_AUTH": "Se așteaptă autentificarea..."
|
||||
},
|
||||
"D_CONFLICT": {
|
||||
"ADDITIONAL_INFO": "Additional Info",
|
||||
"CHANGES": "Changes",
|
||||
"CHANGES_SINCE_LAST_SYNC": "Changes since last sync",
|
||||
"COMPARISON_RESULT": "Comparison Result",
|
||||
"DATE": "Date",
|
||||
"LAST_SYNC": "Last Sync:",
|
||||
"LAST_SYNCED": "Last Synced",
|
||||
"LAST_WRITE": "Last Write",
|
||||
"ADDITIONAL_INFO": "Informații suplimentare",
|
||||
"CHANGES": "Modificări",
|
||||
"CHANGES_SINCE_LAST_SYNC": "Modificări de la ultima sincronizare",
|
||||
"COMPARISON_RESULT": "Rezultatul comparării",
|
||||
"DATE": "Dată",
|
||||
"LAST_SYNC": "Ultima Sincronizare:",
|
||||
"LAST_SYNCED": "Ultima sincronizare",
|
||||
"LAST_WRITE": "Ultima scriere",
|
||||
"LOCAL": "Local",
|
||||
"LOCAL_REMOTE": "Local -> Remote",
|
||||
"NEVER": "Never",
|
||||
"OVERWRITE_WARNING": "WARNING: The {{targetName}} data contains approximately {{targetChanges}} changes while the {{sourceName}} data only has {{sourceChanges}} changes. Are you sure you want to overwrite the {{targetName}} data with the {{sourceName}} version? This action cannot be undone.",
|
||||
"REMOTE": "Remote",
|
||||
"RESULT": "Result",
|
||||
"TEXT": "<p>Update from remote. Both local and remote data seem to be modified.</p>",
|
||||
"TIME": "Time",
|
||||
"TIMESTAMP": "Timestamp",
|
||||
"TITLE": "Sync: Conflicting Data",
|
||||
"USE_LOCAL": "Keep local",
|
||||
"USE_REMOTE": "Keep remote",
|
||||
"VECTOR_CLOCK": "Vector Clock",
|
||||
"VECTOR_CLOCK_HEADING": "Vector Clock",
|
||||
"VECTOR_COMPARISON_CONCURRENT": "Concurrent (True Conflict)",
|
||||
"VECTOR_COMPARISON_EQUAL": "Equal",
|
||||
"VECTOR_COMPARISON_LOCAL_GREATER": "Local > Remote",
|
||||
"VECTOR_COMPARISON_LOCAL_LESS": "Local < Remote"
|
||||
"LOCAL_REMOTE": "Local -> Distant",
|
||||
"NEVER": "Niciodată",
|
||||
"OVERWRITE_WARNING": "ATENȚIE: Datele {{targetName}} conțin aproximativ {{targetChanges}} modificări în timp ce datele {{sourceName}} au doar {{sourceChanges}} modificări. Ești sigur că vrei să înlocuiești datele {{targetName}} cu versiunea {{sourceName}}? Această acțiune nu poate fi anulată.",
|
||||
"REMOTE": "Distant",
|
||||
"RESULT": "Rezultat",
|
||||
"TEXT": "<p>Actualizează de la distanță. Atât datele locale cât și cele de pe server par modificate.</p>",
|
||||
"TIME": "Timp",
|
||||
"TIMESTAMP": "Marca temporală",
|
||||
"TITLE": "Sincronizare: Date conflictuale",
|
||||
"USE_LOCAL": "Păstrează local",
|
||||
"USE_REMOTE": "Păstrează distant",
|
||||
"VECTOR_CLOCK": "Ceas vectorial",
|
||||
"VECTOR_CLOCK_HEADING": "Ceas vectorial",
|
||||
"VECTOR_COMPARISON_CONCURRENT": "Concurent (conflict adevărat)",
|
||||
"VECTOR_COMPARISON_EQUAL": "Egal",
|
||||
"VECTOR_COMPARISON_LOCAL_GREATER": "Local > Distant",
|
||||
"VECTOR_COMPARISON_LOCAL_LESS": "Local < Distant"
|
||||
},
|
||||
"D_DATA_REPAIR_CONFIRM": {
|
||||
"MSG": "Datele tale par deteriorate. Dorești să încerci repararea automată? Aceasta poate duce la pierderea minoră de date.",
|
||||
|
|
@ -1045,28 +1063,28 @@
|
|||
"UNKNOWN_ERROR": "Eroare necunoscută de sincronizare: {{err}}"
|
||||
},
|
||||
"D_DECRYPT_ERROR": {
|
||||
"BTN_OVER_WRITE_REMOTE": "Use This Password & Overwrite Remote",
|
||||
"CHANGE_PW_AND_DECRYPT": "Retry Decrypt",
|
||||
"P1": "Your remote data is encrypted and decryption failed.",
|
||||
"P2_FORGOT_PW": "If you <strong>forgot your password</strong>, enter the correct password and click \"Retry Decrypt\".",
|
||||
"P3_PW_CHANGED": "If you <strong>changed your password on another device</strong>, enter your new password and click \"Overwrite Remote\". This will upload your local data to the server.",
|
||||
"PASSWORD": "Password",
|
||||
"TITLE": "Decryption Failed"
|
||||
"BTN_OVER_WRITE_REMOTE": "Folosește această parolă și suprascrie-o pe cea de la distanță",
|
||||
"CHANGE_PW_AND_DECRYPT": "Reîncearcă decriptarea",
|
||||
"P1": "Datele tale de la distanță sunt criptate și decriptarea a eșuat.",
|
||||
"P2_FORGOT_PW": "Dacă ai <strong>uitat parola</strong>, introdu parola corectă și apasă \"Reîncearcă decriptarea\".",
|
||||
"P3_PW_CHANGED": "Dacă ai <strong>schimbat parola pe un alt dispozitiv</strong>, introdu noua parolă și apasă \"Suprascrie datele de la distanță\". Aceasta va încărca datele locale criptate cu această parolă pe server.",
|
||||
"PASSWORD": "Parolă",
|
||||
"TITLE": "Decriptarea a eșuat"
|
||||
},
|
||||
"D_ENTER_PASSWORD": {
|
||||
"BTN_FORCE_OVERWRITE": "Use Local Data",
|
||||
"BTN_SAVE_AND_SYNC": "Save & Sync",
|
||||
"FORCE_OVERWRITE_CONFIRM": "This will <strong>delete all data on the server</strong> and upload your local data encrypted with this password. Continue?",
|
||||
"FORCE_OVERWRITE_INFO": "If you prefer to keep your local data, you can overwrite the server instead.",
|
||||
"FORCE_OVERWRITE_TITLE": "Overwrite Server Data?",
|
||||
"MESSAGE": "The sync server contains encrypted data, but no encryption password is configured on this device. Please enter your encryption password to decrypt and sync your data.",
|
||||
"PASSWORD_LABEL": "Encryption Password",
|
||||
"TITLE": "Encryption Password Required"
|
||||
"BTN_FORCE_OVERWRITE": "Folosește datele locale",
|
||||
"BTN_SAVE_AND_SYNC": "Salvează și sincronizează",
|
||||
"FORCE_OVERWRITE_CONFIRM": "Acest lucru va <strong>șterge toate datele de pe server</strong> și va încărca datele locale criptate cu această parolă. Continui?",
|
||||
"FORCE_OVERWRITE_INFO": "Dacă preferi să păstrezi datele locale, poți suprascrie serverul în schimb.",
|
||||
"FORCE_OVERWRITE_TITLE": "Suprascrii datele de pe server?",
|
||||
"MESSAGE": "Serverul de sincronizare conține date criptate, dar nu este configurată nicio parolă de criptare pe acest dispozitiv. Te rog să introduci parola de criptare pentru a decripta și sincroniza datele.",
|
||||
"PASSWORD_LABEL": "Parolă de criptare",
|
||||
"TITLE": "Parolă de criptare necesară"
|
||||
},
|
||||
"D_FRESH_CLIENT_CONFIRM": {
|
||||
"MESSAGE": "This appears to be a fresh installation. Remote data with {{count}} changes was found. Do you want to download and overwrite your local data with it?",
|
||||
"OK": "Download Remote Data",
|
||||
"TITLE": "Initial Sync"
|
||||
"MESSAGE": "Aceasta pare să fie o instalare curată. Datele de la distanță cu {{count}} modificări au fost găsite. Dorești să descarci și să înlocuiești datele locale cu ele?",
|
||||
"OK": "Descarcă datele de la distanță",
|
||||
"TITLE": "Sincronizare inițială"
|
||||
},
|
||||
"D_INCOMPLETE_SYNC": {
|
||||
"BTN_CLOSE_APP": "Închide aplicația",
|
||||
|
|
@ -1271,13 +1289,17 @@
|
|||
"SETUP_ENCRYPTION_PASSWORD_WARNING": "Folosește aceeași parolă pe toate dispozitivele. O parolă uitată poate fi resetată prin suprascrierea datelor de la distanță.",
|
||||
"SETUP_ENCRYPTION_TITLE": "SuperSync: Setare parolă"
|
||||
},
|
||||
"NEXTCLOUD": {
|
||||
"L_SERVER_URL": "Nextcloud Server URL",
|
||||
"SERVER_URL_DESCRIPTION": "e.g. https://cloud.example.com or https://example.com/nextcloud",
|
||||
"L_APP_PASSWORD": "App Password",
|
||||
"APP_PASSWORD_DESCRIPTION": "Go to Nextcloud → Settings → Security → App passwords to generate one"
|
||||
},
|
||||
"TITLE": "Sync",
|
||||
"WEB_DAV": {
|
||||
"CONDITIONAL_HEADER_WARNING_MSG": "Your WebDAV server does not support conditional headers (If-Unmodified-Since). This means Super Productivity cannot detect concurrent changes from other devices during upload.<br><br>Sync will still work, but there is a small risk of data conflicts if you make changes on multiple devices simultaneously.",
|
||||
"CONDITIONAL_HEADER_WARNING_TITLE": "Reduced Sync Safety",
|
||||
"CORS_INFO": "<strong>Making it work in a web browser:</strong> Allow Super Productivity to make CORS requests to your WebDAV server. This can have negative security implications! For Nextcloud, please refer to <a href='https://github.com/nextcloud/server/issues/3131'>this GitHub thread</a> for more information. One approach to make this work on mobile is allowing \"https://app.super-productivity.com\" via the Nextcloud app <a href='https://apps.nextcloud.com/apps/webapppassword'>webapppassword<a>. Use at your own risk!</p>",
|
||||
"CORS_INFO": "<strong>Making it work in a web browser:</strong> You need to allow Super Productivity to make CORS requests to your server. For Nextcloud, one approach is allowing \"https://app.super-productivity.com\" via the <a href='https://apps.nextcloud.com/apps/webapppassword'>webapppassword</a> app. Refer to <a href='https://github.com/nextcloud/server/issues/3131'>this GitHub thread</a> for more information.",
|
||||
"D_SYNC_FOLDER_PATH": "Path relative to the WebDAV server root where sync files will be stored (e.g. '/super-productivity' or '/'). This is NOT your server's internal directory path.",
|
||||
"INFO": "<strong>Warning:</strong> WebDAV implementations differ wildly. Super Productivity is known to work well with Nextcloud, <strong>but it may not work with your provider</strong>. Test thoroughly before relying on it.",
|
||||
"INFO": "<strong>Warning:</strong> Generic WebDAV sync is provided <strong>as-is without support</strong>. If you are using Nextcloud, please use the Nextcloud sync option instead.",
|
||||
"L_BASE_URL": "Base URL",
|
||||
"L_PASSWORD": "Password",
|
||||
"L_SYNC_FOLDER_PATH": "Sync Folder Path",
|
||||
|
|
@ -1315,6 +1337,8 @@
|
|||
"ERROR_PERMISSION": "File access denied. Please check your filesystem permissions.",
|
||||
"ERROR_PERMISSION_FLATPAK": "File access denied. Grant filesystem permission via Flatseal or use a path inside ~/.var/app/",
|
||||
"ERROR_PERMISSION_SNAP": "File access denied. Run 'snap connect super-productivity:home' or use a path inside ~/snap/super-productivity/common/",
|
||||
"ERROR_REMOTE_FILE_EMPTY": "Remote sync file is empty. This can happen after an interrupted sync. Overwrite remote with your local data?",
|
||||
"ERROR_REMOTE_FILE_LOCKED": "Remote sync file is locked by the server. This may resolve on the next sync attempt.",
|
||||
"ERROR_UNABLE_TO_READ_REMOTE_DATA": "Error while syncing. Unable to read remote data. Maybe you enabled encryption and your local password does not match the one used to encrypt the remote data?",
|
||||
"FINISH_DAY_SYNC_ERROR": "Sync error while finishing day. Please try again.",
|
||||
"FRESH_CLIENT_SYNC_CANCELLED": "Sincronizare inițială anulată. Datele de la distanță nu au fost aplicate.",
|
||||
|
|
@ -1405,11 +1429,11 @@
|
|||
"TITLE": "Basic Settings"
|
||||
},
|
||||
"S": {
|
||||
"ARCHIVE_CLEANUP_FAILED": "Curățarea datelor arhivate pentru eticheta ștearsă a eșuat",
|
||||
"UPDATED": "Tag settings were updated"
|
||||
"ARCHIVE_CLEANUP_FAILED": "A eșuat curățarea datelor arhivate pentru eticheta ștearsă",
|
||||
"UPDATED": "Setările etichetei au fost actualizate"
|
||||
},
|
||||
"TTL": {
|
||||
"ADD_NEW_TAG": "Add new Tag"
|
||||
"ADD_NEW_TAG": "Adaugă etichetă nouă"
|
||||
}
|
||||
},
|
||||
"TAG_FOLDER": {
|
||||
|
|
@ -1489,47 +1513,47 @@
|
|||
"ADD_SUB_TASK": "Adaugă subsarcină",
|
||||
"ADD_TO_MY_DAY": "Adaugă la lista de azi",
|
||||
"ADD_TO_PROJECT": "Adaugă la proiect",
|
||||
"CONVERT_TO_PARENT_TASK": "Convert to parent task",
|
||||
"DELETE": "Delete task",
|
||||
"DELETE_REPEAT_INSTANCE": "Delete recurring task instance",
|
||||
"DROP_ATTACHMENT": "Drop here to attach to \"{{title}}\"",
|
||||
"DUPLICATE": "Duplicate",
|
||||
"CONVERT_TO_PARENT_TASK": "Convertește în sarcină părinte",
|
||||
"DELETE": "Șterge sarcina",
|
||||
"DELETE_REPEAT_INSTANCE": "Șterge instanța de sarcină recurentă",
|
||||
"DROP_ATTACHMENT": "Plasează aici pentru a atașa la \"{{title}}\"",
|
||||
"DUPLICATE": "Duplică",
|
||||
"EDIT_DEADLINE": "Editează termen limită",
|
||||
"EDIT_SCHEDULED": "Reschedule",
|
||||
"FOCUS_SESSION": "Start focus session",
|
||||
"MARK_DONE": "Mark as completed",
|
||||
"MOVE_TO_BACKLOG": "Move to backlog",
|
||||
"MOVE_TO_OTHER_PROJECT": "Move to project",
|
||||
"MOVE_TO_REGULAR": "Move to regular list",
|
||||
"MOVE_TO_TOP": "Move to top",
|
||||
"OPEN_ATTACH": "Attach file or link",
|
||||
"OPEN_ISSUE": "Open in browser",
|
||||
"OPEN_TIME": "Time tracking",
|
||||
"EDIT_SCHEDULED": "Planifică din nou",
|
||||
"FOCUS_SESSION": "Începe sesiune de concentrare",
|
||||
"MARK_DONE": "Marchează ca finalizată",
|
||||
"MOVE_TO_BACKLOG": "Mută în backlog",
|
||||
"MOVE_TO_OTHER_PROJECT": "Mută în proiect",
|
||||
"MOVE_TO_REGULAR": "Mută în lista obișnuită",
|
||||
"MOVE_TO_TOP": "Mută în partea de sus",
|
||||
"OPEN_ATTACH": "Atașează fișier sau legătură",
|
||||
"OPEN_ISSUE": "Deschide în navigatorul web",
|
||||
"OPEN_TIME": "Contorizare timp",
|
||||
"REMOVE_DEADLINE": "Șterge termen limită",
|
||||
"REMOVE_FROM_MY_DAY": "Elimină din lista de azi",
|
||||
"SCHEDULE": "Schedule task",
|
||||
"SCHEDULE": "Planifică sarcina",
|
||||
"SET_DEADLINE": "Setează termen limită",
|
||||
"TOGGLE_ATTACHMENTS": "Show/hide attachments",
|
||||
"TOGGLE_DETAIL_PANEL": "Show/hide additional info",
|
||||
"TOGGLE_DONE": "Toggle completion status",
|
||||
"TOGGLE_SUB_TASK_VISIBILITY": "Toggle subtask visibility",
|
||||
"TOGGLE_TAGS": "Toggle tags",
|
||||
"TRACK_TIME": "Start tracking time",
|
||||
"TRACK_TIME_STOP": "Pause tracking time",
|
||||
"UNSCHEDULE_TASK": "Unschedule task",
|
||||
"UPDATE_ISSUE_DATA": "Update issue data"
|
||||
"TOGGLE_ATTACHMENTS": "Arată/Ascunde atașamente",
|
||||
"TOGGLE_DETAIL_PANEL": "Arată/Ascunde informații suplimentare",
|
||||
"TOGGLE_DONE": "Comută starea de finalizare",
|
||||
"TOGGLE_SUB_TASK_VISIBILITY": "Comută vizibilitatea subsarcinilor",
|
||||
"TOGGLE_TAGS": "Comută etichetele",
|
||||
"TRACK_TIME": "Începe contorizarea timpului",
|
||||
"TRACK_TIME_STOP": "Pune pe pauză contorizarea timpului",
|
||||
"UNSCHEDULE_TASK": "Șterge planificarea sarcinii",
|
||||
"UPDATE_ISSUE_DATA": "Actualizează datele problemei"
|
||||
},
|
||||
"D_CONFIRM_DELETE": {
|
||||
"MSG": "Dorești să ștergi sarcina \"{{title}}\"?",
|
||||
"OK": "Delete"
|
||||
"OK": "Șterge"
|
||||
},
|
||||
"D_CONFIRM_SHORT_SYNTAX_NEW_TAG": {
|
||||
"MSG": "Do you want to create the new tag {{tagsTxt}}?",
|
||||
"OK": "Create tag"
|
||||
"MSG": "DDorești să creezi eticheta nouă {{tagsTxt}}?",
|
||||
"OK": "Creează etichetă"
|
||||
},
|
||||
"D_CONFIRM_SHORT_SYNTAX_NEW_TAGS": {
|
||||
"MSG": "Do you want to create the new tags {{tagsTxt}}?",
|
||||
"OK": "Create tags"
|
||||
"MSG": "Dorești să creezi etichetele noi {{tagsTxt}}?",
|
||||
"OK": "Creează etichete"
|
||||
},
|
||||
"D_DEADLINE": {
|
||||
"ADD_TIME": "Adaugă ora",
|
||||
|
|
@ -1549,28 +1573,28 @@
|
|||
"SET_DEADLINE": "Setează termen limită"
|
||||
},
|
||||
"D_REMINDER_VIEW": {
|
||||
"ADD_ALL_TO_TODAY": "Add all to Today",
|
||||
"ADD_TO_TODAY": "Add to Today",
|
||||
"ADD_ALL_TO_TODAY": "Adaugă-le pe toate la Astăzi",
|
||||
"ADD_TO_TODAY": "Adaugă la Astăzi",
|
||||
"CLEAR_ALL_REMINDERS": "Șterge toate memento-urile",
|
||||
"CLEAR_REMINDER": "Șterge memento",
|
||||
"COMPLETE": "Complete",
|
||||
"COMPLETE_ALL": "Complete all",
|
||||
"COMPLETE": "Finalizează",
|
||||
"COMPLETE_ALL": "Finalizează-le pe toate",
|
||||
"DEADLINE_REMINDER": "Memento termen limită",
|
||||
"DEADLINE_SECTION": "Termene limită",
|
||||
"DISMISS_ALL_REMINDERS_KEEP_TODAY": "Dismiss All Reminders (Keep in Today)",
|
||||
"DISMISS_REMINDER_KEEP_TODAY": "Dismiss Reminder (Keep in Today)",
|
||||
"DONE": "Done",
|
||||
"DISMISS_ALL_REMINDERS_KEEP_TODAY": "Înlătură toate mementourile (păstrează în Astăzi)",
|
||||
"DISMISS_REMINDER_KEEP_TODAY": "Înlătură memento (păstrează în Astăzi)",
|
||||
"DONE": "Realizat",
|
||||
"DUE_SECTION": "Planificate",
|
||||
"DUE_TASK": "Reminder for planned task",
|
||||
"DUE_TASKS": "Reminder for planned tasks",
|
||||
"RESCHEDULE_EDIT": "Edit (reschedule)",
|
||||
"RESCHEDULE_UNTIL_TOMORROW": "Reschedule for tomorrow",
|
||||
"SNOOZE": "Snooze",
|
||||
"SNOOZE_ALL": "Snooze all",
|
||||
"START": "Start",
|
||||
"TASK_REMINDERS": "Memento-uri sarcini",
|
||||
"UNSCHEDULE": "Unschedule",
|
||||
"UNSCHEDULE_ALL": "Unschedule all"
|
||||
"DUE_TASK": "Memento sarcină planificată",
|
||||
"DUE_TASKS": "Memento sarcini planificate",
|
||||
"RESCHEDULE_EDIT": "Editează (planifică din nou)",
|
||||
"RESCHEDULE_UNTIL_TOMORROW": "Planifică din nou pentru mâine",
|
||||
"SNOOZE": "Amână",
|
||||
"SNOOZE_ALL": "Amână-le pe toate",
|
||||
"START": "Începe",
|
||||
"TASK_REMINDERS": "Mementouri sarcini",
|
||||
"UNSCHEDULE": "Șterge planificarea",
|
||||
"UNSCHEDULE_ALL": "Șterge planificarea tuturor"
|
||||
},
|
||||
"D_SCHEDULE_TASK": {
|
||||
"QA_NEXT_MONTH": "Schedule next month",
|
||||
|
|
@ -1589,9 +1613,9 @@
|
|||
"UNSCHEDULE": "Unschedule"
|
||||
},
|
||||
"D_SELECT_DATE_AND_TIME": {
|
||||
"DATE": "Date",
|
||||
"TIME": "Time",
|
||||
"TITLE": "Select Date and Time"
|
||||
"DATE": "Dată",
|
||||
"TIME": "Oră",
|
||||
"TITLE": "Alege data și ora"
|
||||
},
|
||||
"D_TIME": {
|
||||
"ADD_FOR_OTHER_DAY": "Add time spent for other day",
|
||||
|
|
@ -1671,18 +1695,18 @@
|
|||
"OK": "Remove completely"
|
||||
},
|
||||
"D_CONFIRM_UPDATE_INSTANCES": {
|
||||
"CANCEL": "Only future tasks",
|
||||
"MSG": "There are {{tasksNr}} instances created for this recurring task. Do you want to update all of them with the new defaults or just future tasks?",
|
||||
"OK": "Update all instances"
|
||||
"CANCEL": "Doar sarcinile viitoare",
|
||||
"MSG": "Există {{tasksNr}} instanțe create pentru această sarcină recurentă. Dorești să le actualizezi pe toate cu noile valori implicite sau doar să actualizezi sarcinile viitoare?",
|
||||
"OK": "Actualizează toate instanțele"
|
||||
},
|
||||
"D_SKIP_INSTANCE": {
|
||||
"MSG": "Skip the recurring task on {{date}}? This will prevent the task from being created on this date only.",
|
||||
"MSG": "Omiți sarcina recurentă pe {{date}}? Acest lucru va preveni crearea sarcinii doar pe această dată.",
|
||||
"OK": "Omite"
|
||||
},
|
||||
"D_EDIT": {
|
||||
"ADD": "Add Recurring Task Config",
|
||||
"ADVANCED_CFG": "Advanced configuration",
|
||||
"EDIT": "Edit Recurring Task Config",
|
||||
"ADD": "Adaugă configurație sarcină recurentă",
|
||||
"ADVANCED_CFG": "Configurare avansată",
|
||||
"EDIT": "Editează configurația sarcinii recurente",
|
||||
"HEATMAP_LABEL": "Activitate",
|
||||
"TAG_LABEL": "Etichete",
|
||||
"TIME_SPENT_THIS_MONTH": "Luna aceasta",
|
||||
|
|
@ -1690,105 +1714,105 @@
|
|||
"TIME_SPENT_TOTAL": "Total"
|
||||
},
|
||||
"F": {
|
||||
"C_DAY": "Day",
|
||||
"C_MONTH": "Month",
|
||||
"C_WEEK": "Week",
|
||||
"C_YEAR": "Year",
|
||||
"DEFAULT_ESTIMATE": "Default Estimate",
|
||||
"DISABLE_AUTO_UPDATE_SUBTASKS": "Disable auto-updating subtasks",
|
||||
"DISABLE_AUTO_UPDATE_SUBTASKS_DESCRIPTION": "Do not update inherited subtasks automatically when the newest instance changes",
|
||||
"FRIDAY": "Friday",
|
||||
"INHERIT_SUBTASKS": "Inherit subtasks",
|
||||
"INHERIT_SUBTASKS_DESCRIPTION": "When enabled, subtasks from the most recent task instance will be recreated with the recurring task",
|
||||
"MONDAY": "Monday",
|
||||
"NOTES": "Default notes",
|
||||
"Q_CUSTOM": "Custom recurring config",
|
||||
"Q_DAILY": "Every day",
|
||||
"Q_MONDAY_TO_FRIDAY": "Every Monday through Friday",
|
||||
"Q_MONTHLY_CURRENT_DATE": "Every month on the day {{dateDayStr}}",
|
||||
"C_DAY": "Zi",
|
||||
"C_MONTH": "Lună",
|
||||
"C_WEEK": "Săptămână",
|
||||
"C_YEAR": "An",
|
||||
"DEFAULT_ESTIMATE": "Estimare implicită",
|
||||
"DISABLE_AUTO_UPDATE_SUBTASKS": "Dezactivează auto-actualizarea subsarcinilor",
|
||||
"DISABLE_AUTO_UPDATE_SUBTASKS_DESCRIPTION": "Nu actualiza automat subsarcinile moștenate când instanța cea mai nouă se modifică",
|
||||
"FRIDAY": "vineri",
|
||||
"INHERIT_SUBTASKS": "Moștenește subsarcini",
|
||||
"INHERIT_SUBTASKS_DESCRIPTION": "Când este activat, subsarcinile din instanța cea mai recentă a sarcinii vor fi recreate cu sarcina recurentă",
|
||||
"MONDAY": "luni",
|
||||
"NOTES": "Notițe implicite",
|
||||
"Q_CUSTOM": "Configurare recurentă personalizată",
|
||||
"Q_DAILY": "În fiecare zi",
|
||||
"Q_MONDAY_TO_FRIDAY": "În fiecare zi de luni până vineri",
|
||||
"Q_MONTHLY_CURRENT_DATE": "În fiecare lună în ziua {{dateDayStr}}",
|
||||
"Q_MONTHLY_FIRST_DAY": "În fiecare lună în prima zi",
|
||||
"Q_MONTHLY_LAST_DAY": "În fiecare lună în ultima zi",
|
||||
"Q_WEEKLY_CURRENT_WEEKDAY": "Every week on {{weekdayStr}}",
|
||||
"Q_YEARLY_CURRENT_DATE": "Every year on the {{dayAndMonthStr}}",
|
||||
"QUICK_SETTING": "Recurring Config",
|
||||
"REMIND_AT": "Remind at",
|
||||
"REMIND_AT_PLACEHOLDER": "Select when to remind",
|
||||
"SKIP_FOR_DATE": "Skip for {{date}}",
|
||||
"SKIP_INSTANCE": "Skip for today",
|
||||
"REPEAT_CYCLE": "Recur cycle",
|
||||
"REPEAT_EVERY": "Recur every",
|
||||
"SATURDAY": "Saturday",
|
||||
"SCHEDULE_TYPE_LABEL": "Schedule type",
|
||||
"Q_WEEKLY_CURRENT_WEEKDAY": "În fiecare săptămână {{weekdayStr}}",
|
||||
"Q_YEARLY_CURRENT_DATE": "În fiecare an pe {{dayAndMonthStr}}",
|
||||
"QUICK_SETTING": "Configurație recurentă",
|
||||
"REMIND_AT": "Amintește la",
|
||||
"REMIND_AT_PLACEHOLDER": "Selectează când să fie amintit",
|
||||
"SKIP_FOR_DATE": "Omite pentru {{date}}",
|
||||
"SKIP_INSTANCE": "Omite pentru astăzi",
|
||||
"REPEAT_CYCLE": "Ciclu de repetare",
|
||||
"REPEAT_EVERY": "Repetă la fiecare",
|
||||
"SATURDAY": "sâmbătă",
|
||||
"SCHEDULE_TYPE_LABEL": "Tip programare",
|
||||
"SKIP_OVERDUE": "Omite instanțele întârziate",
|
||||
"SKIP_OVERDUE_DESCRIPTION": "Când este activat, dacă aplicația este deschisă după ce a fost omisă o apariție programată (de ex. o sarcină săptămânală în care azi nu este o zi programată), instanța ratată va fi omisă silențios în loc să fie creată ca sarcină întârziată",
|
||||
"START_DATE": "Start date",
|
||||
"START_TIME": "Scheduled start time",
|
||||
"START_TIME_DESCRIPTION": "E.g. 15:00. Leave blank for an all day task",
|
||||
"SUNDAY": "Sunday",
|
||||
"THURSDAY": "Thursday",
|
||||
"TITLE": "Task title",
|
||||
"TUESDAY": "Tuesday",
|
||||
"WEDNESDAY": "Wednesday"
|
||||
"START_DATE": "Dată de început",
|
||||
"START_TIME": "Timpul programat de început",
|
||||
"START_TIME_DESCRIPTION": "De ex. 15:00. Lasă necompletat pentru o sarcină pe toată ziua",
|
||||
"SUNDAY": "duminică",
|
||||
"THURSDAY": "joi",
|
||||
"TITLE": "Nume sarcină",
|
||||
"TUESDAY": "marți",
|
||||
"WEDNESDAY": "miercuri"
|
||||
},
|
||||
"SNACK_REPEAT_DIALOG_FAIL": "Nu s-a putut deschide dialogul de configurare repetare. Puteți configura din meniul sarcinii."
|
||||
},
|
||||
"TASK_VIEW": {
|
||||
"CUSTOMIZER": {
|
||||
"DEADLINE_NEXT_MONTH": "Due Next Month",
|
||||
"DEADLINE_NEXT_WEEK": "Due Next Week",
|
||||
"DEADLINE_THIS_MONTH": "Due This Month",
|
||||
"DEADLINE_THIS_WEEK": "Due This Week",
|
||||
"DEADLINE_TODAY": "Due Today",
|
||||
"DEADLINE_TOMORROW": "Due Tomorrow",
|
||||
"ENTER_PROJECT": "Filter Projects",
|
||||
"ENTER_TAG": "Filter Tag",
|
||||
"ESTIMATED_TIME": "Estimated Time",
|
||||
"FILTER_BY": "Filter By",
|
||||
"FILTER_DEADLINE": "Deadline",
|
||||
"FILTER_DEFAULT": "No Filter",
|
||||
"FILTER_ESTIMATED_TIME": "Estimated Time",
|
||||
"FILTER_NOT_SPECIFIED": "Not specified",
|
||||
"FILTER_PROJECT": "Project",
|
||||
"FILTER_SCHEDULED_DATE": "Scheduled Date",
|
||||
"FILTER_TAG": "Tag",
|
||||
"FILTER_TIME_SPENT": "Time Spent",
|
||||
"GROUP_BY": "Group By",
|
||||
"GROUP_DEADLINE": "Deadline",
|
||||
"GROUP_DEADLINE_NONE": "No deadline",
|
||||
"GROUP_DEFAULT": "No Group",
|
||||
"GROUP_PROJECT": "Project",
|
||||
"GROUP_SCHEDULED_DATE": "Scheduled Date",
|
||||
"GROUP_TAG": "Tag",
|
||||
"RESET_ALL": "Reset All",
|
||||
"DEADLINE_NEXT_MONTH": "Scadente luna viitoare",
|
||||
"DEADLINE_NEXT_WEEK": "Scadente săptămâna viitoare",
|
||||
"DEADLINE_THIS_MONTH": "Scadente luna aceasta",
|
||||
"DEADLINE_THIS_WEEK": "Scadente săptămâna aceasta",
|
||||
"DEADLINE_TODAY": "Scadente astăzi",
|
||||
"DEADLINE_TOMORROW": "Scadente mâine",
|
||||
"ENTER_PROJECT": "Filtrează proiecte",
|
||||
"ENTER_TAG": "Filtrează etichete",
|
||||
"ESTIMATED_TIME": "Timp estimat",
|
||||
"FILTER_BY": "Filtrează după",
|
||||
"FILTER_DEADLINE": "Scadență",
|
||||
"FILTER_DEFAULT": "Fără filtru",
|
||||
"FILTER_ESTIMATED_TIME": "Timp estimat",
|
||||
"FILTER_NOT_SPECIFIED": "Nu este specificat",
|
||||
"FILTER_PROJECT": "Proiect",
|
||||
"FILTER_SCHEDULED_DATE": "Dată programată",
|
||||
"FILTER_TAG": "Etichetă",
|
||||
"FILTER_TIME_SPENT": "Timp petrecut",
|
||||
"GROUP_BY": "Grupare după",
|
||||
"GROUP_DEADLINE": "Scadență",
|
||||
"GROUP_DEADLINE_NONE": "Fără scadență",
|
||||
"GROUP_DEFAULT": "Fără grup",
|
||||
"GROUP_PROJECT": "Proiect",
|
||||
"GROUP_SCHEDULED_DATE": "Dată programată",
|
||||
"GROUP_TAG": "Etichetă",
|
||||
"RESET_ALL": "Resetează tot",
|
||||
"SAVE_SORT": "Salvează ca implicit",
|
||||
"SCHEDULED_NEXT_MONTH": "Next Month",
|
||||
"SCHEDULED_NEXT_WEEK": "Next Week",
|
||||
"SCHEDULED_THIS_MONTH": "This Month",
|
||||
"SCHEDULED_THIS_WEEK": "This Week",
|
||||
"SCHEDULED_NEXT_MONTH": "Luna viitoare",
|
||||
"SCHEDULED_NEXT_WEEK": "Săptămâna viitoare",
|
||||
"SCHEDULED_THIS_MONTH": "Luna aceasta",
|
||||
"SCHEDULED_THIS_WEEK": "Săptămâna aceasta",
|
||||
"SCHEDULED_TODAY": "Astăzi",
|
||||
"SCHEDULED_TOMORROW": "Mâine",
|
||||
"SORT": "Sortare",
|
||||
"SORT_CREATION_DATE": "Creation Date",
|
||||
"SORT_DEADLINE": "Deadline",
|
||||
"SORT_DEFAULT": "Default",
|
||||
"SORT_NAME": "Name",
|
||||
"SORT_SCHEDULED_DATE": "Scheduled Date",
|
||||
"TIME_1HOUR": "> 1 Hour",
|
||||
"TIME_2HOUR": "> 2 Hours",
|
||||
"TIME_10MIN": "> 10 Minutes",
|
||||
"TIME_30MIN": "> 30 Minutes",
|
||||
"TIME_SPENT": "Time Spent"
|
||||
"SORT_CREATION_DATE": "Dată de creare",
|
||||
"SORT_DEADLINE": "Scadență",
|
||||
"SORT_DEFAULT": "Implicit",
|
||||
"SORT_NAME": "Nume",
|
||||
"SORT_SCHEDULED_DATE": "Dată programată",
|
||||
"TIME_1HOUR": "> 1 oră",
|
||||
"TIME_2HOUR": "> 2 ore",
|
||||
"TIME_10MIN": "> 10 minute",
|
||||
"TIME_30MIN": "> 30 minute",
|
||||
"TIME_SPENT": "Timp petrecut"
|
||||
}
|
||||
},
|
||||
"TIME_TRACKING": {
|
||||
"B": {
|
||||
"ALREADY_DID": "I already did",
|
||||
"SNOOZE": "Snooze {{time}}"
|
||||
"ALREADY_DID": "Deja am făcut",
|
||||
"SNOOZE": "Amână {{time}}"
|
||||
},
|
||||
"B_TTR": {
|
||||
"ADD_TO_TASK": "Add to Task",
|
||||
"MSG": "You have not been tracking time for {{time}}",
|
||||
"MSG_WITHOUT_TIME": "You have not been tracking time"
|
||||
"ADD_TO_TASK": "Adaugă la sarcină",
|
||||
"MSG": "Nu ai ținut evidența timpului pentru {{time}}",
|
||||
"MSG_WITHOUT_TIME": "Nu ai ținut evidența timpului"
|
||||
},
|
||||
"D_ARCHIVE_COMPRESS": {
|
||||
"BTN_COMPRESS": "Comprimă arhiva",
|
||||
|
|
@ -1928,10 +1952,10 @@
|
|||
"DO_IT": "Fă-l!",
|
||||
"DONT_SHOW_AGAIN": "Nu mai afișa din nou",
|
||||
"DUPLICATE": "Duplică",
|
||||
"DURATION_DESCRIPTION": "ex: \"5h 23m\" care rezultă în 5 ore și 23 minute",
|
||||
"DURATION_DESCRIPTION": "d.e. \"5h 23m\" care rezultă în 5 ore și 23 minute",
|
||||
"EDIT": "Editează",
|
||||
"ENABLED": "Activat",
|
||||
"EXAMPLE_VAL": "ex: 32m",
|
||||
"EXAMPLE_VAL": "d.e. 32m",
|
||||
"EXTENSION_INFO": "Te rog <a target=\"_blank\" href=\"https://chrome.google.com/webstore/detail/super-productivity/ljkbjodfmekklcoibdnhahlaalhihmlb\"> descarcă extensia Chrome</a> pentru a permite comunicarea cu API-ul Jira și tratarea timpului mort. Acest lucru nu funcționează pe mobil. <strong>Fără extensie, aceste funcții nu vor funcționa!</strong>",
|
||||
"HIDE": "Ascunde",
|
||||
"ICON_INP_DESCRIPTION": "Toate emoji-urile UTF-8 sunt de asemenea suportate!",
|
||||
|
|
@ -1942,7 +1966,7 @@
|
|||
"NEXT": "Următorul",
|
||||
"NO_CON": "Momentan ești offline. Te rog reconectează-te la internet.",
|
||||
"NONE": "Niciunul",
|
||||
"OK": "Ok",
|
||||
"OK": "OK",
|
||||
"OPEN_IN_BROWSER": "Deschide în navigatorul web",
|
||||
"OVERDUE": "Restante",
|
||||
"PASSWORD_STRENGTH_FAIR": "Acceptabilă",
|
||||
|
|
@ -1962,25 +1986,27 @@
|
|||
},
|
||||
"GCF": {
|
||||
"APP_FEATURES": {
|
||||
"BOARDS": "Boards",
|
||||
"DONATE_PAGE": "Donate page",
|
||||
"BOARDS": "Table",
|
||||
"DONATE_PAGE": "Pagină de donații",
|
||||
"FINISH_DAY": "Încheie ziua",
|
||||
"FOCUS_MODE": "Focus mode",
|
||||
"FOCUS_MODE": "Mod concentrare",
|
||||
"HABITS": "Obiceiuri",
|
||||
"HELP": "Enable or disable specific app features across the UI.",
|
||||
"ISSUES_PANEL": "Issues panel",
|
||||
"PLANNER": "Planner",
|
||||
"PROJECT_NOTES": "Project notes",
|
||||
"HELP": "Activează sau dezactivează anumite funcții ale aplicației din interfață.",
|
||||
"ISSUES_PANEL": "Panou probleme",
|
||||
"PLANNER": "Planificator",
|
||||
"PROJECT_NOTES": "Notițe proiect",
|
||||
"SCHEDULE": "Orar",
|
||||
"SCHEDULE_DAY_PANEL": "Schedule day panel",
|
||||
"SCHEDULE_DAY_PANEL": "Panou programare zi",
|
||||
"SEARCH": "Caută",
|
||||
"SYNC_BUTTON": "Sync button",
|
||||
"TIME_TRACKING": "Stopwatch time tracking",
|
||||
"TITLE": "App Features",
|
||||
"USER_PROFILES": "User profiles (experimental)",
|
||||
"USER_PROFILES_HINT": "Allows you to create and switch between different user profiles, each with separate settings, tasks, and sync configurations. The profile management button will appear in the top-right corner when enabled. Note: Disabling this feature will hide the UI but preserve your profile data. (Experimental feature. No guarantees. Make sure to have a backup!)",
|
||||
"USER_PROFILES_WARNING": "<strong>Funcție experimentală:</strong> Această funcție este încă în dezvoltare și poate fi eliminată sau modificată semnificativ într-o actualizare viitoare. Folosește pe propriul risc și asigură-te că ai backup-uri ale datelor."
|
||||
},
|
||||
"SYNC_BUTTON": "Buton sincronizare",
|
||||
"TIME_TRACKING": "Cronometru urmărire timp",
|
||||
"TITLE": "Funcții aplicație",
|
||||
"USER_PROFILES": "Profiluri utilizator (experimental — ar putea fi eliminate)",
|
||||
"USER_PROFILES_HINT": "Îți permite să creezi și să comuti între diferite profiluri de utilizator, fiecare cu setări, sarcini și configurații de sincronizare separate. Butonul de gestionare a profilurilor va apărea în colțul din dreapta sus când este activat. Notă: Dezactivarea acestei funcții va ascunde interfața dar va păstra datele profilului. (Funcție experimentală. Fără garanții. Asigură-te că ai o copie de rezervă!)",
|
||||
"USER_PROFILES_WARNING": "<strong>Funcție experimentală:</strong> Această funcție este încă în dezvoltare și ar putea fi eliminată sau modificată semnificativ într-o actualizare viitoare. Folosește cu grijă și asigură-te că ai copii de rezervă ale datelor tale.",
|
||||
"EXPERIMENTAL_WARNING_TITLE": "Feature Experimental",
|
||||
"EXPERIMENTAL_WARNING_MSG": "Această funcție este experimentală. Poate conține erori, poate fi modificată semnificativ sau poate fi eliminată într-o actualizare viitoare. Asigură-te că ai copii de rezervă ale datelor tale.<br/><br/>Vrei să o activezi?"
|
||||
},
|
||||
"AUTO_BACKUPS": {
|
||||
"HELP": "Automatically save all data to your app folder to have it ready if something goes wrong.",
|
||||
"LABEL_IS_ENABLED": "Enable automatic backups",
|
||||
|
|
@ -1995,8 +2021,8 @@
|
|||
"SHOW_BANNER_THRESHOLD": "Show a notification X before the event (blank for disabled)"
|
||||
},
|
||||
"CLIPBOARD_IMAGES": {
|
||||
"DELETE": "Delete image",
|
||||
"DELETE_ALL": "Delete All",
|
||||
"DELETE": "Șterge imaginea",
|
||||
"DELETE_ALL": "Șterge-le pe toate",
|
||||
"DELETE_ALL_SUCCESS": "Toate imaginile din clipboard șterse cu succes",
|
||||
"DELETE_SUCCESS": "Imagine din clipboard ștearsă cu succes",
|
||||
"ERROR_DELETING": "Ștergerea imaginii din clipboard a eșuat",
|
||||
|
|
@ -2013,14 +2039,14 @@
|
|||
"PATH_LABEL": "Cale stocare imagini",
|
||||
"PATH_PLACEHOLDER": "Lasă gol pentru locația implicită",
|
||||
"PATH_TITLE": "Locație stocare",
|
||||
"SELECT_PATH": "Select Folder",
|
||||
"SELECT_PATH": "Selectează dosarul",
|
||||
"SELECT_PATH_TITLE": "Selectează dosarul pentru imagini clipboard",
|
||||
"SORT_BY": "Sort by",
|
||||
"SORT_LARGEST": "Largest first",
|
||||
"SORT_NEWEST": "Newest first",
|
||||
"SORT_OLDEST": "Oldest first",
|
||||
"SORT_SMALLEST": "Smallest first",
|
||||
"TITLE": "Imagini clipboard",
|
||||
"SORT_BY": "Sortează după",
|
||||
"SORT_LARGEST": "Cele mai mari primele",
|
||||
"SORT_NEWEST": "Cele mai noi primele",
|
||||
"SORT_OLDEST": "Cele mai vechi primele",
|
||||
"SORT_SMALLEST": "Cele mai mici primele",
|
||||
"TITLE": "Imagini Clipboard",
|
||||
"TOTAL_IMAGES": "Total imagini",
|
||||
"TOTAL_SIZE": "Dimensiune totală"
|
||||
},
|
||||
|
|
@ -2149,6 +2175,7 @@
|
|||
"TIME_LOCALE_IT_IT": "Italienesc: 24 de ore, DD/MM/YYYY",
|
||||
"TIME_LOCALE_JA_JP": "Japonez: 24 de ore, YYYY/MM/DD",
|
||||
"TIME_LOCALE_KO_KR": "Korean: 12 ore AM/PM, YYYY. MM. DD",
|
||||
"TIME_LOCALE_PL_PL": "Polonez: 24 de ore, DD.MM.YYYY",
|
||||
"TIME_LOCALE_PT_BR": "Portughez (Brazilia): 24 de ore, DD/MM/YYYY",
|
||||
"TIME_LOCALE_RO_MD": "Românesc (Moldova): 24 de ore, DD.MM.YYYY",
|
||||
"TIME_LOCALE_RO_RO": "Românesc: 24 de ore, DD.MM.YYYY",
|
||||
|
|
@ -2218,10 +2245,10 @@
|
|||
"L_LUNCH_BREAK_START": "Lunch break start",
|
||||
"L_WORK_END": "Work Day End",
|
||||
"L_WORK_START": "Work Day Start",
|
||||
"LUNCH_BREAK_START_END_DESCRIPTION": "ex: 13:00",
|
||||
"LUNCH_BREAK_START_END_DESCRIPTION": "d.e. 13:00",
|
||||
"MONTH": "Lună",
|
||||
"TITLE": "Programare",
|
||||
"WORK_START_END_DESCRIPTION": "ex: 17:00"
|
||||
"WORK_START_END_DESCRIPTION": "d.e. 17:00"
|
||||
},
|
||||
"SHORT_SYNTAX": {
|
||||
"HELP": "<p>Here you can control short syntax options when creating a task</p>",
|
||||
|
|
@ -2235,9 +2262,9 @@
|
|||
"URL_BEHAVIOR_LABEL": "URL behavior when adding tasks"
|
||||
},
|
||||
"SOUND": {
|
||||
"BREAK_REMINDER_SOUND": "Take a break reminder sound",
|
||||
"DONE_SOUND": "Task completed sound",
|
||||
"IS_INCREASE_DONE_PITCH": "Increase pitch for every task completed",
|
||||
"BREAK_REMINDER_SOUND": "Sunet memento de luat o pauză",
|
||||
"DONE_SOUND": "Sunet sarcină finalizată",
|
||||
"IS_INCREASE_DONE_PITCH": "Crește tonul pentru fiecare sarcină finalizată",
|
||||
"TITLE": "Sunet",
|
||||
"TRACK_TIME_SOUND": "Sunet memento de urmărire a timpului",
|
||||
"VOLUME": "Volum"
|
||||
|
|
@ -2646,18 +2673,19 @@
|
|||
"CREATE_NEW_PROFILE": "Creează un nou profil",
|
||||
"CREATED": "Creat:",
|
||||
"DELETE_PROFILE": "Șterge profilul",
|
||||
"DEPRECATION_WARNING": "Funcția experimentală Profile utilizator va fi eliminată într-o actualizare viitoare. Te rog să îți exporti datele de profil ca backup.",
|
||||
"DIALOG_TITLE": "Administrează profilurile de utilizator",
|
||||
"EXISTING_PROFILES": "Profiluri existente",
|
||||
"EXPORT_PROFILE": "Exportă profilul",
|
||||
"MANAGE_PROFILES": "Gestionează profilurile...",
|
||||
"PROFILE_NAME": "Nume profil",
|
||||
"PROFILE_NAME_PLACEHOLDER": "ex: Muncă, Personal",
|
||||
"PROFILE_NAME_PLACEHOLDER": "d.e. Muncă, Personal",
|
||||
"RENAME": "Redenumește",
|
||||
"SAVE": "Salvează"
|
||||
},
|
||||
"V": {
|
||||
"E_DATETIME": "Valoare introdusă nu este o dată și oră!",
|
||||
"E_DURATION": "Te rog introdu o durată validă (ex: 1h)",
|
||||
"E_DURATION": "Te rog introdu o durată validă (d.e. 1h)",
|
||||
"E_MAX": "Nu trebuie să fie mai mare decât {{val}}",
|
||||
"E_MAX_LENGTH": "Trebuie să aibă cel mult {{val}} caractere",
|
||||
"E_MIN": "Nu trebuie să fie mai mică decât {{val}}",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue