├── logs └── .empty ├── config ├── Seeds │ └── empty ├── features.php ├── plugins.php ├── Migrations │ ├── 20210316024149_UserSpecificProjectSlug.php │ ├── 20210226033714_RenameTasksProjectSectionId.php │ ├── 20241022042500_AddContentToFeedItems.php │ ├── 20210915012937_ConvertCalendarItemsHtmlLinkToText.php │ ├── 20241126033547_AddFaviconToFeeds.php │ ├── 20221030031829_AddDeletedToTasks.php │ ├── 20241103040408_AddExpandedToFeedCategoriesTable.php │ ├── 20241110175742_AddAuthorToFeedItems.php │ ├── 20210215044003_AddEveningToTasks.php │ ├── 20210825015018_AddExpirationToCalendarSubscription.php │ ├── 20240518035817_AddResourceIdToCalendarSubscriptions.php │ ├── 20241112035515_AddDefaultAliasToFeedsTable.php │ ├── 20210322024423_AddThemeToUser.php │ ├── 20210728025420_AddDisplayNameToCalendarProvider.php │ ├── 20240509041300_AddSyncedToCalendarSources.php │ ├── 20210228192141_FixActionOnTasksSectionId.php │ ├── 20241007030938_ExpandFeedItems.php │ ├── 20241207223935_AddUnreadItemCountToFeedSubscriptions.php │ └── 20220530022319_CreateApiTokens.php ├── schema │ ├── i18n.sql │ └── sessions.sql └── bootstrap_cli.php ├── webroot ├── js │ └── .gitkeep ├── favicon.ico ├── favicon.png ├── img │ ├── cake-logo.png │ ├── cake.icon.png │ ├── cake.power.gif │ ├── docket-logo.png │ ├── docket-logo.svg │ └── docket-logo-translucent.svg ├── font │ ├── cakedingbats-webfont.eot │ ├── cakedingbats-webfont.ttf │ ├── cakedingbats-webfont.woff │ └── cakedingbats-webfont.woff2 └── .htaccess ├── src ├── Model │ ├── Behavior │ │ └── .gitkeep │ └── Entity │ │ ├── SavedFeedItem.php │ │ └── ApiToken.php ├── Policy │ ├── ApiTokensTablePolicy.php │ ├── UserPolicy.php │ └── ApiTokenPolicy.php ├── Middleware │ └── ApiCsrfProtectionMiddleware.php ├── Identifier │ └── ApiTokenIdentifier.php └── View │ └── AppView.php ├── .yarnrc.yml ├── tests ├── js │ ├── __mocks__ │ │ └── styleMock.js │ ├── setup.ts │ └── fixtures.ts ├── Fixture │ ├── ApiTokensFixture.php │ ├── LabelsTasksFixture.php │ ├── google-auth.json │ └── vcr │ │ ├── controller_calendarsources_delete.yml │ │ ├── googleoauth_callback_invalid.yml │ │ └── controller_calendarsources_add_auth_fail.yml └── Acceptance │ └── LoginTest.php ├── flutterapp ├── ios │ ├── Runner │ │ ├── Runner-Bridging-Header.h │ │ ├── Assets.xcassets │ │ │ ├── LaunchImage.imageset │ │ │ │ ├── LaunchImage.png │ │ │ │ ├── LaunchImage@2x.png │ │ │ │ ├── LaunchImage@3x.png │ │ │ │ ├── README.md │ │ │ │ └── Contents.json │ │ │ ├── AppIcon.appiconset │ │ │ │ ├── Icon-App-20x20@1x.png │ │ │ │ ├── Icon-App-20x20@2x.png │ │ │ │ ├── Icon-App-20x20@3x.png │ │ │ │ ├── Icon-App-29x29@1x.png │ │ │ │ ├── Icon-App-29x29@2x.png │ │ │ │ ├── Icon-App-29x29@3x.png │ │ │ │ ├── Icon-App-40x40@1x.png │ │ │ │ ├── Icon-App-40x40@2x.png │ │ │ │ ├── Icon-App-40x40@3x.png │ │ │ │ ├── Icon-App-50x50@1x.png │ │ │ │ ├── Icon-App-50x50@2x.png │ │ │ │ ├── Icon-App-57x57@1x.png │ │ │ │ ├── Icon-App-57x57@2x.png │ │ │ │ ├── Icon-App-60x60@2x.png │ │ │ │ ├── Icon-App-60x60@3x.png │ │ │ │ ├── Icon-App-72x72@1x.png │ │ │ │ ├── Icon-App-72x72@2x.png │ │ │ │ ├── Icon-App-76x76@1x.png │ │ │ │ ├── Icon-App-76x76@2x.png │ │ │ │ ├── Icon-App-1024x1024@1x.png │ │ │ │ └── Icon-App-83.5x83.5@2x.png │ │ │ └── LaunchBackground.imageset │ │ │ │ ├── background.png │ │ │ │ ├── darkbackground.png │ │ │ │ └── Contents.json │ │ └── AppDelegate.swift │ ├── Flutter │ │ ├── Debug.xcconfig │ │ ├── Release.xcconfig │ │ └── AppFrameworkInfo.plist │ ├── Runner.xcodeproj │ │ └── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ ├── WorkspaceSettings.xcsettings │ │ │ └── IDEWorkspaceChecks.plist │ ├── Runner.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── WorkspaceSettings.xcsettings │ │ │ └── IDEWorkspaceChecks.plist │ └── .gitignore ├── assets │ ├── splash.png │ ├── docket-logo.png │ └── google-calendar.png ├── android │ ├── gradle.properties │ ├── app │ │ └── src │ │ │ ├── main │ │ │ ├── res │ │ │ │ ├── drawable-hdpi │ │ │ │ │ ├── splash.png │ │ │ │ │ └── ic_launcher_foreground.png │ │ │ │ ├── drawable-mdpi │ │ │ │ │ ├── splash.png │ │ │ │ │ └── ic_launcher_foreground.png │ │ │ │ ├── drawable-xhdpi │ │ │ │ │ ├── splash.png │ │ │ │ │ └── ic_launcher_foreground.png │ │ │ │ ├── drawable │ │ │ │ │ ├── background.png │ │ │ │ │ └── launch_background.xml │ │ │ │ ├── drawable-v21 │ │ │ │ │ ├── background.png │ │ │ │ │ └── launch_background.xml │ │ │ │ ├── drawable-xxhdpi │ │ │ │ │ ├── splash.png │ │ │ │ │ └── ic_launcher_foreground.png │ │ │ │ ├── drawable-xxxhdpi │ │ │ │ │ ├── splash.png │ │ │ │ │ └── ic_launcher_foreground.png │ │ │ │ ├── mipmap-hdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-mdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── drawable-night │ │ │ │ │ ├── background.png │ │ │ │ │ └── launch_background.xml │ │ │ │ ├── mipmap-xhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── values │ │ │ │ │ └── colors.xml │ │ │ │ ├── drawable-night-v21 │ │ │ │ │ ├── background.png │ │ │ │ │ └── launch_background.xml │ │ │ │ └── mipmap-anydpi-v26 │ │ │ │ │ └── ic_launcher.xml │ │ │ ├── kotlin │ │ │ │ └── com │ │ │ │ │ └── docket │ │ │ │ │ └── flutterapp │ │ │ │ │ └── MainActivity.kt │ │ │ └── java │ │ │ │ └── io │ │ │ │ └── flutter │ │ │ │ └── app │ │ │ │ └── FlutterMultiDexApplication.java │ │ │ ├── debug │ │ │ └── AndroidManifest.xml │ │ │ └── profile │ │ │ └── AndroidManifest.xml │ ├── gradle │ │ └── wrapper │ │ │ └── gradle-wrapper.properties │ ├── .gitignore │ ├── settings.gradle │ └── build.gradle ├── test_resources │ ├── login.json │ ├── subtask_update.json │ ├── profile.json │ ├── calendar_source.json │ ├── task_create_today.json │ ├── task_details.json │ ├── project_list.json │ ├── tasks_today.json │ ├── tasks_upcoming.json │ ├── project_completed.json │ └── project_details.json ├── lib │ ├── screens │ │ └── unknown.dart │ ├── models │ │ └── apitoken.dart │ ├── components │ │ ├── loadingindicator.dart │ │ ├── taskgroup.dart │ │ ├── projectbadge.dart │ │ ├── taskaddbutton.dart │ │ ├── iconsnackbar.dart │ │ └── floatingcreatetaskbutton.dart │ ├── viewmodels │ │ ├── taskform.dart │ │ └── login.dart │ ├── db │ │ ├── profile.dart │ │ ├── trashbin.dart │ │ └── projectarchive.dart │ └── dialogs │ │ └── confirmdelete.dart ├── .gitignore ├── test │ └── theme_test.dart └── .metadata ├── assets ├── sass │ ├── components │ │ ├── noProjects.scss │ │ ├── sectionQuickForm.scss │ │ ├── feedSubscriptions.scss │ │ ├── projectBadge.scss │ │ ├── confirm.scss │ │ ├── taskGroup.scss │ │ ├── dueOn.scss │ │ ├── sectionContainer.scss │ │ ├── profileMenu.scss │ │ ├── markdownText.scss │ │ ├── paginator.scss │ │ ├── calendarItemList.scss │ │ ├── flashMessage.scss │ │ ├── modal.scss │ │ ├── checkbox.scss │ │ ├── icons.scss │ │ └── projectItem.scss │ ├── pages │ │ ├── calendars │ │ │ └── add.scss │ │ ├── feeds │ │ │ ├── viewitem.scss │ │ │ └── discover.scss │ │ ├── projects │ │ │ └── view.scss │ │ └── tasks │ │ │ └── add.scss │ └── utils.scss └── js │ ├── types.tsx │ ├── extensions │ ├── ajax.ts │ ├── projectSorter.ts │ ├── removeRow.ts │ └── flashMessage.ts │ ├── locale.ts │ ├── webcomponents │ ├── sideBar.ts │ └── reloadAfter.ts │ └── app.tsx ├── docker ├── run.sh └── nginx.conf ├── .dockerignore ├── plugins ├── Feeds │ ├── .gitignore │ ├── templates │ │ ├── FeedCategories │ │ │ ├── reorder.php │ │ │ ├── toggle_expanded.php │ │ │ ├── delete_confirm.php │ │ │ ├── edit.php │ │ │ └── add.php │ │ └── FeedSubscriptions │ │ │ ├── delete_confirm.php │ │ │ ├── add.php │ │ │ └── edit.php │ ├── src │ │ ├── Service │ │ │ └── FeedSyncException.php │ │ ├── Policy │ │ │ ├── FeedCategoriesTablePolicy.php │ │ │ ├── FeedSubscriptionsTablePolicy.php │ │ │ └── FeedItemsTablePolicy.php │ │ ├── FeedsPlugin.php │ │ ├── View │ │ │ └── Cell │ │ │ │ └── FeedCategoryMenuCell.php │ │ └── Model │ │ │ └── Entity │ │ │ └── FeedItemUser.php │ └── tests │ │ └── Fixture │ │ ├── FeedsFixture.php │ │ ├── FeedItemsFixture.php │ │ ├── FeedCategoriesFixture.php │ │ ├── FeedItemUsersFixture.php │ │ ├── SavedFeedItemsFixture.php │ │ ├── FeedSubscriptionsFixture.php │ │ └── FeedSubscriptionsFeedItemsFixture.php ├── Tasks │ ├── .gitignore │ ├── templates │ │ ├── Projects │ │ │ ├── reorder.php │ │ │ ├── delete_confirm.php │ │ │ ├── archived.php │ │ │ ├── add.php │ │ │ ├── edit.php │ │ │ └── completed.php │ │ ├── Tasks │ │ │ ├── delete_ok.php │ │ │ ├── reschedule.php │ │ │ ├── delete_confirm.php │ │ │ ├── deleted.php │ │ │ ├── view.php │ │ │ └── add.php │ │ ├── ProjectSections │ │ │ ├── view.php │ │ │ ├── delete_confirm.php │ │ │ ├── add.php │ │ │ └── options.php │ │ ├── element │ │ │ ├── task_body.php │ │ │ ├── task_checkbox.php │ │ │ ├── tasks_empty.php │ │ │ ├── project_item.php │ │ │ └── task_due_on.php │ │ └── cell │ │ │ └── ProjectsMenu │ │ │ └── display.php │ ├── src │ │ ├── TasksPlugin.php │ │ ├── Policy │ │ │ ├── ProjectsTablePolicy.php │ │ │ └── TasksTablePolicy.php │ │ └── View │ │ │ └── Cell │ │ │ └── ProjectsMenuCell.php │ └── tests │ │ └── Fixture │ │ ├── TasksFixture.php │ │ ├── ProjectsFixture.php │ │ ├── SubtasksFixture.php │ │ └── ProjectSectionsFixture.php └── Calendar │ ├── .gitignore │ ├── templates │ ├── GoogleOauth │ │ └── complete.php │ ├── element │ │ ├── calendarprovider_tile.php │ │ └── calendaritems.php │ ├── CalendarSources │ │ └── delete_confirm.php │ └── CalendarProviders │ │ └── index.php │ ├── tests │ └── Fixture │ │ ├── CalendarItemsFixture.php │ │ ├── CalendarSourcesFixture.php │ │ ├── CalendarProvidersFixture.php │ │ └── CalendarSubscriptionsFixture.php │ └── src │ ├── Policy │ ├── CalendarProvidersTablePolicy.php │ └── CalendarItemsTablePolicy.php │ └── CalendarPlugin.php ├── templates ├── element │ ├── icons │ │ ├── README.md │ │ ├── dot16.php │ │ ├── kebab16.php │ │ ├── chevron16.php │ │ ├── check16.php │ │ ├── chevron-right16.php │ │ ├── clock16.php │ │ ├── arrowleft16.php │ │ ├── grabber24.php │ │ ├── moon16.php │ │ ├── checkcircle16.php │ │ ├── plus16.php │ │ ├── lock16.php │ │ ├── alert16.php │ │ ├── calendar16.php │ │ ├── directory16.php │ │ ├── note16.php │ │ ├── rss16.php │ │ ├── pencil16.php │ │ ├── trash16.php │ │ ├── clippy16.php │ │ ├── sync16.php │ │ ├── archive16.php │ │ ├── link16.php │ │ ├── workflow16.php │ │ ├── directory-symlink16.php │ │ ├── unlink16.php │ │ ├── sun16.php │ │ ├── LICENSE │ │ └── trophy16.php │ ├── flash │ │ ├── error.php │ │ ├── default.php │ │ └── success.php │ ├── frontend_assets.php │ └── confirm_dialog.php ├── layout │ ├── sheet.php │ ├── card.php │ ├── modal.php │ ├── ajax.php │ ├── error.php │ ├── email │ │ ├── text │ │ │ └── default.php │ │ └── html │ │ │ └── default.php │ └── default.php ├── email │ ├── text │ │ ├── reset_password.php │ │ ├── verify_email.php │ │ └── default.php │ └── html │ │ └── default.php ├── Pages │ ├── home.php │ └── manifest.php ├── Users │ ├── reset_password.php │ └── new_password.php └── Error │ ├── error400.php │ └── error500.php ├── .phive └── phars.xml ├── prettier.config.js ├── babel.config.js ├── .htaccess ├── .eslintrc.js ├── bin ├── cake.php ├── server.js └── cake.bat ├── docker-compose.yaml ├── .editorconfig ├── psalm.xml ├── index.php ├── vite.config.ts ├── jest.config.js ├── phpcs.xml ├── LICENSE ├── tsconfig.json └── .gitignore /logs/.empty: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /config/Seeds/empty: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /webroot/js/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/Model/Behavior/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | -------------------------------------------------------------------------------- /tests/js/__mocks__/styleMock.js: -------------------------------------------------------------------------------- 1 | module.exports = {}; 2 | -------------------------------------------------------------------------------- /flutterapp/ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /webroot/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markstory/docket-app/HEAD/webroot/favicon.ico -------------------------------------------------------------------------------- /webroot/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markstory/docket-app/HEAD/webroot/favicon.png -------------------------------------------------------------------------------- /webroot/img/cake-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markstory/docket-app/HEAD/webroot/img/cake-logo.png -------------------------------------------------------------------------------- /webroot/img/cake.icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markstory/docket-app/HEAD/webroot/img/cake.icon.png -------------------------------------------------------------------------------- /webroot/img/cake.power.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markstory/docket-app/HEAD/webroot/img/cake.power.gif -------------------------------------------------------------------------------- /flutterapp/assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markstory/docket-app/HEAD/flutterapp/assets/splash.png -------------------------------------------------------------------------------- /webroot/img/docket-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markstory/docket-app/HEAD/webroot/img/docket-logo.png -------------------------------------------------------------------------------- /assets/sass/components/noProjects.scss: -------------------------------------------------------------------------------- 1 | .no-projects { 2 | display: flex; 3 | flex-direction: column; 4 | } 5 | -------------------------------------------------------------------------------- /flutterapp/assets/docket-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markstory/docket-app/HEAD/flutterapp/assets/docket-logo.png -------------------------------------------------------------------------------- /assets/sass/components/sectionQuickForm.scss: -------------------------------------------------------------------------------- 1 | .section-quickform { 2 | .title input { 3 | min-width: 200px; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /flutterapp/android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /flutterapp/assets/google-calendar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markstory/docket-app/HEAD/flutterapp/assets/google-calendar.png -------------------------------------------------------------------------------- /webroot/font/cakedingbats-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markstory/docket-app/HEAD/webroot/font/cakedingbats-webfont.eot -------------------------------------------------------------------------------- /webroot/font/cakedingbats-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markstory/docket-app/HEAD/webroot/font/cakedingbats-webfont.ttf -------------------------------------------------------------------------------- /webroot/font/cakedingbats-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markstory/docket-app/HEAD/webroot/font/cakedingbats-webfont.woff -------------------------------------------------------------------------------- /flutterapp/test_resources/login.json: -------------------------------------------------------------------------------- 1 | { 2 | "apiToken": { 3 | "token": "some-long-uuid", 4 | "last_used": null 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /webroot/font/cakedingbats-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markstory/docket-app/HEAD/webroot/font/cakedingbats-webfont.woff2 -------------------------------------------------------------------------------- /assets/sass/pages/calendars/add.scss: -------------------------------------------------------------------------------- 1 | .calendar-name { 2 | display: flex; 3 | align-items: center; 4 | gap: calc($space / 2); 5 | } 6 | -------------------------------------------------------------------------------- /docker/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | echo 'starting php-fpm in background' 3 | php-fpm & 4 | 5 | echo 'starting nginx' 6 | nginx -g 'daemon off;' 7 | -------------------------------------------------------------------------------- /assets/sass/components/feedSubscriptions.scss: -------------------------------------------------------------------------------- 1 | .feed-subscriptions table { 2 | width: 100%; 3 | 4 | th { 5 | text-align: left; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /flutterapp/ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /config/features.php: -------------------------------------------------------------------------------- 1 | [ 6 | 'feed-reader' => false, 7 | ], 8 | ]; 9 | -------------------------------------------------------------------------------- /flutterapp/ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /flutterapp/test_resources/subtask_update.json: -------------------------------------------------------------------------------- 1 | { 2 | "subtask": { 3 | "id": 1, "title": "fold big towels", "ranking": 1, "task_id": 1 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /webroot/.htaccess: -------------------------------------------------------------------------------- 1 | 2 | RewriteEngine On 3 | RewriteCond %{REQUEST_FILENAME} !-f 4 | RewriteRule ^ index.php [L] 5 | 6 | -------------------------------------------------------------------------------- /flutterapp/android/app/src/main/res/drawable-hdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markstory/docket-app/HEAD/flutterapp/android/app/src/main/res/drawable-hdpi/splash.png -------------------------------------------------------------------------------- /flutterapp/android/app/src/main/res/drawable-mdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markstory/docket-app/HEAD/flutterapp/android/app/src/main/res/drawable-mdpi/splash.png -------------------------------------------------------------------------------- /flutterapp/android/app/src/main/res/drawable-xhdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markstory/docket-app/HEAD/flutterapp/android/app/src/main/res/drawable-xhdpi/splash.png -------------------------------------------------------------------------------- /flutterapp/android/app/src/main/res/drawable/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markstory/docket-app/HEAD/flutterapp/android/app/src/main/res/drawable/background.png -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | drivers 2 | flutterapp 3 | vendor 4 | node_modules 5 | .git 6 | .github 7 | .phive 8 | .phpunit.cache 9 | .yarn 10 | config/app_local.php 11 | phpunit.xml 12 | -------------------------------------------------------------------------------- /flutterapp/android/app/src/main/res/drawable-v21/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markstory/docket-app/HEAD/flutterapp/android/app/src/main/res/drawable-v21/background.png -------------------------------------------------------------------------------- /flutterapp/android/app/src/main/res/drawable-xxhdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markstory/docket-app/HEAD/flutterapp/android/app/src/main/res/drawable-xxhdpi/splash.png -------------------------------------------------------------------------------- /flutterapp/android/app/src/main/res/drawable-xxxhdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markstory/docket-app/HEAD/flutterapp/android/app/src/main/res/drawable-xxxhdpi/splash.png -------------------------------------------------------------------------------- /flutterapp/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markstory/docket-app/HEAD/flutterapp/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /flutterapp/android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markstory/docket-app/HEAD/flutterapp/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /flutterapp/android/app/src/main/res/drawable-night/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markstory/docket-app/HEAD/flutterapp/android/app/src/main/res/drawable-night/background.png -------------------------------------------------------------------------------- /flutterapp/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markstory/docket-app/HEAD/flutterapp/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /flutterapp/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markstory/docket-app/HEAD/flutterapp/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /flutterapp/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markstory/docket-app/HEAD/flutterapp/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /flutterapp/android/app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #a848a8 4 | -------------------------------------------------------------------------------- /config/plugins.php: -------------------------------------------------------------------------------- 1 | [], 6 | 'Calendar' => [], 7 | 'Tasks' => [], 8 | 'Feeds' => [], 9 | ]; 10 | -------------------------------------------------------------------------------- /flutterapp/android/app/src/main/res/drawable-night-v21/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markstory/docket-app/HEAD/flutterapp/android/app/src/main/res/drawable-night-v21/background.png -------------------------------------------------------------------------------- /plugins/Feeds/.gitignore: -------------------------------------------------------------------------------- 1 | /composer.lock 2 | /composer.phar 3 | /phpunit.xml 4 | /.phpunit.result.cache 5 | /phpunit.phar 6 | /config/Migrations/schema-dump-default.lock 7 | /vendor/ 8 | /.idea/ 9 | -------------------------------------------------------------------------------- /plugins/Tasks/.gitignore: -------------------------------------------------------------------------------- 1 | /composer.lock 2 | /composer.phar 3 | /phpunit.xml 4 | /.phpunit.result.cache 5 | /phpunit.phar 6 | /config/Migrations/schema-dump-default.lock 7 | /vendor/ 8 | /.idea/ 9 | -------------------------------------------------------------------------------- /plugins/Tasks/templates/Projects/reorder.php: -------------------------------------------------------------------------------- 1 | setLayout('ajax'); 5 | 6 | echo $this->cell('Tasks.ProjectsMenu', ['identity' => $identity]); 7 | -------------------------------------------------------------------------------- /templates/element/icons/README.md: -------------------------------------------------------------------------------- 1 | # Icons 2 | 3 | The icons in this directory have been sourced from 4 | 5 | https://github.com/primer/octicons 6 | 7 | See the LICENSE file in this directory 8 | -------------------------------------------------------------------------------- /plugins/Calendar/.gitignore: -------------------------------------------------------------------------------- 1 | /composer.lock 2 | /composer.phar 3 | /phpunit.xml 4 | /.phpunit.result.cache 5 | /phpunit.phar 6 | /config/Migrations/schema-dump-default.lock 7 | /vendor/ 8 | /.idea/ 9 | -------------------------------------------------------------------------------- /flutterapp/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markstory/docket-app/HEAD/flutterapp/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /flutterapp/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markstory/docket-app/HEAD/flutterapp/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /flutterapp/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markstory/docket-app/HEAD/flutterapp/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /plugins/Feeds/templates/FeedCategories/reorder.php: -------------------------------------------------------------------------------- 1 | setLayout('ajax'); 5 | 6 | echo $this->cell('Feeds.FeedCategoryMenu', ['identity' => $identity]); 7 | -------------------------------------------------------------------------------- /flutterapp/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markstory/docket-app/HEAD/flutterapp/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /flutterapp/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markstory/docket-app/HEAD/flutterapp/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /flutterapp/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markstory/docket-app/HEAD/flutterapp/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /flutterapp/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markstory/docket-app/HEAD/flutterapp/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /flutterapp/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markstory/docket-app/HEAD/flutterapp/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /flutterapp/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markstory/docket-app/HEAD/flutterapp/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /flutterapp/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markstory/docket-app/HEAD/flutterapp/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /flutterapp/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markstory/docket-app/HEAD/flutterapp/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /flutterapp/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markstory/docket-app/HEAD/flutterapp/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /flutterapp/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markstory/docket-app/HEAD/flutterapp/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /flutterapp/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markstory/docket-app/HEAD/flutterapp/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /flutterapp/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markstory/docket-app/HEAD/flutterapp/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png -------------------------------------------------------------------------------- /flutterapp/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markstory/docket-app/HEAD/flutterapp/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png -------------------------------------------------------------------------------- /flutterapp/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markstory/docket-app/HEAD/flutterapp/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png -------------------------------------------------------------------------------- /flutterapp/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markstory/docket-app/HEAD/flutterapp/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png -------------------------------------------------------------------------------- /flutterapp/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markstory/docket-app/HEAD/flutterapp/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /flutterapp/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markstory/docket-app/HEAD/flutterapp/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /flutterapp/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markstory/docket-app/HEAD/flutterapp/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png -------------------------------------------------------------------------------- /flutterapp/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markstory/docket-app/HEAD/flutterapp/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png -------------------------------------------------------------------------------- /flutterapp/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markstory/docket-app/HEAD/flutterapp/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /flutterapp/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markstory/docket-app/HEAD/flutterapp/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /flutterapp/ios/Runner/Assets.xcassets/LaunchBackground.imageset/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markstory/docket-app/HEAD/flutterapp/ios/Runner/Assets.xcassets/LaunchBackground.imageset/background.png -------------------------------------------------------------------------------- /flutterapp/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markstory/docket-app/HEAD/flutterapp/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /flutterapp/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markstory/docket-app/HEAD/flutterapp/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /plugins/Feeds/templates/FeedCategories/toggle_expanded.php: -------------------------------------------------------------------------------- 1 | setLayout('ajax'); 5 | 6 | echo $this->cell('Feeds.FeedCategoryMenu', ['identity' => $identity]); 7 | -------------------------------------------------------------------------------- /templates/element/icons/dot16.php: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /flutterapp/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markstory/docket-app/HEAD/flutterapp/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /.phive/phars.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /flutterapp/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markstory/docket-app/HEAD/flutterapp/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /flutterapp/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markstory/docket-app/HEAD/flutterapp/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /flutterapp/ios/Runner/Assets.xcassets/LaunchBackground.imageset/darkbackground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markstory/docket-app/HEAD/flutterapp/ios/Runner/Assets.xcassets/LaunchBackground.imageset/darkbackground.png -------------------------------------------------------------------------------- /flutterapp/android/app/src/main/kotlin/com/docket/flutterapp/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.docket.flutterapp 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity: FlutterActivity() { 6 | } 7 | -------------------------------------------------------------------------------- /plugins/Tasks/templates/Tasks/delete_ok.php: -------------------------------------------------------------------------------- 1 | setLayout('ajax'); 5 | -------------------------------------------------------------------------------- /assets/sass/pages/feeds/viewitem.scss: -------------------------------------------------------------------------------- 1 | .feed-item-title { 2 | margin-bottom: 0; 3 | 4 | &.feed-item-read { 5 | opacity: 75% 6 | } 7 | } 8 | 9 | .feed-item-body { 10 | margin-bottom: calc($space * 2); 11 | } 12 | -------------------------------------------------------------------------------- /plugins/Calendar/templates/GoogleOauth/complete.php: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Connection Complete

4 |

You can close this browser view now.

5 |
6 |
7 | -------------------------------------------------------------------------------- /plugins/Feeds/src/Service/FeedSyncException.php: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /assets/sass/components/projectBadge.scss: -------------------------------------------------------------------------------- 1 | .project-badge { 2 | display: flex; 3 | align-items: center; 4 | 5 | // Nudge icon to the left so that it lines up with 6 | // any text above it. 7 | margin-left: calc($space / 2 * -1); 8 | } 9 | -------------------------------------------------------------------------------- /templates/layout/sheet.php: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | fetch('content') ?> 7 | 8 | 9 | -------------------------------------------------------------------------------- /tests/Fixture/ApiTokensFixture.php: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tests/js/setup.ts: -------------------------------------------------------------------------------- 1 | function assertDefined(maybe: T | undefined): asserts maybe { 2 | if (typeof maybe === 'undefined' || maybe === null) { 3 | throw new Error('Unexpected value. Did not expect null | undefined'); 4 | } 5 | } 6 | 7 | global.assertDefined = assertDefined; 8 | -------------------------------------------------------------------------------- /plugins/Feeds/tests/Fixture/FeedItemsFixture.php: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /plugins/Calendar/tests/Fixture/CalendarItemsFixture.php: -------------------------------------------------------------------------------- 1 | 8 |
9 | element('icons/alert16') ?> 10 | 11 |
12 | -------------------------------------------------------------------------------- /templates/element/icons/check16.php: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /flutterapp/ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /flutterapp/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /templates/element/icons/chevron-right16.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /plugins/Calendar/tests/Fixture/CalendarSubscriptionsFixture.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /templates/element/icons/clock16.php: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /plugins/Feeds/tests/Fixture/FeedSubscriptionsFeedItemsFixture.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /flutterapp/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /templates/element/icons/arrowleft16.php: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /flutterapp/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /templates/element/icons/grabber24.php: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /templates/element/icons/moon16.php: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /templates/layout/card.php: -------------------------------------------------------------------------------- 1 | extend('default'); 7 | ?> 8 |
9 |
10 |
fetch('content') ?>
11 |
12 |
13 | -------------------------------------------------------------------------------- /assets/sass/pages/projects/view.scss: -------------------------------------------------------------------------------- 1 | .project-view .heading-actions { 2 | &[data-archived="true"] { 3 | color: var(--color-muted); 4 | } 5 | 6 | h1 { 7 | display: flex; 8 | align-items: center; 9 | 10 | margin-bottom: 0; 11 | } 12 | } 13 | 14 | // Completed task list 15 | .project-view .task-list { 16 | margin-top: $space * 5; 17 | } 18 | -------------------------------------------------------------------------------- /templates/email/text/reset_password.php: -------------------------------------------------------------------------------- 1 | Hi , 2 | 3 | Someone (hopefully you) has requested a password reset for Docket. 4 | If this was you, follow the URL below to complete the process. 5 | If this wasn't you, don't worry you can ignore this email. 6 | 7 | Url->build(['_name' => 'users:newPassword', 'token' => $token], ['fullBase' => true]) ?> 8 | -------------------------------------------------------------------------------- /flutterapp/android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | 9 | # Remember to never publicly share your keystore. 10 | # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app 11 | key.properties 12 | **/*.keystore 13 | **/*.jks 14 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | /*eslint-env node*/ 2 | module.exports = { 3 | presets: [ 4 | '@babel/env', 5 | '@babel/preset-typescript', 6 | ], 7 | plugins: ['@babel/plugin-transform-runtime'], 8 | env: { 9 | test: { 10 | // Required, see https://github.com/facebook/jest/issues/9430 11 | plugins: ['dynamic-import-node'], 12 | }, 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /flutterapp/test_resources/calendar_source.json: -------------------------------------------------------------------------------- 1 | { 2 | "source": { 3 | "id":28, 4 | "name":"mark@example.com", 5 | "calendar_provider_id":5, 6 | "provider_id":"mark@example.com", 7 | "color":3, 8 | "last_sync":null, 9 | "created":"2022-09-25T14:53:13+00:00", 10 | "modified":"2022-12-08T01:32:52+00:00" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /flutterapp/test_resources/task_create_today.json: -------------------------------------------------------------------------------- 1 | { 2 | "task": { 3 | "id": 1, 4 | "project": {"id": 1, "slug": "home", "name": "home", "color": 1}, 5 | "title": "fold the towels", 6 | "body": "", 7 | "evening": false, 8 | "completed": false, 9 | "due_on": "__TODAY__", 10 | "child_order": 0, 11 | "day_order": 0 12 | } 13 | } 14 | 15 | -------------------------------------------------------------------------------- /plugins/Tasks/templates/ProjectSections/view.php: -------------------------------------------------------------------------------- 1 | setLayout('ajax'); 8 | 9 | echo $this->element('Tasks.projectsection_item', [ 10 | 'project' => $project, 11 | 'section' => $section, 12 | ]); 13 | -------------------------------------------------------------------------------- /assets/js/extensions/ajax.ts: -------------------------------------------------------------------------------- 1 | import htmx from 'htmx.org'; 2 | 3 | htmx.defineExtension('ajax-header', { 4 | onEvent: function (name, evt) { 5 | if (name === 'htmx:configRequest') { 6 | evt.detail.headers['X-Requested-With'] = 'XMLHttpRequest'; 7 | evt.detail.headers['X-Csrf-Token'] = document.getElementById('csrf-token').getAttribute('content'); 8 | } 9 | }, 10 | }); 11 | -------------------------------------------------------------------------------- /flutterapp/lib/screens/unknown.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class UnknownScreen extends StatelessWidget { 4 | const UnknownScreen({super.key}); 5 | 6 | @override 7 | Widget build(BuildContext context) { 8 | return Scaffold( 9 | appBar: AppBar(), 10 | body: const Center( 11 | child: Text('404!'), 12 | )); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /plugins/Tasks/templates/Tasks/reschedule.php: -------------------------------------------------------------------------------- 1 | setLayout('ajax'); 8 | 9 | $this->response = $this->response->withHeader('Hx-Trigger-After-Swap', 'reposition'); 10 | 11 | echo $this->element('Tasks.task_dueon_menu', ['task' => $task, 'referer' => $referer]); 12 | -------------------------------------------------------------------------------- /templates/element/icons/checkcircle16.php: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /templates/element/icons/plus16.php: -------------------------------------------------------------------------------- 1 | 4 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /.htaccess: -------------------------------------------------------------------------------- 1 | # Uncomment the following to prevent the httpoxy vulnerability 2 | # See: https://httpoxy.org/ 3 | # 4 | # RequestHeader unset Proxy 5 | # 6 | 7 | 8 | RewriteEngine on 9 | RewriteRule ^(\.well-known/.*)$ $1 [L] 10 | RewriteRule ^$ webroot/ [L] 11 | RewriteRule (.*) webroot/$1 [L] 12 | 13 | -------------------------------------------------------------------------------- /flutterapp/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /plugins/Tasks/templates/Tasks/delete_confirm.php: -------------------------------------------------------------------------------- 1 | element('confirm_dialog', [ 8 | 'target' => ['_name' => 'tasks:delete', 'id' => $task->id], 9 | 'title' => 'Are you sure?', 10 | 'description' => 'This will also delete all subtasks this task has.', 11 | ]); 12 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | ecmaVersion: 2020, 5 | sourceType: 'module', 6 | }, 7 | plugins: ['jest'], 8 | extends: [ 9 | // Prettier rules need to be at the end as they tweak other plugins. 10 | 'prettier', 11 | 'plugin:prettier/recommended', 12 | 'prettier/@typescript-eslint', 13 | ], 14 | rules: {}, 15 | }; 16 | -------------------------------------------------------------------------------- /flutterapp/android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/Policy/ApiTokensTablePolicy.php: -------------------------------------------------------------------------------- 1 | where(['ApiTokens.user_id' => $user->id]); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /flutterapp/android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /flutterapp/android/app/src/main/res/drawable-night-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /flutterapp/android/app/src/main/res/drawable-night/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /templates/element/icons/lock16.php: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /plugins/Tasks/templates/Projects/delete_confirm.php: -------------------------------------------------------------------------------- 1 | element('confirm_dialog', [ 8 | 'target' => ['_name' => 'projects:delete', 'slug' => $project->slug], 9 | 'title' => 'Are you sure?', 10 | 'description' => 'This will delete all tasks in this project as well.', 11 | ]); 12 | -------------------------------------------------------------------------------- /assets/js/locale.ts: -------------------------------------------------------------------------------- 1 | const PLACEHOLDER_PATTERN = /\{(\w+)\}/; 2 | 3 | /** 4 | * Stub function until a proper localization library is chosen. 5 | */ 6 | export function t(message: string, data?: Record): string { 7 | return message.replace(PLACEHOLDER_PATTERN, function (_match, key) { 8 | if (data?.hasOwnProperty(key)) { 9 | return String(data[key]); 10 | } 11 | return ''; 12 | }); 13 | } 14 | -------------------------------------------------------------------------------- /templates/Pages/home.php: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Docket

4 |

Your personal todo list

5 |
6 | Html->link('Login', ['_name' => 'users:login'], ['class' => 'button-primary']) ?> 7 | Html->link('Create an Account', ['_name' => 'users:add'], ['class' => 'button-secondary']) ?> 8 |
9 |
10 |
11 | -------------------------------------------------------------------------------- /templates/element/icons/alert16.php: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /bin/cake.php: -------------------------------------------------------------------------------- 1 | #!/usr/bin/php -q 2 | run($argv)); 13 | -------------------------------------------------------------------------------- /plugins/Tasks/src/Policy/ProjectsTablePolicy.php: -------------------------------------------------------------------------------- 1 | where(['Projects.user_id' => $user->id]); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /plugins/Feeds/templates/FeedSubscriptions/delete_confirm.php: -------------------------------------------------------------------------------- 1 | element('confirm_dialog', [ 8 | 'target' => ['_name' => 'feedsubscriptions:delete', 'id' => $feedSubscription->id], 9 | 'title' => 'Are you sure?', 10 | 'description' => 'You can always subscribe again later.', 11 | ]); 12 | -------------------------------------------------------------------------------- /templates/layout/modal.php: -------------------------------------------------------------------------------- 1 | Html 6 | * @var array|null $dialogOptions additional options for the dialog element. deletion-confirm uses this. 7 | */ 8 | ?> 9 | 10 | Html->tag( 11 | 'dialog', 12 | $this->fetch('content'), 13 | $dialogOptions ?? [], 14 | ) ?> 15 | 16 | -------------------------------------------------------------------------------- /templates/element/flash/default.php: -------------------------------------------------------------------------------- 1 | 15 |
16 | -------------------------------------------------------------------------------- /plugins/Feeds/src/Policy/FeedCategoriesTablePolicy.php: -------------------------------------------------------------------------------- 1 | where(['FeedCategories.user_id' => $user->id]); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /flutterapp/android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /templates/element/icons/calendar16.php: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /flutterapp/android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /plugins/Feeds/src/Policy/FeedSubscriptionsTablePolicy.php: -------------------------------------------------------------------------------- 1 | where(['FeedSubscriptions.user_id' => $user->id]); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /flutterapp/ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Flutter 3 | 4 | @UIApplicationMain 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /templates/element/icons/directory16.php: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /templates/element/icons/note16.php: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /templates/element/flash/success.php: -------------------------------------------------------------------------------- 1 | 11 |
12 | element('icons/checkcircle16') ?> 13 | 14 |
15 | -------------------------------------------------------------------------------- /webroot/img/docket-logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /templates/element/icons/rss16.php: -------------------------------------------------------------------------------- 1 | 4 | 5 | -------------------------------------------------------------------------------- /plugins/Feeds/templates/FeedCategories/delete_confirm.php: -------------------------------------------------------------------------------- 1 | set('closable', false); 8 | 9 | $this->setLayout('modal'); 10 | 11 | echo $this->element('confirm_dialog', [ 12 | 'target' => ['_name' => 'feedcategories:delete', 'id' => $feedCategory->id], 13 | 'title' => 'Are you sure?', 14 | 'description' => 'Feeds in this category will also be removed', 15 | ]); 16 | -------------------------------------------------------------------------------- /assets/sass/components/confirm.scss: -------------------------------------------------------------------------------- 1 | .confirm-dialog { 2 | position: fixed; 3 | top: 80px; 4 | left: 25%; 5 | right: 25%; 6 | bottom: auto; 7 | padding-top: 20px; 8 | z-index: $z-modal; 9 | 10 | border-radius: $border-radius-large; 11 | border: none; 12 | box-shadow: var(--shadow-high); 13 | } 14 | 15 | 16 | .confirm-dialog h2 { 17 | font-size: $font-size-large; 18 | font-weight: bold; 19 | margin: 0 0 $space * 2 0; 20 | } 21 | .confirm-dialog p { 22 | margin-bottom: $space; 23 | } 24 | 25 | -------------------------------------------------------------------------------- /plugins/Calendar/templates/element/calendarprovider_tile.php: -------------------------------------------------------------------------------- 1 | kind === 'google') : 8 | $icon = <<<'HTML' 9 | Google Calendar logo 15 | HTML; 16 | endif; 17 | ?> 18 | 19 | display_name) ?> 20 | 21 | -------------------------------------------------------------------------------- /plugins/Tasks/templates/ProjectSections/delete_confirm.php: -------------------------------------------------------------------------------- 1 | element('confirm_dialog', [ 9 | 'target' => ['_name' => 'projectsections:delete', 'projectSlug' => $project->slug, 'id' => $section->id], 10 | 'title' => 'Are you sure?', 11 | 'description' => 'This will delete all tasks in this section as well.', 12 | ]); 13 | -------------------------------------------------------------------------------- /templates/element/icons/pencil16.php: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /templates/element/icons/trash16.php: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /flutterapp/android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def localPropertiesFile = new File(rootProject.projectDir, "local.properties") 4 | def properties = new Properties() 5 | 6 | assert localPropertiesFile.exists() 7 | localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } 8 | 9 | def flutterSdkPath = properties.getProperty("flutter.sdk") 10 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 11 | apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" 12 | -------------------------------------------------------------------------------- /flutterapp/ios/Runner/Assets.xcassets/LaunchBackground.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "background.png", 5 | "idiom" : "universal" 6 | }, 7 | { 8 | "appearances" : [ 9 | { 10 | "appearance" : "luminosity", 11 | "value" : "dark" 12 | } 13 | ], 14 | "filename" : "darkbackground.png", 15 | "idiom" : "universal" 16 | } 17 | ], 18 | "info" : { 19 | "author" : "xcode", 20 | "version" : 1 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /plugins/Feeds/src/Policy/FeedItemsTablePolicy.php: -------------------------------------------------------------------------------- 1 | innerJoinWith('FeedSubscriptions') 18 | ->where(['FeedSubscriptions.user_id' => $user->id]); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /plugins/Tasks/templates/element/task_body.php: -------------------------------------------------------------------------------- 1 | Form->control('body', [ 2 | 'id' => 'task-body-text', 3 | 'label' => [ 4 | 'class' => 'form-section-heading icon-not-due', 5 | 'text' => $this->element('icons/note16') . 'Notes', 6 | 'escape' => false, 7 | ], 8 | 'type' => 'textarea', 9 | 'rows' => 1, 10 | 'templates' => [ 11 | 'formGroup' => '{{label}}{{input}}{{error}}', 12 | 'inputContainer' => '{{content}}', 13 | ], 14 | ]) ?> 15 | -------------------------------------------------------------------------------- /templates/element/icons/clippy16.php: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tests/Fixture/google-auth.json: -------------------------------------------------------------------------------- 1 | { 2 | "web": { 3 | "client_id": "fake-client-id.apps.googleusercontent.com", 4 | "project_id": "api-project-123456", 5 | "auth_uri": "https://accounts.google.com/o/oauth2/auth", 6 | "token_uri": "https://oauth2.googleapis.com/token", 7 | "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", 8 | "client_secret": "client-secret-value", 9 | "redirect_uris": [ 10 | "http://localhost:8765" 11 | ] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /templates/element/icons/sync16.php: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /flutterapp/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "LaunchImage.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "LaunchImage@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "LaunchImage@3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /plugins/Tasks/src/Policy/TasksTablePolicy.php: -------------------------------------------------------------------------------- 1 | innerJoinWith('Projects') 21 | ->where(['Projects.user_id' => $user->id]); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /plugins/Calendar/src/Policy/CalendarProvidersTablePolicy.php: -------------------------------------------------------------------------------- 1 | where(['CalendarProviders.user_id' => $user->id]); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /assets/sass/utils.scss: -------------------------------------------------------------------------------- 1 | // Used to give hover to editable regions. 2 | .editable { 3 | border-radius: $border-radius; 4 | padding: calc($space / 4) calc($space / 2); 5 | margin-left: calc(($space / 2) * -1); 6 | 7 | &:hover { 8 | background: var(--color-disabled); 9 | } 10 | } 11 | 12 | // Placeholder text. 13 | .placeholder { 14 | color: var(--color-muted); 15 | } 16 | 17 | // Counters displayed beside other content. 18 | .counter { 19 | font-size: $font-size-normal; 20 | color: var(--color-muted); 21 | svg { 22 | margin-right: calc($space / 2); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /templates/element/frontend_assets.php: -------------------------------------------------------------------------------- 1 | 8 | 9 | ViteAsset->css('assets/js/app.tsx') ?> 10 | Html->script('http://localhost:3000/@vite/client', ['type' => 'module']) ?> 11 | Html->script('http://localhost:3000/assets/js/app.tsx', ['type' => 'module']) ?> 12 | 13 | ViteAsset->css('assets/js/app.tsx') ?> 14 | ViteAsset->script('assets/js/app.tsx') ?> 15 | assign('title', 'Trash Bin'); 8 | 9 | $this->setLayout('sidebar'); 10 | ?> 11 |

12 | element('icons/trash16') ?> 13 | Trash Bin 14 |

15 |

Trash tasks will be deleted permanently after 14 days

16 | 17 | element('Tasks.task_item_restore', ['task' => $task]) ?> 18 | 19 | -------------------------------------------------------------------------------- /assets/sass/components/taskGroup.scss: -------------------------------------------------------------------------------- 1 | .task-group .add-task { 2 | margin-top: $space; 3 | } 4 | .task-group .dnd-item:last-child .task-row { 5 | border-bottom: 0; 6 | } 7 | 8 | .task-group + .heading-icon, 9 | .task-group + .heading-task-group { 10 | margin-top: calc($space * 5); 11 | } 12 | 13 | .empty-state-container { 14 | display: flex; 15 | flex-direction: column; 16 | align-items: center; 17 | 18 | .hero-icon { 19 | background: var(--color-bg-low); 20 | border-radius: 50%; 21 | padding: calc($space * 3); 22 | 23 | color: var(--color-success-fg); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /templates/element/icons/archive16.php: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /plugins/Tasks/templates/Tasks/view.php: -------------------------------------------------------------------------------- 1 | setLayout('sidebar'); 10 | $this->assign('title', 'Tasks - ' . h($task->title)); 11 | 12 | echo $this->element('Tasks.task_form', [ 13 | 'task' => $task, 14 | 'projects' => $projects, 15 | 'sections' => $sections, 16 | 'referer' => $referer, 17 | 'url' => ['_name' => 'tasks:edit', 'id' => $task->id], 18 | ]); 19 | -------------------------------------------------------------------------------- /src/Policy/UserPolicy.php: -------------------------------------------------------------------------------- 1 | id === $identity->id; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /webroot/img/docket-logo-translucent.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /flutterapp/lib/models/apitoken.dart: -------------------------------------------------------------------------------- 1 | class ApiToken { 2 | final int? id; 3 | final String token; 4 | final String? lastUsed; 5 | 6 | ApiToken({this.id, required this.token, this.lastUsed}); 7 | 8 | factory ApiToken.fromMap(Map json) { 9 | return ApiToken( 10 | token: json['token'], 11 | lastUsed: json['last_used'], 12 | ); 13 | } 14 | factory ApiToken.fake() { 15 | return ApiToken(token: 'abc123'); 16 | } 17 | 18 | Map toMap() { 19 | return { 20 | 'token': token, 21 | 'last_used': lastUsed, 22 | }; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | version: '3' 3 | services: 4 | php: 5 | build: 6 | context: . 7 | dockerfile: Dockerfile 8 | environment: 9 | DATABASE_URL: "mysql://docket:docket-secret@mysql/docket" 10 | ports: 11 | - 5000:5000 12 | depends_on: 13 | - mysql 14 | mysql: 15 | image: docker.io/library/mysql:8.0 16 | environment: 17 | MYSQL_ROOT_PASSWORD: root--secret 18 | MYSQL_DATABASE: docket 19 | MYSQL_USER: docket 20 | MYSQL_PASSWORD: docket-secret 21 | volumes: 22 | - mysql_data:/var/lib/mysql 23 | volumes: 24 | mysql_data: 25 | -------------------------------------------------------------------------------- /flutterapp/test_resources/task_details.json: -------------------------------------------------------------------------------- 1 | { 2 | "task": { 3 | "id": 1, 4 | "project": {"id": 1, "slug": "home", "name": "home", "color": 1}, 5 | "title": "clean the house", 6 | "body": "", 7 | "evening": false, 8 | "completed": false, 9 | "due_on": "__TODAY__", 10 | "child_order": 0, 11 | "day_order": 0, 12 | "subtasks": [ 13 | {"id": 1, "title": "vacuum", "ranking": 0, "completed": false}, 14 | {"id": 2, "title": "clean bathrooms", "ranking": 0, "completed": true} 15 | ], 16 | "subtask_count": 2, 17 | "complete_subtask_count": 1 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /templates/element/icons/link16.php: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/Policy/ApiTokenPolicy.php: -------------------------------------------------------------------------------- 1 | id === $apiToken->user_id; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /flutterapp/lib/components/loadingindicator.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:docket/theme.dart'; 3 | 4 | class LoadingIndicator extends StatelessWidget { 5 | const LoadingIndicator({Key? key}) : super(key: key); 6 | 7 | @override 8 | Widget build(BuildContext context) { 9 | return Padding( 10 | padding: EdgeInsets.only(top: space(4)), 11 | child: Row(mainAxisAlignment: MainAxisAlignment.center, children: const [ 12 | SizedBox( 13 | width: 60, 14 | height: 60, 15 | child: CircularProgressIndicator(), 16 | ), 17 | ])); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /plugins/Tasks/templates/element/task_checkbox.php: -------------------------------------------------------------------------------- 1 | 17 | 24 | -------------------------------------------------------------------------------- /templates/element/icons/workflow16.php: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tests/Fixture/vcr/controller_calendarsources_delete.yml: -------------------------------------------------------------------------------- 1 | 2 | - 3 | request: 4 | method: POST 5 | url: 'https://www.googleapis.com/calendar/v3/channels/stop' 6 | headers: 7 | content-type: application/json 8 | authorization: 'Bearer calendar-access-token' 9 | body: '{"id":"subscription-id"}' 10 | response: 11 | status: 12 | http_version: '1.1' 13 | code: 204 14 | message: No Content 15 | headers: 16 | Content-Type: 'application/json; charset=UTF-8' 17 | Date: 'Wed, 28 Jul 2021 03:30:30 GMT' 18 | -------------------------------------------------------------------------------- /config/Migrations/20210316024149_UserSpecificProjectSlug.php: -------------------------------------------------------------------------------- 1 | table('projects') 19 | ->addIndex(['user_id', 'slug'], ['unique' => true]) 20 | ->save(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /plugins/Calendar/templates/CalendarSources/delete_confirm.php: -------------------------------------------------------------------------------- 1 | set('closable', false); 8 | 9 | $this->setLayout('modal'); 10 | 11 | echo $this->element('confirm_dialog', [ 12 | 'target' => [ 13 | '_name' => 'calendarsources:delete', 14 | 'id' => $calendarSource->id, 15 | 'providerId' => $calendarSource->calendar_provider_id, 16 | ], 17 | 'title' => 'Are you sure?', 18 | 'description' => 'This will delete all events in this calendar.', 19 | ]); 20 | -------------------------------------------------------------------------------- /assets/js/webcomponents/sideBar.ts: -------------------------------------------------------------------------------- 1 | class SideBar extends HTMLElement { 2 | connectedCallback() { 3 | const button = this.querySelector('[data-expander]'); 4 | if (!button) { 5 | console.error('Could not find expander for side-bar'); 6 | return; 7 | } 8 | const sidebar = this; 9 | button.addEventListener('click', function (evt) { 10 | evt.preventDefault(); 11 | const current = sidebar.dataset.expanded; 12 | sidebar.dataset.expanded = current === 'false' ? 'true' : 'false'; 13 | }); 14 | } 15 | } 16 | 17 | customElements.define('side-bar', SideBar); 18 | 19 | export default SideBar; 20 | -------------------------------------------------------------------------------- /config/Migrations/20210226033714_RenameTasksProjectSectionId.php: -------------------------------------------------------------------------------- 1 | table('tasks') 19 | ->renameColumn('project_section_id', 'section_id') 20 | ->update(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /plugins/Tasks/templates/Projects/archived.php: -------------------------------------------------------------------------------- 1 | setLayout('sidebar'); 8 | $this->assign('title', 'Archived Projects'); 9 | ?> 10 |

Archived Projects

11 | element('Tasks.project_item', ['project' => $project, 'showMenu' => true]); 14 | endforeach; 15 | 16 | if (empty($projects)) : ?> 17 |
18 |

Nothing to see

19 |

You don't have any archived projects.

20 |
21 | 22 | -------------------------------------------------------------------------------- /templates/email/text/verify_email.php: -------------------------------------------------------------------------------- 1 | Hi , 2 | 3 | 4 | To help ensure you're a human we need to verify that belongs to a human. Failing to verify your email address will result in your account being deleted in 10 days. 5 | 6 | Before your email can be changed from to we need you to verify that is yours. 7 | 8 | 9 | Make sure you're logged in and then visit the following link: 10 | 11 | Url->build(['_name' => 'users:verifyEmail', 'token' => $token], ['fullBase' => true]) ?> 12 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | ; This file is for unifying the coding style for different editors and IDEs. 2 | ; More information at https://editorconfig.org 3 | 4 | root = true 5 | 6 | [*] 7 | indent_style = space 8 | indent_size = 4 9 | end_of_line = lf 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true 12 | 13 | [*.bat] 14 | end_of_line = crlf 15 | 16 | [*.yml] 17 | indent_size = 2 18 | 19 | [*.js] 20 | indent_size = 2 21 | 22 | [*.tsx] 23 | indent_size = 2 24 | 25 | [*.ts] 26 | indent_size = 2 27 | 28 | [*.scss] 29 | indent_size = 2 30 | 31 | [*.twig] 32 | indent_size = 2 33 | insert_final_newline = false 34 | 35 | [Makefile] 36 | indent_style = tab 37 | -------------------------------------------------------------------------------- /config/Migrations/20241022042500_AddContentToFeedItems.php: -------------------------------------------------------------------------------- 1 | table('feed_items'); 18 | $table->addColumn('content', 'text', [ 19 | 'default' => '', 20 | ]); 21 | $table->update(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /assets/js/extensions/projectSorter.ts: -------------------------------------------------------------------------------- 1 | import htmx from 'htmx.org'; 2 | import Sortable from 'sortablejs'; 3 | 4 | (function () { 5 | htmx.defineExtension('project-sorter', { 6 | onEvent: function (name, evt) { 7 | if (name !== 'htmx:afterProcessNode') { 8 | return; 9 | } 10 | const element = evt.target as HTMLElement; 11 | // Implementing elements listen to the `end` event 12 | // triggered on this element and submits a form. 13 | new Sortable(element, { 14 | animation: 150, 15 | ghostClass: 'dnd-ghost', 16 | dragClass: 'dnd-item-dragging', 17 | handle: '.dnd-handle', 18 | }); 19 | }, 20 | }); 21 | })(); 22 | -------------------------------------------------------------------------------- /config/Migrations/20210915012937_ConvertCalendarItemsHtmlLinkToText.php: -------------------------------------------------------------------------------- 1 | table('calendar_items'); 18 | $table->changeColumn('html_link', 'text', [ 19 | 'null' => true, 20 | ]); 21 | $table->save(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /config/Migrations/20241126033547_AddFaviconToFeeds.php: -------------------------------------------------------------------------------- 1 | table('feeds'); 18 | $table->addColumn('favicon_url', 'text', [ 19 | 'default' => null, 20 | 'null' => true, 21 | ]); 22 | $table->update(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /assets/sass/components/dueOn.scss: -------------------------------------------------------------------------------- 1 | // TODO refactor element/task_due_on to use more shared CSS 2 | .due-on { 3 | display: flex; 4 | align-items: center; 5 | 6 | svg { 7 | margin-right: calc($space / 2); 8 | } 9 | 10 | &.none { 11 | color: var(--color-due-none); 12 | } 13 | &.overdue { 14 | color: var(--color-due-overdue); 15 | } 16 | &.today { 17 | color: var(--color-due-today); 18 | } 19 | &.evening { 20 | color: var(--color-due-evening); 21 | } 22 | &.tomorrow { 23 | color: var(--color-due-tomorrow); 24 | } 25 | &.week { 26 | color: var(--color-due-week); 27 | } 28 | &.fortnight { 29 | color: var(--color-due-fortnight); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /templates/Pages/manifest.php: -------------------------------------------------------------------------------- 1 | disableAutoLayout(); 3 | 4 | $this->response = $this->response->withType('application/manifest+json'); 5 | 6 | echo json_encode([ 7 | 'background_color' => '#a848a8', 8 | 'display' => 'standalone', 9 | 'icons' => [ 10 | [ 11 | 'src' => $this->Url->assetUrl('img/docket-logo.png'), 12 | 'sizes' => '192x192', 13 | 'type' => 'image/png', 14 | 'purpose' => 'maskable any', 15 | ], 16 | ], 17 | 'name' => 'Docket', 18 | 'description' => 'Your personal todo list', 19 | 'short_name' => 'Docket', 20 | 'start_url' => $this->Url->build(['_name' => 'tasks:today']), 21 | ]); 22 | -------------------------------------------------------------------------------- /assets/sass/components/sectionContainer.scss: -------------------------------------------------------------------------------- 1 | .section-container { 2 | margin-top: calc($space * 5); 3 | 4 | .controls { 5 | display: flex; 6 | justify-content: space-between; 7 | 8 | margin-bottom: $space; 9 | border-bottom: 1px solid var(--color-border); 10 | 11 | drop-down { 12 | visibility: hidden; 13 | } 14 | &:hover drop-down, 15 | &:hover .dnd-handle { 16 | visibility: visible; 17 | } 18 | } 19 | 20 | &.dnd-ghost { 21 | opacity: 0.5; 22 | } 23 | 24 | .heading { 25 | display: flex; 26 | align-items: center; 27 | margin: 0 0 0 -34px; 28 | } 29 | 30 | .section-quickform { 31 | margin-bottom: $space; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /config/Migrations/20221030031829_AddDeletedToTasks.php: -------------------------------------------------------------------------------- 1 | table('tasks'); 19 | $table->addColumn('deleted_at', 'datetime', [ 20 | 'default' => null, 21 | 'null' => true, 22 | ]); 23 | $table->update(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /psalm.xml: -------------------------------------------------------------------------------- 1 | 2 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /templates/element/icons/directory-symlink16.php: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /flutterapp/lib/components/taskgroup.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'package:docket/components/taskitem.dart'; 4 | import 'package:docket/models/task.dart'; 5 | 6 | class TaskGroup extends StatelessWidget { 7 | final List tasks; 8 | final bool showDate; 9 | final bool showProject; 10 | 11 | const TaskGroup({required this.tasks, this.showDate = false, this.showProject = false, super.key}); 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | var taskItems = tasks.map((Task task) { 16 | return TaskItem(task: task, showDate: showDate, showProject: showProject); 17 | }).toList(); 18 | 19 | return Column(children: taskItems); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /index.php: -------------------------------------------------------------------------------- 1 | fetch('content'); 17 | -------------------------------------------------------------------------------- /templates/layout/error.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Html->charset() ?> 5 | 6 | <?= $this->fetch('title') ?> 7 | 8 | Html->meta('icon') ?> 9 | 10 | fetch('meta') ?> 11 | fetch('css') ?> 12 | fetch('script') ?> 13 | 14 | element('frontend_assets') ?> 15 | 16 | 17 |
18 |
19 |
20 | Flash->render() ?> 21 | fetch('content') ?> 22 | Html->link(__('Back'), 'javascript:history.back()') ?> 23 |
24 |
25 |
26 | 27 | 28 | -------------------------------------------------------------------------------- /config/Migrations/20241103040408_AddExpandedToFeedCategoriesTable.php: -------------------------------------------------------------------------------- 1 | table('feed_categories'); 18 | $table->addColumn('expanded', 'boolean', [ 19 | 'default' => false, 20 | 'null' => false, 21 | ]); 22 | $table->update(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /config/Migrations/20241110175742_AddAuthorToFeedItems.php: -------------------------------------------------------------------------------- 1 | table('feed_items'); 18 | $table->addColumn('author', 'string', [ 19 | 'default' => null, 20 | 'limit' => 255, 21 | 'null' => true, 22 | ]); 23 | $table->update(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /flutterapp/ios/.gitignore: -------------------------------------------------------------------------------- 1 | **/dgph 2 | *.mode1v3 3 | *.mode2v3 4 | *.moved-aside 5 | *.pbxuser 6 | *.perspectivev3 7 | **/*sync/ 8 | .sconsign.dblite 9 | .tags* 10 | **/.vagrant/ 11 | **/DerivedData/ 12 | Icon? 13 | **/Pods/ 14 | **/.symlinks/ 15 | profile 16 | xcuserdata 17 | **/.generated/ 18 | Flutter/App.framework 19 | Flutter/Flutter.framework 20 | Flutter/Flutter.podspec 21 | Flutter/Generated.xcconfig 22 | Flutter/ephemeral/ 23 | Flutter/app.flx 24 | Flutter/app.zip 25 | Flutter/flutter_assets/ 26 | Flutter/flutter_export_environment.sh 27 | ServiceDefinitions.json 28 | Runner/GeneratedPluginRegistrant.* 29 | 30 | # Exceptions to above rules. 31 | !default.mode1v3 32 | !default.mode2v3 33 | !default.pbxuser 34 | !default.perspectivev3 35 | -------------------------------------------------------------------------------- /templates/element/icons/unlink16.php: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /config/Migrations/20210215044003_AddEveningToTasks.php: -------------------------------------------------------------------------------- 1 | table('tasks') 19 | ->addColumn('evening', 'boolean', [ 20 | 'default' => false, 21 | 'null' => false, 22 | 'after' => 'completed', 23 | ]) 24 | ->update(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /config/Migrations/20210825015018_AddExpirationToCalendarSubscription.php: -------------------------------------------------------------------------------- 1 | table('calendar_subscriptions'); 18 | $table->addColumn('expires_at', 'timestamp', [ 19 | 'null' => true, 20 | 'after' => 'verifier', 21 | ]); 22 | $table->update(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /config/Migrations/20240518035817_AddResourceIdToCalendarSubscriptions.php: -------------------------------------------------------------------------------- 1 | table('calendar_subscriptions'); 18 | $table->addColumn('resource_id', 'string', [ 19 | 'default' => null, 20 | 'null' => true, 21 | ]); 22 | $table->update(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /config/Migrations/20241112035515_AddDefaultAliasToFeedsTable.php: -------------------------------------------------------------------------------- 1 | table('feeds'); 18 | $table->addColumn('default_alias', 'string', [ 19 | 'default' => null, 20 | 'limit' => 255, 21 | 'null' => true, 22 | ]); 23 | $table->update(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /config/schema/i18n.sql: -------------------------------------------------------------------------------- 1 | # Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org) 2 | # 3 | # Licensed under The MIT License 4 | # For full copyright and license information, please see the LICENSE.txt 5 | # Redistributions of files must retain the above copyright notice. 6 | # MIT License (https://opensource.org/licenses/mit-license.php) 7 | 8 | CREATE TABLE i18n ( 9 | id int NOT NULL auto_increment, 10 | locale varchar(6) NOT NULL, 11 | model varchar(255) NOT NULL, 12 | foreign_key int(10) NOT NULL, 13 | field varchar(255) NOT NULL, 14 | content text, 15 | PRIMARY KEY (id), 16 | UNIQUE INDEX I18N_LOCALE_FIELD(locale, model, foreign_key, field), 17 | INDEX I18N_FIELD(model, foreign_key, field) 18 | ); 19 | -------------------------------------------------------------------------------- /templates/layout/email/text/default.php: -------------------------------------------------------------------------------- 1 | fetch('content'); 18 | -------------------------------------------------------------------------------- /tests/Acceptance/LoginTest.php: -------------------------------------------------------------------------------- 1 | createClient(); 11 | 12 | $client->request('GET', '/login'); 13 | $client->waitFor('input[name="password"]'); 14 | $this->assertTextContains('Login', $client->getTitle()); 15 | 16 | $client->submitForm('Login', [ 17 | 'email' => 'mark@example.com', 18 | 'password' => 'password123', 19 | ]); 20 | $client->waitFor('[data-testid="loggedin"]'); 21 | $this->assertTextContains("Today's Tasks", $client->getTitle()); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /flutterapp/android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.8.21' 3 | repositories { 4 | google() 5 | mavenCentral() 6 | } 7 | 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:7.1.2' 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 11 | } 12 | } 13 | 14 | allprojects { 15 | repositories { 16 | google() 17 | mavenCentral() 18 | } 19 | } 20 | 21 | rootProject.buildDir = '../build' 22 | subprojects { 23 | project.buildDir = "${rootProject.buildDir}/${project.name}" 24 | } 25 | subprojects { 26 | project.evaluationDependsOn(':app') 27 | } 28 | 29 | tasks.register("clean", Delete) { 30 | delete rootProject.buildDir 31 | } 32 | -------------------------------------------------------------------------------- /config/Migrations/20210322024423_AddThemeToUser.php: -------------------------------------------------------------------------------- 1 | table('users'); 19 | $table->addColumn('theme', 'string', [ 20 | 'default' => 'light', 21 | 'limit' => 255, 22 | 'null' => false, 23 | 'after' => 'timezone', 24 | ]); 25 | $table->update(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /flutterapp/lib/viewmodels/taskform.dart: -------------------------------------------------------------------------------- 1 | import 'package:docket/models/task.dart'; 2 | 3 | /// 4 | /// Abstract interface that TaskForm depends on. 5 | /// Allows viewmodels from both new and edit screens which 6 | /// need to manage state separately. 7 | /// 8 | abstract class TaskFormViewModel { 9 | Task get task; 10 | bool get loading; 11 | 12 | Future reorderSubtask(int oldItemIndex, int oldListIndex, int newItemIndex, int newListIndex); 13 | 14 | Future saveSubtask(Task task, Subtask subtask); 15 | 16 | Future toggleSubtask(Task task, Subtask subtask); 17 | 18 | Future deleteSubtask(Task task, Subtask subtask); 19 | 20 | void addListener(void Function() listener); 21 | 22 | void removeListener(void Function() listener); 23 | } 24 | -------------------------------------------------------------------------------- /plugins/Feeds/src/FeedsPlugin.php: -------------------------------------------------------------------------------- 1 | addServiceProvider(new FeedServiceProvider()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /assets/js/extensions/removeRow.ts: -------------------------------------------------------------------------------- 1 | import htmx from 'htmx.org'; 2 | 3 | (function () { 4 | htmx.defineExtension('remove-row', { 5 | onEvent: function (name, event) { 6 | if (name !== 'htmx:afterProcessNode') { 7 | return; 8 | } 9 | const element = event.target as HTMLElement; 10 | element.addEventListener('click', function () { 11 | const selector = element.getAttribute('remove-row-target'); 12 | let row: HTMLElement | null = null; 13 | if (selector) { 14 | row = element.closest(selector); 15 | } else { 16 | row = element.parentNode as HTMLElement; 17 | } 18 | if (row) { 19 | row.parentNode!.removeChild(row); 20 | } 21 | }); 22 | }, 23 | }); 24 | })(); 25 | -------------------------------------------------------------------------------- /config/Migrations/20210728025420_AddDisplayNameToCalendarProvider.php: -------------------------------------------------------------------------------- 1 | table('calendar_providers'); 19 | $table->addColumn('display_name', 'string', [ 20 | 'null' => false, 21 | 'default' => '', 22 | 'after' => 'identifier', 23 | ]); 24 | $table->update(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /config/Migrations/20240509041300_AddSyncedToCalendarSources.php: -------------------------------------------------------------------------------- 1 | table('calendar_sources'); 18 | $table->addColumn('synced', 'boolean', [ 19 | // Before this all calendarsources were being synced. 20 | 'default' => true, 21 | 'null' => false, 22 | ]); 23 | $table->update(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /assets/sass/pages/feeds/discover.scss: -------------------------------------------------------------------------------- 1 | .discover-feeds { 2 | margin: calc($space * 3) 0; 3 | 4 | .empty { 5 | color: var(--color-muted); 6 | } 7 | 8 | } 9 | .discover-back { 10 | display: flex; 11 | gap: $space; 12 | align-items: center; 13 | } 14 | .discover-feeds.feed-list { 15 | display: flex; 16 | flex-direction: column; 17 | gap: $space; 18 | 19 | } 20 | .feed-list .feed-item { 21 | border-bottom: 1px solid var(--color-border); 22 | padding-bottom: calc($space * 2); 23 | 24 | &:last-child { 25 | border-bottom: none; 26 | padding-bottom: 0; 27 | margin-bottom: 0; 28 | } 29 | 30 | h4 { 31 | margin-bottom: calc($space / 2); 32 | } 33 | .url { 34 | color: var(--color-muted); 35 | } 36 | form { 37 | margin-top: $space; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /config/Migrations/20210228192141_FixActionOnTasksSectionId.php: -------------------------------------------------------------------------------- 1 | table('tasks') 19 | ->dropForeignKey('section_id') 20 | ->addForeignKey('section_id', 'project_sections', 'id', [ 21 | 'update' => 'NO_ACTION', 22 | 'delete' => 'SET_NULL', 23 | ]) 24 | ->update(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /plugins/Calendar/src/CalendarPlugin.php: -------------------------------------------------------------------------------- 1 | addServiceProvider(new CalendarServiceProvider()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /templates/Users/reset_password.php: -------------------------------------------------------------------------------- 1 | setLayout('card'); 5 | $this->assign('title', 'Forgot your password'); 6 | ?> 7 |

Forgot your password

8 |

We will send you an email with instructions to reset it.

9 | Form->create( 10 | null, 11 | [ 12 | 'class' => 'form-narrow', 13 | 'url' => ['controller' => 'Users', 'action' => 'resetPassword'], 14 | ], 15 | ) ?> 16 | Form->control('email', ['required' => true, 'type' => 'email']) ?> 17 |
18 | Form->submit('Reset Password', ['class' => 'button button-primary']) ?> 19 | Html->link('Login', ['_path' => 'Users::login'], ['class' => 'button button-muted']) ?> 20 |
21 | Form->end() ?> 22 | -------------------------------------------------------------------------------- /flutterapp/.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | migrate_working_dir/ 12 | tests.sqlite 13 | 14 | # IntelliJ related 15 | *.iml 16 | *.ipr 17 | *.iws 18 | .idea/ 19 | 20 | # Flutter/Dart/Pub related 21 | **/doc/api/ 22 | **/ios/Flutter/.last_build_id 23 | .dart_tool/ 24 | .flutter-plugins 25 | .flutter-plugins-dependencies 26 | .packages 27 | .pub-cache/ 28 | .pub/ 29 | /build/ 30 | /android/key.properties 31 | 32 | # Web related 33 | lib/generated_plugin_registrant.dart 34 | 35 | # Symbolication related 36 | app.*.symbols 37 | 38 | # Obfuscation related 39 | app.*.map.json 40 | 41 | # Android Studio will place build artifacts here 42 | /android/app/debug 43 | /android/app/profile 44 | /android/app/release 45 | -------------------------------------------------------------------------------- /config/schema/sessions.sql: -------------------------------------------------------------------------------- 1 | # Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org) 2 | # 3 | # Licensed under The MIT License 4 | # For full copyright and license information, please see the LICENSE.txt 5 | # Redistributions of files must retain the above copyright notice. 6 | # MIT License (https://opensource.org/licenses/mit-license.php) 7 | 8 | CREATE TABLE `sessions` ( 9 | `id` char(40) CHARACTER SET ascii COLLATE ascii_bin NOT NULL, 10 | `created` datetime DEFAULT CURRENT_TIMESTAMP, -- optional, requires MySQL 5.6.5+ 11 | `modified` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, -- optional, requires MySQL 5.6.5+ 12 | `data` blob DEFAULT NULL, -- for PostgreSQL use bytea instead of blob 13 | `expires` int(10) unsigned DEFAULT NULL, 14 | PRIMARY KEY (`id`) 15 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 16 | -------------------------------------------------------------------------------- /flutterapp/lib/db/profile.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_cache/json_cache.dart'; 2 | 3 | import 'package:docket/db/repository.dart'; 4 | import 'package:docket/models/userprofile.dart'; 5 | 6 | class ProfileRepo extends Repository { 7 | static const String name = 'userprofile'; 8 | 9 | ProfileRepo(JsonCache database, Duration duration) : super(database, duration); 10 | 11 | @override 12 | String keyName() { 13 | return 'v1:$name'; 14 | } 15 | 16 | /// Set completed tasks for a project into the lookup 17 | @override 18 | Future set(UserProfile token) async { 19 | return setMap(token.toMap()); 20 | } 21 | 22 | Future get() async { 23 | var data = await getMap(); 24 | if (data == null) { 25 | return null; 26 | } 27 | return UserProfile.fromMap(data); 28 | } 29 | } 30 | 31 | -------------------------------------------------------------------------------- /config/Migrations/20241007030938_ExpandFeedItems.php: -------------------------------------------------------------------------------- 1 | table('feed_items'); 18 | $table 19 | ->addColumn('guid', 'string', [ 20 | 'null' => false, 21 | ]) 22 | ->addColumn('url', 'string', [ 23 | 'null' => false, 24 | ]) 25 | ->addIndex(['feed_id', 'guid'], ['unique' => true]) 26 | ->save(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /flutterapp/android/app/src/main/java/io/flutter/app/FlutterMultiDexApplication.java: -------------------------------------------------------------------------------- 1 | // Generated file. 2 | // 3 | // If you wish to remove Flutter's multidex support, delete this entire file. 4 | // 5 | // Modifications to this file should be done in a copy under a different name 6 | // as this file may be regenerated. 7 | 8 | package io.flutter.app; 9 | 10 | import android.app.Application; 11 | import android.content.Context; 12 | import androidx.annotation.CallSuper; 13 | import androidx.multidex.MultiDex; 14 | 15 | /** 16 | * Extension of {@link android.app.Application}, adding multidex support. 17 | */ 18 | public class FlutterMultiDexApplication extends Application { 19 | @Override 20 | @CallSuper 21 | protected void attachBaseContext(Context base) { 22 | super.attachBaseContext(base); 23 | MultiDex.install(this); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /templates/email/html/default.php: -------------------------------------------------------------------------------- 1 | ' . $line . "

\n"; 21 | endforeach; 22 | -------------------------------------------------------------------------------- /assets/sass/components/profileMenu.scss: -------------------------------------------------------------------------------- 1 | .profile-menu-container { 2 | display: flex; 3 | gap: $space; 4 | align-items: center; 5 | margin: 0 0 calc($space * 3) calc($space / 2); 6 | 7 | .button-muted { 8 | border: 1px solid var(--color-border); 9 | 10 | &:hover { 11 | box-shadow: var(--shadow-button); 12 | } 13 | } 14 | } 15 | 16 | .profile-menu { 17 | display: block; 18 | 19 | .avatar, 20 | .avatar:hover { 21 | height: 50px; 22 | padding: 0; 23 | background: none; 24 | border: none; 25 | box-shadow: none; 26 | } 27 | 28 | img { 29 | border-radius: 50%; 30 | border: 2px solid var(--color-menu-selected); 31 | } 32 | .avatar:hover img { 33 | border-color: var(--color-secondary); 34 | } 35 | 36 | .dropdown-menu .dropdown { 37 | top: 60px; 38 | left: -2px; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /assets/sass/pages/tasks/add.scss: -------------------------------------------------------------------------------- 1 | // Page specific CSS for task add modal. 2 | // TODO all of this can be deleted! 3 | .sheet-overlay { 4 | position: fixed; 5 | top: 0px; 6 | bottom: 0px; 7 | left: 0px; 8 | right: 0px; 9 | background: rgba(66, 66, 66, 0.3); 10 | 11 | display: flex; 12 | justify-content: center; 13 | } 14 | .sheet-body { 15 | background: #fff; 16 | margin-top: 2vh; 17 | width: 85%; 18 | max-width: 850px; 19 | bottom: auto; 20 | padding: calc($space * 3) calc($space * 6); 21 | overflow-y: auto; 22 | z-index: $z-modal; 23 | 24 | border-radius: $border-radius-large; 25 | border: none; 26 | box-shadow: var(--shadow-high); 27 | } 28 | 29 | @media (max-width: $breakpoint-phone) { 30 | .sheet-body { 31 | width: 95%; 32 | margin-left: calc($space * 2); 33 | margin-right: calc($space * 2); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /plugins/Calendar/src/Policy/CalendarItemsTablePolicy.php: -------------------------------------------------------------------------------- 1 | fetchTable('Calendar.CalendarSources'); 20 | 21 | $sourceQuery = $sources 22 | ->subquery() 23 | ->select(['CalendarSources.id']) 24 | ->innerJoinWith('CalendarProviders') 25 | ->where(['CalendarProviders.user_id' => $user->id]); 26 | 27 | return $query->where(['CalendarItems.calendar_source_id IN' => $sourceQuery]); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /assets/sass/components/markdownText.scss: -------------------------------------------------------------------------------- 1 | markdown-text { 2 | width: 100%; 3 | padding-bottom: calc($space * 3); 4 | 5 | textarea { 6 | height: auto; 7 | max-width: 100%; 8 | width: calc(100% - $form-icon-input-margin); 9 | resize: none; 10 | 11 | margin-top: $space; 12 | min-height: 36px; 13 | } 14 | } 15 | 16 | @media (max-width: $breakpoint-phone) { 17 | markdown-text { 18 | padding-bottom: $space; 19 | } 20 | } 21 | 22 | .markdown-text-preview, 23 | .markdown-text { 24 | h1, h2, h3 { 25 | color: var(--color-low-emphasis); 26 | } 27 | ul, ol { 28 | margin-left: 0; 29 | padding-left: calc($space * 2); 30 | } 31 | } 32 | 33 | .markdown-text-preview { 34 | position: relative; 35 | 36 | .button-focusreveal { 37 | position: absolute; 38 | right: $space; 39 | bottom: -24px; 40 | } 41 | } 42 | 43 | -------------------------------------------------------------------------------- /flutterapp/lib/components/projectbadge.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'package:docket/theme.dart'; 4 | 5 | class ProjectBadge extends StatelessWidget { 6 | final String text; 7 | final int color; 8 | final bool isActive; 9 | 10 | const ProjectBadge({required this.text, required this.color, this.isActive = false, super.key}); 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | var theme = Theme.of(context); 15 | var docketColors = theme.extension()!; 16 | var projectColor = getProjectColor(color); 17 | var icon = isActive ? Icons.star : Icons.circle; 18 | 19 | return Wrap(spacing: space(1.1), children: [ 20 | Icon(icon, color: projectColor, size: 14), 21 | Text( 22 | text, 23 | style: TextStyle(color: docketColors.secondaryText), 24 | ), 25 | ]); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /bin/server.js: -------------------------------------------------------------------------------- 1 | const {spawn} = require('child_process'); 2 | 3 | const servers = [ 4 | {prefix: 'php', command: 'bin/cake', args: ['server']}, 5 | {prefix: 'react', command: 'npm', args: ['run', 'dev']}, 6 | ]; 7 | 8 | const processes = servers.map(command => { 9 | return { 10 | ...command, 11 | process: spawn(command.command, command.args), 12 | }; 13 | }); 14 | 15 | processes.forEach(({prefix, process}) => { 16 | process.stdout.on('data', data => { 17 | console.log(`${prefix} | ${data}`.trim()); 18 | }); 19 | process.stderr.on('data', data => { 20 | console.log(`${prefix} | ERR | ${data}`.trim()); 21 | }); 22 | }); 23 | 24 | process.on('SIGINT', () => { 25 | console.log('Killing all processes..'); 26 | processes.forEach(({command, process}) => { 27 | console.log(`Killing ${command}`); 28 | process.kill(); 29 | }); 30 | process.exit(2); 31 | }); 32 | -------------------------------------------------------------------------------- /assets/sass/components/paginator.scss: -------------------------------------------------------------------------------- 1 | .paginator { 2 | display: flex; 3 | align-items: center; 4 | gap: calc($space * 2); 5 | 6 | p { 7 | color: var(--color-gray-500); 8 | font-size: $font-size-small; 9 | margin-bottom: 0; 10 | } 11 | } 12 | .pagination { 13 | list-style: none; 14 | margin: calc($space * 2) 0; 15 | padding: 0; 16 | display: flex; 17 | gap: $space; 18 | align-items: center; 19 | 20 | li { 21 | white-space: nowrap; 22 | } 23 | 24 | .disabled a { 25 | pointer-events: none; 26 | color: var(--color-muted); 27 | } 28 | 29 | a { 30 | @extend .button-muted; 31 | } 32 | .active a { 33 | @extend .button-secondary; 34 | } 35 | } 36 | 37 | @media (max-width: $breakpoint-phone) { 38 | .pagination li { 39 | display: none; 40 | } 41 | li.active, 42 | li.next, 43 | li.prev { 44 | display: block; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tests/Fixture/vcr/googleoauth_callback_invalid.yml: -------------------------------------------------------------------------------- 1 | - 2 | request: 3 | method: GET 4 | url: 'https://www.googleapis.com/oauth2/v2/userinfo' 5 | headers: 6 | Content-Type: application/json 7 | Authorization: 'Bearer goog-access-token' 8 | response: 9 | status: 10 | http_version: '1.1' 11 | code: '400' 12 | message: 'Ok' 13 | headers: 14 | Cache-Control: 'no-cache, no-store, max-age=0, must-revalidate' 15 | Content-Type: 'application/json; charset=utf-8' 16 | body: > 17 | { 18 | "error": { 19 | "code": 400, 20 | "message": "Invalid access token", 21 | "errors": [ 22 | {"domain": "calendar", "reason": "invalid", "message": "Not good"} 23 | ] 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import {defineConfig} from 'vite'; 2 | import path from 'path'; 3 | 4 | const projectRootDir = path.resolve(__dirname); 5 | 6 | export default defineConfig({ 7 | plugins: [], 8 | build: { 9 | emptyOutDir: false, 10 | outDir: './webroot/', 11 | 12 | // make a manifest and source maps. 13 | manifest: true, 14 | sourcemap: true, 15 | 16 | rollupOptions: { 17 | // Use a custom non-html entry point 18 | input: path.resolve(projectRootDir, './assets/js/app.tsx'), 19 | }, 20 | }, 21 | resolve: { 22 | alias: [ 23 | { 24 | find: 'app', 25 | replacement: path.resolve(projectRootDir, './assets/js'), 26 | }, 27 | ], 28 | }, 29 | esbuild: {}, 30 | optimizeDeps: { 31 | include: [], 32 | }, 33 | server: { 34 | watch: { 35 | ignored: ['**/vendor/**', '**/flutterapp/**'], 36 | }, 37 | }, 38 | }); 39 | -------------------------------------------------------------------------------- /flutterapp/lib/dialogs/confirmdelete.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | void showConfirmDelete({ 4 | required BuildContext context, 5 | required void Function () onConfirm, 6 | String title = 'Are you sure?', 7 | String content = 'Are you sure you want to proceed?', 8 | String cancelButtonText = 'Cancel', 9 | String confirmButtonText = 'Yes', 10 | }) { 11 | showDialog( 12 | context: context, 13 | builder: (BuildContext context) { 14 | return AlertDialog( 15 | title: Text(title), 16 | content: Text(content), 17 | actions: [ 18 | TextButton( 19 | onPressed: onConfirm, 20 | child: Text(confirmButtonText), 21 | ), 22 | ElevatedButton(child: Text(cancelButtonText), onPressed: () { 23 | Navigator.pop(context); 24 | }), 25 | ] 26 | ); 27 | } 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /flutterapp/ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 11.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /plugins/Tasks/templates/Tasks/add.php: -------------------------------------------------------------------------------- 1 | request->is('htmx'); 10 | 11 | $this->setLayout('sidebar'); 12 | if ($isHtmx) { 13 | $this->setLayout('sheet'); 14 | } 15 | 16 | $this->assign('title', 'New Task'); 17 | ?> 18 |
19 | 23 | element('Tasks.task_form', [ 24 | 'task' => $task, 25 | 'projects' => $projects, 26 | 'sections' => $sections, 27 | 'referer' => $referer, 28 | 'url' => ['_name' => 'tasks:add'], 29 | ]); ?> 30 |
31 | -------------------------------------------------------------------------------- /plugins/Tasks/templates/Projects/add.php: -------------------------------------------------------------------------------- 1 | setLayout('sidebar'); 11 | $this->assign('title', 'New Project'); 12 | ?> 13 |

New Project

14 | Form->create( 16 | $project, 17 | ['class' => 'form-narrow'] 18 | ); 19 | echo $this->Form->hidden('referer', ['value' => $referer]); 20 | echo $this->Form->control('name', ['autofocus' => true]); 21 | echo $this->Form->control('color', [ 22 | 'type' => 'colorpicker', 23 | 'colors' => Configure::read('Colors'), 24 | ]); 25 | ?> 26 |
27 | Form->submit('Save', ['class' => 'button button-primary']) ?> 28 | 29 | Cancel 30 | 31 |
32 | Form->end() ?> 33 | -------------------------------------------------------------------------------- /plugins/Tasks/templates/element/tasks_empty.php: -------------------------------------------------------------------------------- 1 | get('globalAddContext') ?? []; 5 | ?> 6 |
7 | 8 | element('icons/trophy16', ['size' => 48]) ?> 9 | 10 |

All done

11 |

Congratulations! Create a task for what is next.

12 |

13 | Html->link( 14 | $this->element('icons/plus16') . 'Create a Task', 15 | ['_name' => 'tasks:add', '?' => $createParams], 16 | [ 17 | 'escape' => false, 18 | 'class' => 'button-primary', 19 | 'hx-get' => $this->Url->build(['_name' => 'tasks:add', '?' => $createParams]), 20 | 'hx-target' => 'main.main', 21 | 'hx-swap' => 'beforeend', 22 | ] 23 | ) ?> 24 |

25 |
26 | -------------------------------------------------------------------------------- /flutterapp/lib/db/trashbin.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_cache/json_cache.dart'; 2 | 3 | import 'package:docket/db/repository.dart'; 4 | import 'package:docket/models/task.dart'; 5 | 6 | class TrashbinRepo extends Repository { 7 | static const String name = 'trashbin'; 8 | 9 | TrashbinRepo(JsonCache database, Duration duration) : super(database, duration); 10 | 11 | @override 12 | String keyName() { 13 | return 'v1:$name'; 14 | } 15 | 16 | /// Refresh the data stored for the 'today' view. 17 | @override 18 | Future set(TaskViewData tasks) async { 19 | return setMap(tasks.toMap()); 20 | } 21 | 22 | Future get() async { 23 | var data = await getMap(); 24 | // Likely loading. 25 | if (data == null || data['tasks'] == null) { 26 | return TaskViewData(isEmpty: true, tasks: [], calendarItems: []); 27 | } 28 | return TaskViewData.fromMap(data); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /templates/layout/email/html/default.php: -------------------------------------------------------------------------------- 1 | 17 | 18 | 19 | 20 | <?= $this->fetch('title') ?> 21 | 22 | 23 | fetch('content') ?> 24 | 25 | 26 | -------------------------------------------------------------------------------- /templates/element/icons/sun16.php: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /plugins/Tasks/templates/element/project_item.php: -------------------------------------------------------------------------------- 1 | $project 5 | * @var bool|null $showMenu 6 | * @var string $url 7 | */ 8 | $url = $this->Url->build(['_name' => 'projects:view', 'slug' => $project->slug]); 9 | 10 | $active = strpos($this->request->getPath(), $url) !== false; 11 | ?> 12 |
13 | 14 | 15 | element('icons/dot16', ['color' => $project->color_hex]) ?> 16 | name) ?> 17 | 18 | 19 | element('Tasks.project_menu', ['project' => $project]); 22 | else : ?> 23 | incomplete_task_count) ?> 24 | 25 |
26 | -------------------------------------------------------------------------------- /assets/js/app.tsx: -------------------------------------------------------------------------------- 1 | import 'vite/modulepreload-polyfill'; 2 | 3 | import '../sass/app.scss'; 4 | 5 | // Htmx setup 6 | import htmx from 'htmx.org'; 7 | 8 | // Expose htmx on window 9 | // @ts-ignore-next-line 10 | window.htmx = htmx; 11 | 12 | // htmx extensions 13 | import 'app/extensions/ajax'; 14 | import 'app/extensions/flashMessage'; 15 | import 'app/extensions/hotkeys'; 16 | import 'app/extensions/projectSorter'; 17 | import 'app/extensions/taskSorter'; 18 | import 'app/extensions/sectionSorter'; 19 | import 'app/extensions/subtaskSorter'; 20 | import 'app/extensions/removeRow'; 21 | 22 | // Webcomponents 23 | import 'app/webcomponents/dropDown.ts'; 24 | import 'app/webcomponents/dueOn.ts'; 25 | import 'app/webcomponents/keyboardList'; 26 | import 'app/webcomponents/markdownText.ts'; 27 | import 'app/webcomponents/modalWindow.ts'; 28 | import 'app/webcomponents/reloadAfter.ts'; 29 | import 'app/webcomponents/selectBox.ts'; 30 | import 'app/webcomponents/sideBar.ts'; 31 | -------------------------------------------------------------------------------- /tests/js/fixtures.ts: -------------------------------------------------------------------------------- 1 | import {Task, Project} from 'app/types'; 2 | 3 | export function makeTask(props?: Partial): Task { 4 | const defaults: Task = { 5 | id: 1, 6 | section_id: null, 7 | title: '', 8 | body: '', 9 | completed: false, 10 | evening: false, 11 | due_on: null, 12 | day_order: 0, 13 | child_order: 0, 14 | subtask_count: 0, 15 | complete_subtask_count: 0, 16 | created: '', 17 | modified: '', 18 | project: makeProject(props?.project ?? {}), 19 | }; 20 | return { 21 | ...defaults, 22 | ...props, 23 | }; 24 | } 25 | 26 | export function makeProject(props?: Partial): Project { 27 | const defaults: Project = { 28 | id: 1, 29 | name: 'Work', 30 | slug: 'work', 31 | color: 0, 32 | favorite: false, 33 | archived: false, 34 | incomplete_task_count: 0, 35 | sections: [], 36 | }; 37 | return { 38 | ...defaults, 39 | ...props, 40 | }; 41 | } 42 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /*eslint-env node*/ 2 | module.exports = { 3 | verbose: false, 4 | collectCoverageFrom: [ 5 | 'tests/js/spec/**/*.{js,jsx,tsx}', 6 | 'assets/js/**/*.{js,jsx,ts,tsx}', 7 | ], 8 | coverageReporters: ['html', 'cobertura'], 9 | coverageDirectory: '.artifacts/coverage', 10 | moduleNameMapper: { 11 | '\\.(css|less|png|jpg|mp4|svg)$': '/tests/js/__mocks__/styleMock.js', 12 | '^app(.*)$': '/assets/js$1', 13 | '^tests(.*)$': '/tests/js$1', 14 | }, 15 | modulePaths: ['/assets/js'], 16 | testMatch: ['/tests/js/**/*(*.)@(spec|test).(js|ts)?(x)'], 17 | 18 | unmockedModulePathPatterns: ['/node_modules/react'], 19 | transform: { 20 | '^.+\\.jsx?$': 'babel-jest', 21 | '^.+\\.tsx?$': 'babel-jest', 22 | }, 23 | moduleFileExtensions: ['js', 'ts', 'jsx', 'tsx'], 24 | globals: {}, 25 | setupFiles: ['/tests/js/setup.ts'], 26 | 27 | reporters: ['default'], 28 | }; 29 | -------------------------------------------------------------------------------- /bin/cake.bat: -------------------------------------------------------------------------------- 1 | :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: 2 | :: 3 | :: Cake is a Windows batch script for invoking CakePHP shell commands 4 | :: 5 | :: CakePHP(tm) : Rapid Development Framework (https://cakephp.org) 6 | :: Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org) 7 | :: 8 | :: Licensed under The MIT License 9 | :: Redistributions of files must retain the above copyright notice. 10 | :: 11 | :: @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org) 12 | :: @link https://cakephp.org CakePHP(tm) Project 13 | :: @since 2.0.0 14 | :: @license https://opensource.org/licenses/mit-license.php MIT License 15 | :: 16 | :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: 17 | 18 | @echo off 19 | 20 | SET app=%0 21 | SET lib=%~dp0 22 | 23 | php "%lib%cake.php" %* 24 | 25 | echo. 26 | 27 | exit /B %ERRORLEVEL% 28 | -------------------------------------------------------------------------------- /flutterapp/lib/viewmodels/login.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'package:docket/actions.dart' as actions; 4 | import 'package:docket/database.dart'; 5 | 6 | class LoginViewModel extends ChangeNotifier { 7 | late LocalDatabase _database; 8 | 9 | String? _loginError; 10 | 11 | bool get hasToken => _database.apiToken.hasToken; 12 | 13 | String? get loginError => _loginError; 14 | 15 | LoginViewModel(LocalDatabase database) { 16 | _database = database; 17 | } 18 | 19 | Future login(String? email, String? password) async { 20 | if (email == null || password == null) { 21 | _loginError = 'E-mail and password are required'; 22 | return; 23 | } 24 | 25 | try { 26 | var apiToken = await actions.doLogin(email, password); 27 | await _database.apiToken.set(apiToken); 28 | } catch (e) { 29 | _loginError = 'Authentication failed.'; 30 | } 31 | 32 | notifyListeners(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /assets/js/extensions/flashMessage.ts: -------------------------------------------------------------------------------- 1 | import htmx from 'htmx.org'; 2 | 3 | (function () { 4 | function startTimer(element: HTMLElement, duration: number) { 5 | const timerId = setTimeout(function () { 6 | element.dataset.state = 'hidden'; 7 | }, duration); 8 | element.dataset.timer = String(timerId); 9 | } 10 | 11 | htmx.defineExtension('flash-message', { 12 | onEvent: function (name, event) { 13 | if (name !== 'htmx:afterProcessNode') { 14 | return; 15 | } 16 | const element = event.target as HTMLElement; 17 | element.addEventListener('mouseleave', function () { 18 | clearTimeout(Number(element.dataset.timer)); 19 | startTimer(element, 1500); 20 | }); 21 | 22 | element.addEventListener('mouseenter', function () { 23 | clearTimeout(Number(element.dataset.timer)); 24 | }); 25 | 26 | // Setup initial timeout 27 | startTimer(element, 4000); 28 | }, 29 | }); 30 | })(); 31 | -------------------------------------------------------------------------------- /plugins/Tasks/src/View/Cell/ProjectsMenuCell.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | protected array $_validCellOptions = ['identity']; 21 | 22 | /** 23 | * @var \App\Model\Entity\User 24 | */ 25 | protected User $identity; 26 | 27 | /** 28 | * Default display method. 29 | * 30 | * @return void 31 | */ 32 | public function display($identity): void 33 | { 34 | $projects = $this->fetchTable('Tasks.Projects'); 35 | 36 | $query = $identity->applyScope('index', $projects->find('active')->find('top')); 37 | $this->set('projects', $query->all()); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /plugins/Feeds/src/View/Cell/FeedCategoryMenuCell.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | protected array $_validCellOptions = ['identity']; 21 | 22 | /** 23 | * @var \App\Model\Entity\User 24 | */ 25 | protected User $identity; 26 | 27 | /** 28 | * Default display method. 29 | * 30 | * @return void 31 | */ 32 | public function display($identity): void 33 | { 34 | $categories = $this->fetchTable('Feeds.FeedCategories'); 35 | 36 | $query = $identity->applyScope('index', $categories->find('menu')); 37 | $this->set('feedCategories', $query->all()); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /plugins/Feeds/templates/FeedCategories/edit.php: -------------------------------------------------------------------------------- 1 | request->is('htmx'); 9 | 10 | $this->setLayout('sidebar'); 11 | if ($isHtmx) { 12 | $this->set('closable', true); 13 | $this->setLayout('modal'); 14 | } 15 | 16 | ?> 17 | 21 | Form->create($feedCategory); 23 | echo $this->Form->control('color', [ 24 | 'type' => 'colorpicker', 25 | 'colors' => Configure::read('Colors'), 26 | ]); 27 | echo $this->Form->control('title'); 28 | ?> 29 |
30 | Form->submit('Save', ['class' => 'button button-primary']) ?> 31 | 32 | Cancel 33 | 34 |
35 | -------------------------------------------------------------------------------- /plugins/Calendar/templates/element/calendaritems.php: -------------------------------------------------------------------------------- 1 | 8 |
9 | 10 | all_day; 13 | $startTime = $item->getFormattedTime($identity->timezone); 14 | if ($startTime) : 15 | $start = ''; 16 | endif; 17 | $class = 'calendar-item'; 18 | if ($allDay) : 19 | $class .= ' all-day'; 20 | endif; 21 | $style = '--calendar-color: ' . h($item->color_hex) . ';'; 22 | ?> 23 |
24 | 25 | 26 | title) ?> 27 | 28 |
29 | 30 |
31 | -------------------------------------------------------------------------------- /plugins/Tasks/templates/cell/ProjectsMenu/display.php: -------------------------------------------------------------------------------- 1 | $projects 5 | */ 6 | ?> 7 | Form->create( 8 | null, 9 | [ 10 | 'class' => 'dnd-dropper-left-offset', 11 | 'hx-ext' => 'project-sorter', 12 | 'hx-trigger' => 'end', 13 | 'hx-post' => $this->Url->build(['_name' => 'projects:reorder']), 14 | 'hx-swap' => 'outerHTML', 15 | ] 16 | ) ?> 17 | 18 | 19 |
20 | Form->hidden('id[]', ['value' => $project->id]) ?> 21 | 24 | element('Tasks.project_item', ['project' => $project]) ?> 25 |
26 | 27 | Form->end() ?> 28 | -------------------------------------------------------------------------------- /src/Middleware/ApiCsrfProtectionMiddleware.php: -------------------------------------------------------------------------------- 1 | getHeaderLine('Authorization'); 21 | if ($authorization && strtolower(substr($authorization, 0, 6)) === 'bearer') { 22 | return $handler->handle($request); 23 | } 24 | 25 | return parent::process($request, $handler); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /flutterapp/lib/db/projectarchive.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_cache/json_cache.dart'; 2 | 3 | import 'package:docket/db/repository.dart'; 4 | import 'package:docket/models/project.dart'; 5 | 6 | class ProjectArchiveRepo extends Repository> { 7 | static const String name = 'projectarchive'; 8 | 9 | ProjectArchiveRepo(JsonCache database, Duration duration) : super(database, duration); 10 | 11 | @override 12 | String keyName() { 13 | return 'v1:$name'; 14 | } 15 | 16 | /// Refresh the data stored for the 'upcoming' view. 17 | @override 18 | Future set(List data) async { 19 | return setMap({'projects': data.map((project) => project.toMap()).toList()}); 20 | } 21 | 22 | Future?> get() async { 23 | var data = await getMap(); 24 | // Likely loading. 25 | if (data == null || data['projects'] == null) { 26 | return null; 27 | } 28 | 29 | return (data['projects'] as List).map((item) => Project.fromMap(item)).toList(); 30 | } 31 | } 32 | 33 | -------------------------------------------------------------------------------- /plugins/Feeds/templates/FeedCategories/add.php: -------------------------------------------------------------------------------- 1 | request->is('htmx'); 9 | 10 | $this->setLayout('Feeds.feedreader'); 11 | if ($isHtmx) { 12 | $this->setLayout('sheet'); 13 | } 14 | 15 | $this->assign('title', 'Create Category'); 16 | ?> 17 | 21 | Form->create($feedCategory, ['class' => 'form-narrow']); 23 | echo $this->Form->control('color', [ 24 | 'type' => 'colorpicker', 25 | 'colors' => Configure::read('Colors'), 26 | ]); 27 | echo $this->Form->control('title'); 28 | ?> 29 |
30 | Form->submit('Save', ['class' => 'button button-primary']) ?> 31 | 32 | Cancel 33 | 34 |
35 | -------------------------------------------------------------------------------- /flutterapp/test/theme_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | 4 | import 'package:docket/theme.dart'; 5 | 6 | void main() { 7 | TestWidgetsFlutterBinding.ensureInitialized(); 8 | 9 | group('theme.DocketColors', () { 10 | // These tests aren't meant to be exhaustive. 11 | // We're just aiming to get coverage, as the application doesn't 12 | // actually use these methods but flutter requires them to be implemented. 13 | test('copyWith()', () { 14 | var light = DocketColors.light; 15 | var updated = light.copyWith(actionEdit: Colors.purple); 16 | 17 | expect(light.actionEdit, isNot(equals(Colors.purple))); 18 | expect(updated.actionEdit, equals(Colors.purple)); 19 | }); 20 | 21 | test('lerp()', () { 22 | var light = DocketColors.light; 23 | var dark = DocketColors.dark; 24 | var updated = light.lerp(dark, 0.0); 25 | 26 | expect(updated.disabledText, equals(light.disabledText)); 27 | }); 28 | }); 29 | } 30 | -------------------------------------------------------------------------------- /plugins/Feeds/templates/FeedSubscriptions/add.php: -------------------------------------------------------------------------------- 1 | request->is('htmx'); 7 | 8 | $this->setLayout('sidebar'); 9 | if ($isHtmx) { 10 | $this->setLayout('sheet'); 11 | } 12 | 13 | $this->assign('title', 'Add Feed'); 14 | ?> 15 | 19 | Form->create($feedSubscription); 21 | echo $this->Form->control('url', ['label' => 'Feed URL']); 22 | echo $this->Form->control('alias', ['label' => 'Name']); 23 | echo $this->Form->control('feed_category_id', [ 24 | 'options' => $feedCategories, 25 | ]); 26 | ?> 27 |
28 | Form->submit('Save', ['class' => 'button button-primary']) ?> 29 | 30 | Cancel 31 | 32 |
33 | Form->end() ?> 34 | -------------------------------------------------------------------------------- /config/Migrations/20241207223935_AddUnreadItemCountToFeedSubscriptions.php: -------------------------------------------------------------------------------- 1 | table('feed_subscriptions'); 18 | $table->addColumn('unread_item_count', 'integer', [ 19 | 'default' => 0, 20 | 'limit' => 11, 21 | 'null' => false, 22 | ]); 23 | $table->update(); 24 | 25 | $table = $this->table('feed_categories'); 26 | $table->addColumn('unread_item_count', 'integer', [ 27 | 'default' => 0, 28 | 'limit' => 11, 29 | 'null' => false, 30 | ]); 31 | $table->update(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /phpcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | *tests/js/* 4 | 5 | 6 | 7 | 0 8 | 9 | 10 | 0 11 | 12 | 13 | 0 14 | 15 | 16 | 0 17 | 18 | 19 | 0 20 | 21 | 22 | 0 23 | 24 | 25 | 0 26 | 27 | 28 | -------------------------------------------------------------------------------- /plugins/Tasks/templates/Projects/edit.php: -------------------------------------------------------------------------------- 1 | setLayout('sidebar'); 11 | $this->assign('title', 'Edit Project'); 12 | ?> 13 |

Edit name) ?> Project

14 | Form->create($project, ['class' => 'form-narrow']); 16 | echo $this->Form->hidden('referer', ['value' => $referer]); 17 | echo $this->Form->control('name', ['autofocus' => true]); 18 | echo $this->Form->control('color', [ 19 | 'type' => 'colorpicker', 20 | 'colors' => Configure::read('Colors'), 21 | ]); 22 | echo $this->Form->control('archived', [ 23 | 'type' => 'checkbox', 24 | 'nestedInput' => false, 25 | ]); 26 | ?> 27 |
28 | Form->submit('Save', ['class' => 'button button-primary']) ?> 29 | 30 | Cancel 31 | 32 |
33 | Form->end() ?> 34 | -------------------------------------------------------------------------------- /assets/sass/components/calendarItemList.scss: -------------------------------------------------------------------------------- 1 | .calendar-item-list { 2 | padding: $space; 3 | background: var(--color-bg-level1); 4 | border-radius: $border-radius; 5 | margin-bottom: calc($space * 1.5); 6 | } 7 | 8 | .calendar-item { 9 | // Expects a `--calendar-color` CSS property to be set. 10 | display: flex; 11 | align-items: center; 12 | margin-bottom: $space; 13 | 14 | &:last-child { 15 | margin-bottom: 0; 16 | } 17 | 18 | &.all-day:before { 19 | display: inline-block; 20 | content: ''; 21 | border-radius: $border-radius; 22 | background: var(--calendar-color); 23 | width: 4px; 24 | height: 1.2em; 25 | margin-right: calc($space / 2); 26 | } 27 | 28 | time { 29 | color: var(--calendar-color); 30 | font-weight: 500; 31 | font-variant-numeric: tabular-nums; 32 | margin-right: calc($space / 2); 33 | } 34 | 35 | a { 36 | color: var(--color-fg); 37 | text-decoration: none; 38 | } 39 | 40 | a:hover { 41 | color: var(--color-primary); 42 | text-decoration: underline; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /tests/Fixture/vcr/controller_calendarsources_add_auth_fail.yml: -------------------------------------------------------------------------------- 1 | 2 | - 3 | request: 4 | method: GET 5 | url: 'https://www.googleapis.com/calendar/v3/users/me/calendarList' 6 | headers: 7 | Host: www.googleapis.com 8 | Content-Type: application/json 9 | User-Agent: 'Docket Calendar Sync google-api-php-client/2.10.1' 10 | Authorization: 'Bearer calendar-access-token' 11 | response: 12 | status: 13 | http_version: '1.1' 14 | code: '401' 15 | message: Ok 16 | headers: 17 | Vary: [Origin, X-Origin, Referer] 18 | Content-Type: 'application/json; charset=UTF-8' 19 | Date: 'Mon, 12 Jul 2021 02:54:22 GMT' 20 | body: > 21 | { 22 | "error": { 23 | "code": 401, 24 | "message": "Authentication failed", 25 | "errors": [ 26 | {"domain": "calendar", "reason": "invalid", "message": "Not good"} 27 | ] 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /plugins/Calendar/templates/CalendarProviders/index.php: -------------------------------------------------------------------------------- 1 | setLayout('sidebar'); 9 | $this->assign('title', 'Synced Calendars'); 10 | ?> 11 | 18 |

19 | Events from linked calendars will be displayed in "today" and "upcoming" views. 20 |

21 |

Connected Calendar Accounts

22 |
    23 | 24 | element( 25 | 'Calendar.calendarprovider_item', 26 | [ 27 | 'provider' => $provider, 28 | ] 29 | ); 30 | ?> 31 | 32 |
33 | -------------------------------------------------------------------------------- /assets/js/webcomponents/reloadAfter.ts: -------------------------------------------------------------------------------- 1 | class ReloadAfter extends HTMLElement { 2 | private timeoutId: number | undefined; 3 | 4 | connectedCallback() { 5 | const attrValue = this.getAttribute('timestamp'); 6 | let deadline = Number(attrValue); 7 | if (isNaN(deadline)) { 8 | console.error(`Invalid 'timestamp' attribute value ${attrValue}`); 9 | return; 10 | } 11 | // Seconds -> milliseconds 12 | deadline = deadline * 1000; 13 | 14 | const currentTime = Date.now(); 15 | const delay = deadline - currentTime; 16 | this.timeoutId = setTimeout(function () { 17 | console.log( 18 | `Triggering reload as current time is ${delay} seconds after ${currentTime}` 19 | ); 20 | window.location.reload(); 21 | }, delay); 22 | } 23 | 24 | disconnectedCallback() { 25 | const timeout = this.timeoutId; 26 | if (timeout) { 27 | clearTimeout(timeout); 28 | this.timeoutId = undefined; 29 | } 30 | } 31 | } 32 | 33 | customElements.define('reload-after', ReloadAfter); 34 | 35 | export default ReloadAfter; 36 | -------------------------------------------------------------------------------- /templates/Error/error400.php: -------------------------------------------------------------------------------- 1 | setLayout('error'); 9 | 10 | if (Configure::read('debug')) : 11 | $this->setLayout('dev_error'); 12 | 13 | $this->assign('title', $message); 14 | $this->assign('templateName', 'error400.php'); 15 | 16 | $this->start('file'); 17 | ?> 18 | queryString)) : ?> 19 |

20 | SQL Query: 21 | queryString) ?> 22 |

23 | 24 | params)) : ?> 25 | SQL Query Params: 26 | params) ?> 27 | 28 | element('auto_table_warning') ?> 29 | end(); 32 | endif; 33 | ?> 34 |

35 |

36 | : 37 | '{$url}'") ?> 38 |

39 | -------------------------------------------------------------------------------- /flutterapp/test_resources/tasks_today.json: -------------------------------------------------------------------------------- 1 | { 2 | "tasks": [ 3 | { 4 | "id": 1, 5 | "project": {"id": 1, "slug": "home", "name": "home", "color": 1}, 6 | "title": "clean dishes", 7 | "body": "", 8 | "evening": false, 9 | "completed": false, 10 | "due_on": "__TODAY__", 11 | "child_order": 0, 12 | "day_order": 0 13 | }, 14 | { 15 | "id": 2, 16 | "project": {"id": 1, "slug": "home", "name": "home", "color": 1}, 17 | "title": "cut grass", 18 | "body": "", 19 | "evening": false, 20 | "completed": false, 21 | "due_on": "__TODAY__", 22 | "child_order": 1, 23 | "day_order": 1 24 | } 25 | ], 26 | "calendarItems": [ 27 | { 28 | "id": 1, 29 | "calendar_source_id": 1, 30 | "provider_id": "google", 31 | "color": 1, 32 | "title": "Get haircut", 33 | "start_time": null, 34 | "end_time": null, 35 | "start_date": "__TODAY__", 36 | "end_date": "__TODAY__", 37 | "all_day": true, 38 | "html_link": "" 39 | } 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /assets/sass/components/flashMessage.scss: -------------------------------------------------------------------------------- 1 | @keyframes flash-message-append { 2 | from { 3 | top: -60px; 4 | } 5 | to { 6 | top: 16px; 7 | } 8 | } 9 | .flash-messages { 10 | display: flex; 11 | flex-direction: column; 12 | gap: $space; 13 | 14 | position: fixed; 15 | left: 50%; 16 | top: 16px; 17 | width: max-content; 18 | transform: translateX(-50%); 19 | z-index: $z-flash; 20 | } 21 | 22 | .flash-message { 23 | animation: 0.5s ease-out 0s 1 flash-message-append; 24 | 25 | padding: $space * 2; 26 | background: var(--color-bg); 27 | box-shadow: var(--shadow-med); 28 | border-radius: $border-radius; 29 | 30 | svg { 31 | margin-right: $space; 32 | vertical-align: -0.225em; 33 | } 34 | 35 | transition: top 0.3s, opacity 0.3s; 36 | 37 | &[data-state="visible"] { 38 | top: 16px; 39 | } 40 | &[data-state="hidden"] { 41 | top: -60px; 42 | opacity: 0; 43 | display: none; 44 | } 45 | } 46 | 47 | .flash-success svg { 48 | color: var(--color-success-fg); 49 | } 50 | .flash-error svg { 51 | color: var(--color-error-fg); 52 | } 53 | 54 | -------------------------------------------------------------------------------- /flutterapp/test_resources/tasks_upcoming.json: -------------------------------------------------------------------------------- 1 | { 2 | "tasks": [ 3 | { 4 | "id": 1, 5 | "project": {"id": 1, "slug": "home", "name": "home", "color": 1}, 6 | "title": "clean dishes", 7 | "body": "", 8 | "evening": false, 9 | "completed": false, 10 | "due_on": "__TODAY__", 11 | "child_order": 0, 12 | "day_order": 0 13 | }, 14 | { 15 | "id": 2, 16 | "project": {"id": 1, "slug": "home", "name": "home", "color": 1}, 17 | "title": "cut grass", 18 | "body": "", 19 | "evening": false, 20 | "completed": false, 21 | "due_on": "__TOMORROW__", 22 | "child_order": 1, 23 | "day_order": 1 24 | } 25 | ], 26 | "calendarItems": [ 27 | { 28 | "id": 1, 29 | "calendar_source_id": 1, 30 | "provider_id": "google", 31 | "color": 1, 32 | "title": "Get haircut", 33 | "start_time": null, 34 | "end_time": null, 35 | "start_date": "__TODAY__", 36 | "end_date": "__TODAY__", 37 | "all_day": true, 38 | "html_link": "" 39 | } 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /templates/element/confirm_dialog.php: -------------------------------------------------------------------------------- 1 | setLayout('modal'); 13 | 14 | $this->set('closable', false); 15 | $this->set('dialogOptions', [ 16 | 'class' => 'confirm-dialog', 17 | 'data-testid' => 'confirm-dialog', 18 | ]); 19 | ?> 20 | Form->create(null, ['url' => $target]) ?> 21 |

22 |

23 |
24 | Html->link('Cancel', '#', [ 25 | 'modal-close' => 'true', 26 | 'class' => 'button button-muted', 27 | 'data-testid' => 'confirm-cancel', 28 | 'tabindex' => 0, 29 | ]) ?> 30 | Form->button('Ok', [ 31 | 'type' => 'submit', 32 | 'class' => 'button button-danger', 33 | 'data-testid' => 'confirm-proceed', 34 | ]) ?> 35 |
36 | Form->end() ?> 37 | -------------------------------------------------------------------------------- /flutterapp/test_resources/project_completed.json: -------------------------------------------------------------------------------- 1 | { 2 | "project": { 3 | "id": 1, 4 | "name": "Home", 5 | "slug": "home", 6 | "color": 1, 7 | "ranking": 2, 8 | "incomplete_task_count": 3, 9 | "sections": [ 10 | { 11 | "id": 1, 12 | "name": "Chores", 13 | "ranking": 1 14 | }, 15 | { 16 | "id": 2, 17 | "name": "Bills", 18 | "ranking": 0 19 | } 20 | ] 21 | }, 22 | "tasks": [ 23 | { 24 | "id": 1, 25 | "project": {"id": 1, "slug": "home", "name": "home", "color": 1}, 26 | "title": "clean dishes", 27 | "body": "", 28 | "evening": false, 29 | "completed": true, 30 | "due_on": null, 31 | "child_order": 0, 32 | "day_order": 0 33 | }, 34 | { 35 | "id": 2, 36 | "project": {"id": 1, "slug": "home", "name": "home", "color": 1}, 37 | "title": "cut grass", 38 | "body": "", 39 | "evening": false, 40 | "completed": true, 41 | "due_on": null, 42 | "child_order": 1, 43 | "day_order": 1 44 | } 45 | ] 46 | } 47 | -------------------------------------------------------------------------------- /flutterapp/test_resources/project_details.json: -------------------------------------------------------------------------------- 1 | { 2 | "project": { 3 | "id": 1, 4 | "name": "Home", 5 | "slug": "home", 6 | "color": 1, 7 | "ranking": 2, 8 | "incomplete_task_count": 3, 9 | "sections": [ 10 | { 11 | "id": 1, 12 | "name": "Chores", 13 | "ranking": 1 14 | }, 15 | { 16 | "id": 2, 17 | "name": "Bills", 18 | "ranking": 2 19 | } 20 | ] 21 | }, 22 | "tasks": [ 23 | { 24 | "id": 1, 25 | "project": {"id": 1, "slug": "home", "name": "home", "color": 1}, 26 | "title": "clean dishes", 27 | "body": "", 28 | "evening": false, 29 | "completed": false, 30 | "due_on": null, 31 | "child_order": 0, 32 | "day_order": 0 33 | }, 34 | { 35 | "id": 2, 36 | "project": {"id": 1, "slug": "home", "name": "home", "color": 1}, 37 | "title": "cut grass", 38 | "body": "", 39 | "evening": false, 40 | "completed": false, 41 | "due_on": null, 42 | "child_order": 1, 43 | "day_order": 1 44 | } 45 | ] 46 | } 47 | -------------------------------------------------------------------------------- /plugins/Tasks/templates/Projects/completed.php: -------------------------------------------------------------------------------- 1 | setLayout('sidebar'); 8 | $this->assign('title', $project->name . ' Completed Tasks'); 9 | 10 | ?> 11 |
12 |
13 |
14 |

15 | Html->link( 16 | $this->element('icons/arrowleft16') . h($project->name), 17 | ['_name' => 'projects:view', 'slug' => $project->slug], 18 | ['class' => 'heading-back', 'escape' => false] 19 | ) ?> 20 | / Completed Tasks 21 |

22 |
23 |
24 | 25 |
26 | element('Tasks.task_item', ['task' => $task, 'showDueOn' => true]); 29 | endforeach; 30 | ?> 31 |
32 |
33 | -------------------------------------------------------------------------------- /templates/Users/new_password.php: -------------------------------------------------------------------------------- 1 | setLayout('card'); 9 | $this->assign('title', 'Reset your password'); 10 | ?> 11 |

Reset your password

12 |

Update your password. Your password must be at least 10 characters long.

13 | Form->create( 14 | $user, 15 | [ 16 | 'type' => 'post', 17 | 'url' => ['controller' => 'Users', 'action' => 'newPassword', $token], 18 | ], 19 | ) ?> 20 | Form->control('password', [ 21 | 'type' => 'password', 22 | 'value' => '', 23 | 'required' => true, 24 | ]) ?> 25 | Form->control('confirm_password', [ 26 | 'type' => 'password', 27 | 'value' => '', 28 | 'required' => true, 29 | ]) ?> 30 |
31 | Form->submit('Reset Password', ['class' => 'button button-primary']) ?> 32 | Html->link( 33 | 'Log In', 34 | ['_path' => 'Users::login'], 35 | ['class' => 'button button-muted'] 36 | ) ?> 37 |
38 | 39 | -------------------------------------------------------------------------------- /assets/sass/components/modal.scss: -------------------------------------------------------------------------------- 1 | .modal-title { 2 | display: flex; 3 | justify-content: space-between; 4 | align-items: center; 5 | 6 | margin-bottom: calc($space * 3); 7 | 8 | h1, h2, h3, h4 { 9 | margin: 0; 10 | } 11 | } 12 | 13 | .modal-close { 14 | @extend .button-bare; 15 | 16 | display: flex; 17 | justify-content: center; 18 | position: relative; 19 | top: -16px; 20 | right: -16px; 21 | 22 | border: none; 23 | border-radius: 50%; 24 | font-size: $font-size-large; 25 | line-height: $font-size-large; 26 | height: 30px; 27 | width: 30px; 28 | padding: 5px; 29 | margin: 0; 30 | box-shadow: none; 31 | } 32 | 33 | .modal-sheet { 34 | width: 75%; 35 | max-width: 850px; 36 | min-height: 80%; 37 | } 38 | .modal-sheet.feed-subscriptions-edit { 39 | max-width: 600px; 40 | min-height: auto; 41 | } 42 | @media (max-width: $breakpoint-phone) { 43 | .modal-sheet { 44 | height: 95%; 45 | width: 95%; 46 | } 47 | } 48 | 49 | .modal-contents { 50 | height: 100%; 51 | overflow: auto; 52 | } 53 | 54 | modal-window .flash-message { 55 | margin-bottom: calc($space * 3); 56 | } 57 | -------------------------------------------------------------------------------- /src/Identifier/ApiTokenIdentifier.php: -------------------------------------------------------------------------------- 1 | fetchTable('ApiTokens'); 21 | 22 | /** @var \App\Model\Entity\ApiToken|null $tokenUser */ 23 | $tokenUser = $apiTokens->find() 24 | ->where(['ApiTokens.token' => $credentials['token']]) 25 | ->contain('Users') 26 | ->first(); 27 | 28 | if (!$tokenUser) { 29 | return null; 30 | } 31 | 32 | // Update the last used timestamp. 33 | $tokenUser->last_used = new DateTime(); 34 | $apiTokens->save($tokenUser); 35 | 36 | return $tokenUser->user; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /assets/sass/components/checkbox.scss: -------------------------------------------------------------------------------- 1 | $checkbox-size: 16px; 2 | 3 | .checkbox { 4 | position: relative; 5 | min-width: $checkbox-size; 6 | width: $checkbox-size; 7 | margin: 0; 8 | 9 | .box { 10 | display: block; 11 | cursor: pointer; 12 | width: $checkbox-size; 13 | height: $checkbox-size; 14 | border: 1px solid var(--color-border-med); 15 | border-radius: 4px; 16 | } 17 | input:checked ~ .box { 18 | border-color: var(--color-success-fg); 19 | } 20 | 21 | input:active ~ .box, 22 | input:focus ~ .box { 23 | border-color: var(--color-input-focus-border); 24 | box-shadow: var(--color-input-focus-shadow) 0 0 0 3px; 25 | } 26 | 27 | // Hide the input without removing tabindex. 28 | input { 29 | opacity: 0; 30 | position: absolute; 31 | pointer-events: none; 32 | } 33 | svg { 34 | color: var(--color-success-fg); 35 | width: 14px; 36 | height: 14px; 37 | } 38 | 39 | .check { 40 | visibility: hidden; 41 | position: absolute; 42 | top: 1px; 43 | left: 1px; 44 | } 45 | input:checked ~ .check { 46 | visibility: visible; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /assets/sass/components/icons.scss: -------------------------------------------------------------------------------- 1 | .icon-error { 2 | color: var(--color-error-fg); 3 | } 4 | 5 | // Colored icon utilities 6 | .icon-today svg, 7 | .today svg { 8 | color: var(--color-due-today); 9 | } 10 | .icon-tomorrow svg, 11 | .tomorrow svg { 12 | color: var(--color-due-tomorrow); 13 | } 14 | .icon-evening svg, 15 | .evening svg { 16 | color: var(--color-due-evening); 17 | } 18 | .icon-not-due svg, 19 | .not-due svg { 20 | color: var(--color-due-none); 21 | } 22 | .none { 23 | color: var(--color-due-none); 24 | } 25 | .icon-overdue svg, 26 | .overdue { 27 | color: var(--color-due-overdue); 28 | } 29 | .icon-week svg, 30 | .week { 31 | color: var(--color-due-week); 32 | } 33 | .icon-fortnight svg, 34 | .fortnight { 35 | color: var(--color-due-fortnight); 36 | } 37 | .icon-delete svg { 38 | color: var(--color-action-delete); 39 | } 40 | .icon-archive svg { 41 | color: var(--color-low-emphasis); 42 | } 43 | .icon-edit svg { 44 | color: var(--color-action-edit); 45 | } 46 | .icon-lock svg { 47 | color: var(--color-action-lock); 48 | } 49 | .icon-complete svg { 50 | color: var(--color-action-complete); 51 | } 52 | 53 | -------------------------------------------------------------------------------- /templates/layout/default.php: -------------------------------------------------------------------------------- 1 | theme}"; 7 | endif; 8 | ?> 9 | 10 | 11 | 12 | Html->charset() ?> 13 | 14 | 15 | 16 | <?= $this->fetch('title') ?> 17 | 18 | Html->meta('icon') ?> 19 | 20 | 21 | fetch('meta') ?> 22 | 23 | fetch('script') ?> 24 | fetch('css') ?> 25 | 26 | element('frontend_assets') ?> 27 | 28 | 29 |
30 |
31 | Flash->render() ?> 32 |
33 | fetch('content') ?> 34 |
35 |
36 |
37 | 38 | 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020, Mark Story (http://mark-story.com) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /plugins/Tasks/templates/ProjectSections/add.php: -------------------------------------------------------------------------------- 1 | setLayout('modal'); 10 | $this->assign('title', 'Create a section'); 11 | ?> 12 | 16 |

17 | Sections help organize tasks in a project into logical chunks. 18 | When a section is deleted, all the tasks within that section are also deleted. 19 | Sections in a project can be sorted as you see fit. 20 |

21 | Form->create($section, [ 23 | 'class' => 'form-modal', 24 | ]); 25 | echo $this->Form->hidden('referer', ['value' => $referer]); 26 | echo $this->Form->control('name'); 27 | ?> 28 |
29 | Form->button('Save', [ 30 | 'class' => 'button button-primary', 31 | 'data-testid' => 'save-section', 32 | ]); ?> 33 |
34 | Form->end() ?> 35 | -------------------------------------------------------------------------------- /plugins/Feeds/templates/FeedSubscriptions/edit.php: -------------------------------------------------------------------------------- 1 | request->is('htmx'); 8 | 9 | $this->setLayout('sidebar'); 10 | if ($isHtmx) { 11 | $this->setLayout('sheet'); 12 | $this->set('sheet.class', 'feed-subscriptions-edit'); 13 | } 14 | 15 | $this->assign('title', 'Edit Feed'); 16 | ?> 17 | 21 | Form->create($feedSubscription, ['class' => 'form-modal']); 23 | echo $this->Form->control('url', ['label' => 'Feed URL', 'value' => $feedSubscription->feed->url]); 24 | echo $this->Form->control('alias', ['label' => 'Name']); 25 | echo $this->Form->control('feed_category_id', [ 26 | 'options' => $feedCategories, 27 | ]); 28 | ?> 29 |
30 | Form->submit('Save', ['class' => 'button button-primary']) ?> 31 |
32 | Form->end() ?> 33 | -------------------------------------------------------------------------------- /assets/sass/components/projectItem.scss: -------------------------------------------------------------------------------- 1 | // Should be similar to .links a in projectFilter as well. 2 | // TODO Could be generalized with feed-category-item 3 | .project-item { 4 | display: flex; 5 | justify-content: space-between; 6 | align-items: center; 7 | gap: $space; 8 | width: max-content; 9 | padding-right: calc($space / 2); 10 | border-radius: $border-radius; 11 | margin-bottom: 1px; 12 | 13 | // Disable hover background on context menu button. 14 | > .button-default:hover { 15 | background: transparent; 16 | box-shadow: none; 17 | } 18 | 19 | &.active, 20 | &:hover { 21 | background: var(--color-bg-level1-active); 22 | } 23 | 24 | .project-badge { 25 | height: 40px; 26 | flex-grow: 1; 27 | } 28 | .project-badge svg { 29 | margin: 0 4px 0 2px; 30 | } 31 | 32 | a { 33 | display: flex; 34 | align-items: center; 35 | flex-grow: 1; 36 | 37 | color: var(--color-fg); 38 | text-decoration: none; 39 | padding-left: calc($space / 2); 40 | } 41 | 42 | .counter { 43 | margin: 0 calc($space / 2); 44 | } 45 | } 46 | 47 | .drag-ghost .project-item { 48 | width: 200px; 49 | } 50 | -------------------------------------------------------------------------------- /templates/element/icons/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 GitHub Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 5 | "allowJs": true, 6 | "jsx": "react-jsx", 7 | "importHelpers": true, 8 | "isolatedModules": true, 9 | "module": "ESNext", 10 | "moduleResolution": "Node", 11 | "typeRoots": ["./node_modules/@types"], 12 | "noEmitHelpers": true, 13 | "noFallthroughCasesInSwitch": true, 14 | "noImplicitAny": true, 15 | "noImplicitReturns": true, 16 | "noUnusedLocals": false, 17 | "noUnusedParameters": true, 18 | "pretty": false, 19 | "resolveJsonModule": true, 20 | "sourceMap": true, 21 | "strict": true, 22 | "skipLibCheck": false, 23 | "esModuleInterop": true, 24 | "outDir": "webroot/js", 25 | "types": [ 26 | "vite/client", 27 | "jest", 28 | ], 29 | "baseUrl": ".", 30 | "paths": { 31 | "app/*": ["assets/js/*"], 32 | "tests/*": ["tests/js/*"] 33 | } 34 | }, 35 | "include": [ 36 | "assets/js", 37 | "assets/js/**/*.tsx", 38 | "tests/js/**/*.ts", 39 | "tests/js/**/*.tsx" 40 | ], 41 | "exclude": ["./node_modules"] 42 | } 43 | -------------------------------------------------------------------------------- /templates/element/icons/trophy16.php: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /config/Migrations/20220530022319_CreateApiTokens.php: -------------------------------------------------------------------------------- 1 | table('api_tokens'); 19 | $table->addColumn('user_id', 'integer', [ 20 | 'default' => null, 21 | 'limit' => 11, 22 | 'null' => false, 23 | ]); 24 | $table->addColumn('token', 'string', [ 25 | 'default' => null, 26 | 'limit' => 255, 27 | 'null' => false, 28 | ]); 29 | $table->addColumn('last_used', 'timestamp', [ 30 | 'default' => null, 31 | 'null' => true, 32 | ]); 33 | $table->addColumn('created', 'timestamp', [ 34 | 'default' => null, 35 | 'null' => false, 36 | ]); 37 | $table->create(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # CakePHP specific files # 2 | ########################## 3 | /config/Migrations/*.lock 4 | /config/app_local.php 5 | /config/.env 6 | /logs/* 7 | /tmp/* 8 | /tools/* 9 | /vendor/* 10 | 11 | # Node packages 12 | /node_modules/* 13 | /.yarn/* 14 | 15 | # Compiled files. 16 | /webroot/js/*.js 17 | /webroot/css/app.css 18 | /webroot/mix-manifest.json 19 | /webroot/manifest.json 20 | /webroot/assets/ 21 | /docket.sqlite 22 | /tests.sqlite 23 | 24 | # application config 25 | google-auth.json 26 | google-services.json 27 | 28 | # OS generated files # 29 | ###################### 30 | .DS_Store 31 | .DS_Store? 32 | ._* 33 | .Spotlight-V100 34 | .Trashes 35 | ehthumbs.db 36 | Thumbs.db 37 | .directory 38 | 39 | # Tool specific files # 40 | ####################### 41 | # PHPUnit 42 | .phpunit.result.cache 43 | /phpunit.xml 44 | 45 | # vim 46 | *~ 47 | *.swp 48 | *.swo 49 | # sublime text & textmate 50 | *.sublime-* 51 | *.stTheme.cache 52 | *.tmlanguage.cache 53 | *.tmPreferences.cache 54 | # Eclipse 55 | .settings/* 56 | # JetBrains, aka PHPStorm, IntelliJ IDEA 57 | .idea/* 58 | # NetBeans 59 | nbproject/* 60 | # Visual Studio Code 61 | .vscode 62 | # Sass preprocessor 63 | .sass-cache/ 64 | -------------------------------------------------------------------------------- /src/View/AppView.php: -------------------------------------------------------------------------------- 1 | loadHelper('Html');` 35 | * 36 | * @return void 37 | */ 38 | public function initialize(): void 39 | { 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /flutterapp/lib/components/taskaddbutton.dart: -------------------------------------------------------------------------------- 1 | import 'package:docket/viewmodels/taskadd.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | import 'package:docket/routes.dart'; 5 | import 'package:provider/provider.dart'; 6 | 7 | /// Button to create a new task with some fields initialized. 8 | class TaskAddButton extends StatelessWidget { 9 | final DateTime? dueOn; 10 | final int? projectId; 11 | final int? sectionId; 12 | final bool? evening; 13 | 14 | const TaskAddButton({this.dueOn, this.projectId, this.sectionId, this.evening, super.key}); 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | var theme = Theme.of(context); 19 | return IconButton( 20 | icon: const Icon(Icons.add), 21 | color: theme.colorScheme.primary, 22 | onPressed: () { 23 | var viewmodel = Provider.of(context, listen: false); 24 | 25 | viewmodel.task.dueOn = dueOn; 26 | viewmodel.task.sectionId = sectionId; 27 | viewmodel.task.evening = evening ?? false; 28 | viewmodel.task.projectId = projectId; 29 | 30 | Navigator.pushNamed(context, Routes.taskAdd); 31 | }); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /plugins/Tasks/templates/ProjectSections/options.php: -------------------------------------------------------------------------------- 1 | combine('id', 'name')->toArray(); 13 | echo $this->Form->control('section_id', [ 14 | 'label' => [ 15 | 'class' => 'form-section-heading icon-week', 16 | 'text' => $this->element('icons/directory-symlink16') . 'Section', 17 | 'escape' => false, 18 | ], 19 | 'options' => $options, 20 | 'empty' => true, 21 | 'value' => $value, 22 | ]); 23 | else : 24 | echo $this->Form->control('section_id', [ 25 | 'label' => [ 26 | 'class' => 'form-section-heading icon-week', 27 | 'text' => $this->element('icons/directory-symlink16') . 'Section', 28 | 'escape' => false, 29 | ], 30 | 'type' => 'text', 31 | 'disabled' => 'true', 32 | 'placeholder' => 'No Sections', 33 | 'empty' => true, 34 | ]); 35 | endif; 36 | -------------------------------------------------------------------------------- /plugins/Tasks/templates/element/task_due_on.php: -------------------------------------------------------------------------------- 1 | due_on) : 8 | $diff = $this->Date->today()->diffInDays($task->due_on, false); 9 | $className = 'due-on '; 10 | $thisEvening = $diff == 0 && $task->evening; 11 | 12 | if ($diff < 0) : 13 | $className .= 'overdue'; 14 | elseif ($diff == 0 && !$task->evening) : 15 | $className .= 'today'; 16 | elseif ($thisEvening) : 17 | $className .= 'evening'; 18 | elseif ($diff >= 1 && $diff < 2) : 19 | $className .= 'tomorrow'; 20 | elseif ($diff >= 2 && $diff < 8) : 21 | $className .= 'week'; 22 | endif; 23 | 24 | $formatted = $thisEvening ? 'This evening' : $this->Date->formatCompact($task->due_on); 25 | $icon = $task->evening ? 'moon' : 'calendar'; 26 | ?> 27 | 31 | 32 | No Due Date 33 | 34 | -------------------------------------------------------------------------------- /plugins/Feeds/src/Model/Entity/FeedItemUser.php: -------------------------------------------------------------------------------- 1 | 30 | */ 31 | protected array $_accessible = [ 32 | 'user_id' => true, 33 | 'feed_item_id' => true, 34 | 'read_at' => true, 35 | 'saved_at' => true, 36 | 'user' => true, 37 | 'feed_item' => true, 38 | ]; 39 | } 40 | -------------------------------------------------------------------------------- /flutterapp/.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled. 5 | 6 | version: 7 | revision: fb57da5f945d02ef4f98dfd9409a72b7cce74268 8 | channel: stable 9 | 10 | project_type: app 11 | 12 | # Tracks metadata for the flutter migrate command 13 | migration: 14 | platforms: 15 | - platform: root 16 | create_revision: fb57da5f945d02ef4f98dfd9409a72b7cce74268 17 | base_revision: fb57da5f945d02ef4f98dfd9409a72b7cce74268 18 | - platform: android 19 | create_revision: fb57da5f945d02ef4f98dfd9409a72b7cce74268 20 | base_revision: fb57da5f945d02ef4f98dfd9409a72b7cce74268 21 | - platform: ios 22 | create_revision: fb57da5f945d02ef4f98dfd9409a72b7cce74268 23 | base_revision: fb57da5f945d02ef4f98dfd9409a72b7cce74268 24 | 25 | # User provided section 26 | 27 | # List of Local paths (relative to this file) that should be 28 | # ignored by the migrate tool. 29 | # 30 | # Files that are not part of the templates will be ignored by default. 31 | unmanaged_files: 32 | - 'lib/main.dart' 33 | - 'ios/Runner.xcodeproj/project.pbxproj' 34 | -------------------------------------------------------------------------------- /config/bootstrap_cli.php: -------------------------------------------------------------------------------- 1 | 30 | */ 31 | protected array $_accessible = [ 32 | 'feed_subscription_id' => true, 33 | 'title' => true, 34 | 'body' => true, 35 | 'created' => true, 36 | 'modified' => true, 37 | 'feed_subscription' => true, 38 | ]; 39 | } 40 | -------------------------------------------------------------------------------- /src/Model/Entity/ApiToken.php: -------------------------------------------------------------------------------- 1 | 29 | */ 30 | protected array $_accessible = [ 31 | 'last_used' => true, 32 | 'created' => true, 33 | ]; 34 | 35 | /** 36 | * Fields that are excluded from JSON versions of the entity. 37 | * 38 | * @var array 39 | */ 40 | protected array $_hidden = [ 41 | 'id', 42 | 'user_id', 43 | ]; 44 | } 45 | -------------------------------------------------------------------------------- /templates/Error/error500.php: -------------------------------------------------------------------------------- 1 | setLayout('error'); 9 | 10 | if (Configure::read('debug')) : 11 | $this->setLayout('dev_error'); 12 | 13 | $this->assign('title', $message); 14 | $this->assign('templateName', 'error500.php'); 15 | 16 | $this->start('file'); 17 | ?> 18 | queryString)) : ?> 19 |

20 | SQL Query: 21 | queryString) ?> 22 |

23 | 24 | params)) : ?> 25 | SQL Query Params: 26 | params) ?> 27 | 28 | 29 | Error in: 30 | getFile()), $error->getLine()) ?> 31 | 32 | element('auto_table_warning'); 34 | 35 | $this->end(); 36 | endif; 37 | ?> 38 |

39 |

40 | : 41 | 42 |

43 | -------------------------------------------------------------------------------- /flutterapp/lib/components/iconsnackbar.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:docket/theme.dart'; 3 | 4 | SnackBar successSnackBar({BuildContext? context, ThemeData? theme, required String text}) { 5 | assert(context != null || theme != null, "one of theme or context is required"); 6 | 7 | theme = theme ?? Theme.of(context!); 8 | var colors = theme.extension()!; 9 | 10 | return SnackBar( 11 | content: Row(mainAxisAlignment: MainAxisAlignment.start, children: [ 12 | Padding( 13 | padding: const EdgeInsets.only(left: 4, right: 4), 14 | child: Icon(Icons.check_circle, color: colors.actionComplete), 15 | ), 16 | Text(text), 17 | ])); 18 | } 19 | 20 | SnackBar errorSnackBar({BuildContext? context, ThemeData? theme, required String text}) { 21 | assert(context != null || theme != null, "one of theme or context is required"); 22 | theme = theme ?? Theme.of(context!); 23 | 24 | return SnackBar( 25 | content: Row(mainAxisAlignment: MainAxisAlignment.start, children: [ 26 | Padding( 27 | padding: const EdgeInsets.only(left: 4, right: 4), 28 | child: Icon(Icons.error_outline, color: theme.colorScheme.error), 29 | ), 30 | Text(text), 31 | ])); 32 | } 33 | -------------------------------------------------------------------------------- /flutterapp/lib/components/floatingcreatetaskbutton.dart: -------------------------------------------------------------------------------- 1 | import 'package:docket/viewmodels/taskadd.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | import 'package:docket/routes.dart'; 5 | import 'package:provider/provider.dart'; 6 | 7 | class FloatingCreateTaskButton extends StatelessWidget { 8 | final DateTime? dueOn; 9 | final int? projectId; 10 | final int? sectionId; 11 | final bool? evening; 12 | 13 | const FloatingCreateTaskButton({this.dueOn, this.projectId, this.sectionId, this.evening, super.key}); 14 | 15 | @override 16 | Widget build(BuildContext context) { 17 | var theme = Theme.of(context); 18 | return FloatingActionButton( 19 | key: const ValueKey("floating-task-add"), 20 | onPressed: () { 21 | var viewmodel = Provider.of(context, listen: false); 22 | 23 | viewmodel.task.dueOn = dueOn; 24 | viewmodel.task.sectionId = sectionId; 25 | viewmodel.task.projectId = projectId; 26 | viewmodel.task.evening = evening ?? false; 27 | 28 | Navigator.pushNamed(context, Routes.taskAdd); 29 | }, 30 | backgroundColor: theme.colorScheme.primary, 31 | child: Icon(Icons.add, color: theme.colorScheme.onPrimary), 32 | ); 33 | } 34 | } 35 | --------------------------------------------------------------------------------