Working App Intents!

main
Laurent 3 years ago
parent 89d0adb448
commit 26c94d7471
  1. 56
      LaunchIntents/IntentHandler.swift
  2. 66
      LeCountdown.xcodeproj/project.pbxproj
  3. 15
      LeCountdown/AppDelegate.swift
  4. 212
      LeCountdown/Base.lproj/SiriIntents.intentdefinition
  5. 24
      LeCountdown/Conductor.swift
  6. 13
      LeCountdown/Info.plist
  7. 68
      LeCountdown/Intent/StartTimerIntent.swift
  8. 44
      LeCountdown/Intent/TimerIdentifierAppEntity.swift
  9. 21
      LeCountdown/Intent/TimerShortcuts.swift
  10. 8
      LeCountdown/LeCountdownApp.swift
  11. 2
      LeCountdown/Utils/Preferences.swift
  12. 6
      LeCountdown/Utils/Tip.swift
  13. 29
      LeCountdown/Views/ContentView.swift
  14. 11
      LeCountdown/Views/DialView.swift
  15. 1
      LeCountdown/Views/HomeView.swift
  16. 12
      LeCountdown/en.lproj/AppShortcuts.strings
  17. 0
      LeCountdown/en.lproj/SiriIntents.strings
  18. 12
      LeCountdown/fr.lproj/AppShortcuts.strings
  19. 5
      LeCountdown/fr.lproj/Localizable.strings
  20. 7
      LeCountdown/fr.lproj/SiriIntents.strings

@ -7,34 +7,34 @@
import Intents
class IntentHandler: INExtension, SelectTimerIntentHandling, LaunchTimerIntentHandling {
// MARK: - SelectTimerIntentHandling
func resolveTimer(for intent: LaunchTimerIntent) async -> TimerIdentifierResolutionResult {
if let timer = intent.timer {
print("resolveTimer(for intent: LaunchTimerIntent) success !")
return .success(with: timer)
}
print("resolveTimer(for intent: LaunchTimerIntent) needsValue")
return .needsValue()
}
func handle(intent: LaunchTimerIntent) async -> LaunchTimerIntentResponse {
if let timerIdentifier = intent.timer,
let timerId = timerIdentifier.identifier,
let timer = IntentDataProvider.main.timer(id: timerId) {
do {
let _ = try await TimerRouter.performAction(timer: timer)
print("handle(intent: LaunchTimerIntent) success !")
return .success(timer: timerIdentifier)
} catch {
return LaunchTimerIntentResponse(code: LaunchTimerIntentResponseCode(rawValue: 1)!, userActivity: nil)
}
}
print("handle(intent: LaunchTimerIntent) no timer")
return LaunchTimerIntentResponse(code: LaunchTimerIntentResponseCode(rawValue: 1)!, userActivity: nil)
}
class IntentHandler: INExtension, SelectTimerIntentHandling {
//
// // MARK: - SelectTimerIntentHandling
//
// func resolveTimer(for intent: LaunchTimerIntent) async -> TimerIdentifierResolutionResult {
// if let timer = intent.timer {
// print("resolveTimer(for intent: LaunchTimerIntent) success !")
// return .success(with: timer)
// }
// print("resolveTimer(for intent: LaunchTimerIntent) needsValue")
// return .needsValue()
// }
//
// func handle(intent: LaunchTimerIntent) async -> LaunchTimerIntentResponse {
// if let timerIdentifier = intent.timer,
// let timerId = timerIdentifier.identifier,
// let timer = IntentDataProvider.main.timer(id: timerId) {
// do {
// let _ = try await TimerRouter.performAction(timer: timer)
// print("handle(intent: LaunchTimerIntent) success !")
// return .success(timer: timerIdentifier)
// } catch {
// return LaunchTimerIntentResponse(code: LaunchTimerIntentResponseCode(rawValue: 1)!, userActivity: nil)
// }
// }
// print("handle(intent: LaunchTimerIntent) no timer")
// return LaunchTimerIntentResponse(code: LaunchTimerIntentResponseCode(rawValue: 1)!, userActivity: nil)
// }
// MARK: - SelectTimerIntentHandling

@ -75,7 +75,6 @@
C473C30729A91BCC0056B38A /* fr.xcloc in Resources */ = {isa = PBXBuildFile; fileRef = C473C30629A91BCB0056B38A /* fr.xcloc */; };
C473C30829A91BCC0056B38A /* fr.xcloc in Resources */ = {isa = PBXBuildFile; fileRef = C473C30629A91BCB0056B38A /* fr.xcloc */; };
C473C30929A91BCC0056B38A /* fr.xcloc in Resources */ = {isa = PBXBuildFile; fileRef = C473C30629A91BCB0056B38A /* fr.xcloc */; };
C473C31329A925D50056B38A /* SiriIntents.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = C473C31629A925D50056B38A /* SiriIntents.intentdefinition */; };
C473C31829A926F50056B38A /* LaunchWidget.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = C473C31C29A926F50056B38A /* LaunchWidget.intentdefinition */; };
C473C31929A926F50056B38A /* LaunchWidget.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = C473C31C29A926F50056B38A /* LaunchWidget.intentdefinition */; };
C473C31A29A926F50056B38A /* LaunchWidget.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = C473C31C29A926F50056B38A /* LaunchWidget.intentdefinition */; };
@ -157,7 +156,10 @@
C4BA2B6A29A4BE1800CB4FBA /* Date+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2B6929A4BE1800CB4FBA /* Date+Extensions.swift */; };
C4BA2B7329A60CF000CB4FBA /* Shortcut.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2B7229A60CF000CB4FBA /* Shortcut.swift */; };
C4BA2B7929A65C1400CB4FBA /* UIDevice+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2B7829A65C1400CB4FBA /* UIDevice+Extensions.swift */; };
C4E5D66129B64C1B008E7465 /* SiriIntents.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = C473C31629A925D50056B38A /* SiriIntents.intentdefinition */; };
C4E5D66629B73AED008E7465 /* StartTimerIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E5D66429B73AED008E7465 /* StartTimerIntent.swift */; };
C4E5D66729B73AED008E7465 /* TimerIdentifierAppEntity.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E5D66529B73AED008E7465 /* TimerIdentifierAppEntity.swift */; };
C4E5D66A29B73FC6008E7465 /* TimerShortcuts.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E5D66929B73FC6008E7465 /* TimerShortcuts.swift */; };
C4E5D66D29B753D7008E7465 /* AppShortcuts.strings in Resources */ = {isa = PBXBuildFile; fileRef = C4E5D66F29B753D7008E7465 /* AppShortcuts.strings */; };
C4F8B1532987FE6F005C86A5 /* LaunchWidgetLiveActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = C438C7D72981216200BF3EF9 /* LaunchWidgetLiveActivity.swift */; };
C4F8B15729891271005C86A5 /* Conductor.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F8B15629891271005C86A5 /* Conductor.swift */; };
C4F8B15929891528005C86A5 /* forest_stream.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = C4F8B15829891528005C86A5 /* forest_stream.mp3 */; };
@ -306,9 +308,7 @@
C473C2F829A8DC0A0056B38A /* LaunchWidgetAttributes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchWidgetAttributes.swift; sourceTree = "<group>"; };
C473C30229A91BB90056B38A /* en.xcloc */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = en.xcloc; path = Localizations/en.xcloc; sourceTree = "<group>"; };
C473C30629A91BCB0056B38A /* fr.xcloc */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = fr.xcloc; path = Localizations/fr.xcloc; sourceTree = "<group>"; };
C473C32629AA307D0056B38A /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.intentdefinition; name = Base; path = Base.lproj/SiriIntents.intentdefinition; sourceTree = "<group>"; };
C473C32729AA307D0056B38A /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.intentdefinition; name = Base; path = Base.lproj/LaunchWidget.intentdefinition; sourceTree = "<group>"; };
C473C32829AA30890056B38A /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/SiriIntents.strings; sourceTree = "<group>"; };
C473C32929AA30890056B38A /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/LaunchWidget.strings; sourceTree = "<group>"; };
C473C32B29AA330E0056B38A /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = "<group>"; };
C473C33829ACDBD70056B38A /* TipView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TipView.swift; sourceTree = "<group>"; };
@ -366,7 +366,11 @@
C4BA2B6B29A4C47100CB4FBA /* LeCountdown.0.6.2.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = LeCountdown.0.6.2.xcdatamodel; sourceTree = "<group>"; };
C4BA2B7229A60CF000CB4FBA /* Shortcut.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Shortcut.swift; sourceTree = "<group>"; };
C4BA2B7829A65C1400CB4FBA /* UIDevice+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIDevice+Extensions.swift"; sourceTree = "<group>"; };
C4E5D66329B64C7F008E7465 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/SiriIntents.strings; sourceTree = "<group>"; };
C4E5D66429B73AED008E7465 /* StartTimerIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartTimerIntent.swift; sourceTree = "<group>"; };
C4E5D66529B73AED008E7465 /* TimerIdentifierAppEntity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimerIdentifierAppEntity.swift; sourceTree = "<group>"; };
C4E5D66929B73FC6008E7465 /* TimerShortcuts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimerShortcuts.swift; sourceTree = "<group>"; };
C4E5D66E29B753D7008E7465 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/AppShortcuts.strings; sourceTree = "<group>"; };
C4E5D67029B753DC008E7465 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/AppShortcuts.strings; sourceTree = "<group>"; };
C4F8B15629891271005C86A5 /* Conductor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Conductor.swift; sourceTree = "<group>"; };
C4F8B15829891528005C86A5 /* forest_stream.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = forest_stream.mp3; sourceTree = "<group>"; };
C4F8B15E298961A7005C86A5 /* ReorderableForEach.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReorderableForEach.swift; sourceTree = "<group>"; };
@ -476,6 +480,7 @@
C438C8092981DDF800BF3EF9 /* Model */,
C445FA8D2987B82E0054D761 /* Sound */,
C4BA2B6629A3C49200CB4FBA /* Stats */,
C4E5D66829B73FAB008E7465 /* Intent */,
C438C80A2981DE1A00BF3EF9 /* Utils */,
C438C8082981DDD200BF3EF9 /* Widget */,
C445FA962987D0CF0054D761 /* Sound_Assets */,
@ -484,8 +489,8 @@
C438C80429813B3100BF3EF9 /* LeCountdown.entitlements */,
C473C30229A91BB90056B38A /* en.xcloc */,
C473C30629A91BCB0056B38A /* fr.xcloc */,
C473C31629A925D50056B38A /* SiriIntents.intentdefinition */,
C473C32A29AA330E0056B38A /* Localizable.strings */,
C4E5D66F29B753D7008E7465 /* AppShortcuts.strings */,
C4060DCD297AE73D003FAB80 /* Info.plist */,
C4060DC5297AE73D003FAB80 /* Preview Content */,
);
@ -679,6 +684,16 @@
path = Stats;
sourceTree = "<group>";
};
C4E5D66829B73FAB008E7465 /* Intent */ = {
isa = PBXGroup;
children = (
C4E5D66429B73AED008E7465 /* StartTimerIntent.swift */,
C4E5D66529B73AED008E7465 /* TimerIdentifierAppEntity.swift */,
C4E5D66929B73FC6008E7465 /* TimerShortcuts.swift */,
);
path = Intent;
sourceTree = "<group>";
};
C4F8B188298AC248005C86A5 /* Generation */ = {
isa = PBXGroup;
children = (
@ -914,6 +929,7 @@
C4BA2AE82995ACC200CB4FBA /* Clave_Loop_LLL.wav in Resources */,
C445FA952987D01C0054D761 /* train_horn.mp3 in Resources */,
C4BA2AE02995ABD200CB4FBA /* HighChords_Loop_River.wav in Resources */,
C4E5D66D29B753D7008E7465 /* AppShortcuts.strings in Resources */,
C4060DC4297AE73D003FAB80 /* Assets.xcassets in Resources */,
C473C30329A91BB90056B38A /* en.xcloc in Resources */,
C4BA2AE62995AC3F00CB4FBA /* Loop_ToneSD_Boavista.wav in Resources */,
@ -987,6 +1003,7 @@
C4BA2B04299A42EF00CB4FBA /* NewDataView.swift in Sources */,
C4BA2B4C299FCE0C00CB4FBA /* Stopwatch+CoreDataProperties.swift in Sources */,
C438C7FF2981300500BF3EF9 /* IntentDataProvider.swift in Sources */,
C4E5D66629B73AED008E7465 /* StartTimerIntent.swift in Sources */,
C4BA2AD62993F62700CB4FBA /* SoundSelectionView.swift in Sources */,
C498E5A5299152B400E90DE0 /* GreenCheckmarkView.swift in Sources */,
C4BA2B3E299FC86800CB4FBA /* StatsView.swift in Sources */,
@ -1006,7 +1023,6 @@
C473C33C29ACEC4F0056B38A /* Tip.swift in Sources */,
C498E5A3298D720600E90DE0 /* TestView.swift in Sources */,
C4BA2B06299A8F8D00CB4FBA /* PresetsView.swift in Sources */,
C473C31329A925D50056B38A /* SiriIntents.intentdefinition in Sources */,
C473C33929ACDBD70056B38A /* TipView.swift in Sources */,
C445FA8F2987B83B0054D761 /* SoundPlayer.swift in Sources */,
C4F8B1A7298AC2FC005C86A5 /* AbstractSoundTimer+CoreDataClass.swift in Sources */,
@ -1036,6 +1052,7 @@
C4BA2B49299FCE0C00CB4FBA /* IntervalGroup+CoreDataProperties.swift in Sources */,
C4F8B17C298AC234005C86A5 /* Record+CoreDataClass.swift in Sources */,
C4BA2B5B299FFAB000CB4FBA /* Logger.swift in Sources */,
C4E5D66729B73AED008E7465 /* TimerIdentifierAppEntity.swift in Sources */,
C4F8B162298A9A1F005C86A5 /* NewAlarmView.swift in Sources */,
C4742B5B298414B000D5D950 /* ImageSelectionView.swift in Sources */,
C438C7C5298024E900BF3EF9 /* NSManagedContext+Extensions.swift in Sources */,
@ -1047,6 +1064,7 @@
C498E59F298D4DEA00E90DE0 /* LiveTimerListView.swift in Sources */,
C4060DC0297AE73B003FAB80 /* LeCountdownApp.swift in Sources */,
C473C31829A926F50056B38A /* LaunchWidget.intentdefinition in Sources */,
C4E5D66A29B73FC6008E7465 /* TimerShortcuts.swift in Sources */,
C4742B5729840F6400D5D950 /* CoolPic.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@ -1146,7 +1164,6 @@
C4BA2AF82996A4F000CB4FBA /* CustomSound+CoreDataProperties.swift in Sources */,
C4F8B199298AC288005C86A5 /* Record+CoreDataClass.swift in Sources */,
C438C8012981327600BF3EF9 /* Persistence.swift in Sources */,
C4E5D66129B64C1B008E7465 /* SiriIntents.intentdefinition in Sources */,
C473C31A29A926F50056B38A /* LaunchWidget.intentdefinition in Sources */,
C4BA2B5D299FFAB000CB4FBA /* Logger.swift in Sources */,
C473C2FB29A8DC3A0056B38A /* LaunchWidgetAttributes.swift in Sources */,
@ -1192,16 +1209,6 @@
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
C473C31629A925D50056B38A /* SiriIntents.intentdefinition */ = {
isa = PBXVariantGroup;
children = (
C473C32629AA307D0056B38A /* Base */,
C473C32829AA30890056B38A /* fr */,
C4E5D66329B64C7F008E7465 /* en */,
);
name = SiriIntents.intentdefinition;
sourceTree = "<group>";
};
C473C31C29A926F50056B38A /* LaunchWidget.intentdefinition */ = {
isa = PBXVariantGroup;
children = (
@ -1219,6 +1226,15 @@
name = Localizable.strings;
sourceTree = "<group>";
};
C4E5D66F29B753D7008E7465 /* AppShortcuts.strings */ = {
isa = PBXVariantGroup;
children = (
C4E5D66E29B753D7008E7465 /* fr */,
C4E5D67029B753DC008E7465 /* en */,
);
name = AppShortcuts.strings;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
@ -1352,6 +1368,7 @@
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = LeCountdown/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Momo;
INFOPLIST_KEY_NSAppleMusicUsageDescription = NSAppleMusicUsageDescription;
INFOPLIST_KEY_NSSupportsLiveActivities = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
@ -1363,7 +1380,7 @@
"@executable_path/Frameworks",
);
MARKETING_VERSION = 0.1;
PRODUCT_BUNDLE_IDENTIFIER = com.staxriver.LeCountdown2;
PRODUCT_BUNDLE_IDENTIFIER = com.staxriver.LeCountdown;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
@ -1385,6 +1402,7 @@
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = LeCountdown/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Momo;
INFOPLIST_KEY_NSAppleMusicUsageDescription = NSAppleMusicUsageDescription;
INFOPLIST_KEY_NSSupportsLiveActivities = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
@ -1396,7 +1414,7 @@
"@executable_path/Frameworks",
);
MARKETING_VERSION = 0.1;
PRODUCT_BUNDLE_IDENTIFIER = com.staxriver.LeCountdown2;
PRODUCT_BUNDLE_IDENTIFIER = com.staxriver.LeCountdown;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
@ -1499,7 +1517,7 @@
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 0.1;
PRODUCT_BUNDLE_IDENTIFIER = com.staxriver.LeCountdown2.LaunchWidget;
PRODUCT_BUNDLE_IDENTIFIER = com.staxriver.LeCountdown.LaunchWidget;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
@ -1527,7 +1545,7 @@
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 0.1;
PRODUCT_BUNDLE_IDENTIFIER = com.staxriver.LeCountdown2.LaunchWidget;
PRODUCT_BUNDLE_IDENTIFIER = com.staxriver.LeCountdown.LaunchWidget;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
@ -1554,7 +1572,7 @@
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 0.1;
PRODUCT_BUNDLE_IDENTIFIER = com.staxriver.LeCountdown2.LaunchIntents;
PRODUCT_BUNDLE_IDENTIFIER = com.staxriver.LeCountdown.LaunchIntents;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
@ -1581,7 +1599,7 @@
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 0.1;
PRODUCT_BUNDLE_IDENTIFIER = com.staxriver.LeCountdown2.LaunchIntents;
PRODUCT_BUNDLE_IDENTIFIER = com.staxriver.LeCountdown.LaunchIntents;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
SWIFT_EMIT_LOC_STRINGS = YES;

@ -10,6 +10,13 @@ import UIKit
class AppDelegate : NSObject, UIApplicationDelegate {
override init() {
super.init()
NotificationCenter.default.addObserver(self, selector: #selector(_contextDidChange), name: NSNotification.Name.NSManagedObjectContextDidSave, object: nil)
}
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
UNUserNotificationCenter.current().delegate = self
@ -27,6 +34,14 @@ class AppDelegate : NSObject, UIApplicationDelegate {
return true
}
@objc fileprivate func _contextDidChange(_ notification: Notification) {
TimerShortcuts.updateAppShortcutParameters()
}
deinit {
NotificationCenter.default.removeObserver(self)
}
}
extension AppDelegate: UNUserNotificationCenterDelegate {

@ -1,212 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>INEnums</key>
<array/>
<key>INIntentDefinitionModelVersion</key>
<string>1.2</string>
<key>INIntentDefinitionNamespace</key>
<string>ggxqDx</string>
<key>INIntentDefinitionSystemVersion</key>
<string>22A400</string>
<key>INIntentDefinitionToolsBuildVersion</key>
<string>14C18</string>
<key>INIntentDefinitionToolsVersion</key>
<string>14.2</string>
<key>INIntents</key>
<array>
<dict>
<key>INIntentCategory</key>
<string>start</string>
<key>INIntentDescription</key>
<string>Launch timers and stopwatches</string>
<key>INIntentDescriptionID</key>
<string>NdKydA</string>
<key>INIntentLastParameterTag</key>
<integer>2</integer>
<key>INIntentName</key>
<string>LaunchTimer</string>
<key>INIntentParameterCombinations</key>
<dict>
<key>timer</key>
<dict>
<key>INIntentParameterCombinationIsPrimary</key>
<true/>
<key>INIntentParameterCombinationSubtitle</key>
<string>Starts immediately</string>
<key>INIntentParameterCombinationSubtitleID</key>
<string>r4Ikzx</string>
<key>INIntentParameterCombinationSupportsBackgroundExecution</key>
<true/>
<key>INIntentParameterCombinationTitle</key>
<string>Launch ${timer}</string>
<key>INIntentParameterCombinationTitleID</key>
<string>JfqtH6</string>
</dict>
</dict>
<key>INIntentParameters</key>
<array>
<dict>
<key>INIntentParameterConfigurable</key>
<true/>
<key>INIntentParameterDisplayName</key>
<string>Timer</string>
<key>INIntentParameterDisplayNameID</key>
<string>wU1mYs</string>
<key>INIntentParameterDisplayPriority</key>
<integer>1</integer>
<key>INIntentParameterName</key>
<string>timer</string>
<key>INIntentParameterObjectType</key>
<string>TimerIdentifier</string>
<key>INIntentParameterObjectTypeNamespace</key>
<string>ggxqDx</string>
<key>INIntentParameterPromptDialogs</key>
<array>
<dict>
<key>INIntentParameterPromptDialogCustom</key>
<true/>
<key>INIntentParameterPromptDialogType</key>
<string>Configuration</string>
</dict>
<dict>
<key>INIntentParameterPromptDialogCustom</key>
<true/>
<key>INIntentParameterPromptDialogFormatString</key>
<string>This is the mandatory prompt for your ${timer}</string>
<key>INIntentParameterPromptDialogFormatStringID</key>
<string>6ZcaR8</string>
<key>INIntentParameterPromptDialogType</key>
<string>Primary</string>
</dict>
</array>
<key>INIntentParameterSupportsResolution</key>
<true/>
<key>INIntentParameterTag</key>
<integer>2</integer>
<key>INIntentParameterType</key>
<string>Object</string>
</dict>
</array>
<key>INIntentResponse</key>
<dict>
<key>INIntentResponseCodes</key>
<array>
<dict>
<key>INIntentResponseCodeFormatString</key>
<string>${timer} has been launched</string>
<key>INIntentResponseCodeFormatStringID</key>
<string>E3Sz5n</string>
<key>INIntentResponseCodeName</key>
<string>success</string>
<key>INIntentResponseCodeSuccess</key>
<true/>
</dict>
<dict>
<key>INIntentResponseCodeName</key>
<string>failure</string>
</dict>
</array>
<key>INIntentResponseLastParameterTag</key>
<integer>2</integer>
<key>INIntentResponseParameters</key>
<array>
<dict>
<key>INIntentResponseParameterDisplayName</key>
<string>Timer</string>
<key>INIntentResponseParameterDisplayNameID</key>
<string>Vfpf1t</string>
<key>INIntentResponseParameterDisplayPriority</key>
<integer>1</integer>
<key>INIntentResponseParameterName</key>
<string>timer</string>
<key>INIntentResponseParameterObjectType</key>
<string>TimerIdentifier</string>
<key>INIntentResponseParameterObjectTypeNamespace</key>
<string>ggxqDx</string>
<key>INIntentResponseParameterTag</key>
<integer>2</integer>
<key>INIntentResponseParameterType</key>
<string>Object</string>
</dict>
</array>
</dict>
<key>INIntentTitle</key>
<string>Launch Timer</string>
<key>INIntentTitleID</key>
<string>nrTIGB</string>
<key>INIntentType</key>
<string>Custom</string>
<key>INIntentVerb</key>
<string>Start</string>
</dict>
</array>
<key>INTypes</key>
<array>
<dict>
<key>INTypeDisplayName</key>
<string>Timer Identifier</string>
<key>INTypeDisplayNameID</key>
<string>02RXTq</string>
<key>INTypeLastPropertyTag</key>
<integer>99</integer>
<key>INTypeName</key>
<string>TimerIdentifier</string>
<key>INTypeProperties</key>
<array>
<dict>
<key>INTypePropertyDefault</key>
<true/>
<key>INTypePropertyDisplayPriority</key>
<integer>1</integer>
<key>INTypePropertyName</key>
<string>identifier</string>
<key>INTypePropertyTag</key>
<integer>1</integer>
<key>INTypePropertyType</key>
<string>String</string>
</dict>
<dict>
<key>INTypePropertyDefault</key>
<true/>
<key>INTypePropertyDisplayPriority</key>
<integer>2</integer>
<key>INTypePropertyName</key>
<string>displayString</string>
<key>INTypePropertyTag</key>
<integer>2</integer>
<key>INTypePropertyType</key>
<string>String</string>
</dict>
<dict>
<key>INTypePropertyDefault</key>
<true/>
<key>INTypePropertyDisplayPriority</key>
<integer>3</integer>
<key>INTypePropertyName</key>
<string>pronunciationHint</string>
<key>INTypePropertyTag</key>
<integer>3</integer>
<key>INTypePropertyType</key>
<string>String</string>
</dict>
<dict>
<key>INTypePropertyDefault</key>
<true/>
<key>INTypePropertyDisplayPriority</key>
<integer>4</integer>
<key>INTypePropertyName</key>
<string>alternativeSpeakableMatches</string>
<key>INTypePropertySupportsMultipleValues</key>
<true/>
<key>INTypePropertyTag</key>
<integer>4</integer>
<key>INTypePropertyType</key>
<string>SpeakableString</string>
</dict>
</array>
</dict>
</array>
</dict>
</plist>

@ -125,7 +125,7 @@ class Conductor: ObservableObject {
// self._launchLiveActivity(countdown: countdown, endDate: date)
self._createTimerIntent(countdown)
// self._createTimerIntent(countdown)
Logger.log("countdowns count = \(self.currentCountdowns.count)")
@ -156,7 +156,7 @@ class Conductor: ObservableObject {
Conductor.maestro.currentStopwatches[stopwatch.stringId] = now
self._launchLiveActivity(stopwatch: stopwatch, start: now)
self._createTimerIntent(stopwatch)
// self._createTimerIntent(stopwatch)
}
}
@ -233,16 +233,16 @@ class Conductor: ObservableObject {
// MARK: - Intent
fileprivate func _createTimerIntent(_ timer: AbstractTimer) {
let intent = LaunchTimerIntent()
let invocationPhrase = String(format: NSLocalizedString("Launch %@", comment: ""), timer.displayName)
intent.suggestedInvocationPhrase = String(format: invocationPhrase, timer.displayName)
intent.timer = TimerIdentifier(identifier: timer.stringId, display: timer.displayName)
let interaction = INInteraction(intent: intent, response: nil)
interaction.donate()
}
// fileprivate func _createTimerIntent(_ timer: AbstractTimer) {
// let intent = LaunchTimerIntent()
//
// let invocationPhrase = String(format: NSLocalizedString("Launch %@", comment: ""), timer.displayName)
// intent.suggestedInvocationPhrase = String(format: invocationPhrase, timer.displayName)
// intent.timer = TimerIdentifier(identifier: timer.stringId, display: timer.displayName)
//
// let interaction = INInteraction(intent: intent, response: nil)
// interaction.donate()
// }
fileprivate func _scheduleAppRefresh(countdown: Countdown) {
let request = BGAppRefreshTaskRequest(identifier: BGTaskIdentifier.refresh.rawValue)

@ -2,6 +2,19 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>INAlternativeAppNames</key>
<array>
<array>
<dict>
<key>INAlternativeAppName</key>
<string>Momo</string>
</dict>
</array>
<dict>
<key>INAlternativeAppName</key>
<string>Gogo</string>
</dict>
</array>
<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
<string>com.staxriver.lecountdown.refresh</string>

@ -0,0 +1,68 @@
//
// LaunchTimer.swift
// LeCountdown
//
// Created by Laurent Morvillier on 07/03/2023.
//
import Foundation
import AppIntents
import SwiftUI
@available(iOS 16.0, macOS 13.0, watchOS 9.0, tvOS 16.0, *)
struct StartTimerIntent: AppIntent, CustomIntentMigratedAppIntent {
static let intentClassName = "StartTimerIntent"
static var title: LocalizedStringResource = "Launch Timer"
static var description = IntentDescription("Launch timers and stopwatches")
@Parameter(title: "Timer")
var timer: TimerIdentifierAppEntity?
static var parameterSummary: some ParameterSummary {
Summary()
}
func perform() async throws -> some IntentResult & ProvidesDialog & ShowsSnippetView {
let timerIdentifier: TimerIdentifierAppEntity
if let timer {
timerIdentifier = timer
} else {
timerIdentifier = try await $timer.requestDisambiguation(among: timerEntities())
}
if let abstractTimer = IntentDataProvider.main.timer(id: timerIdentifier.id) {
do {
let _ = try await TimerRouter.performAction(timer: abstractTimer)
print("perform() success !")
return .result(dialog: "Timer \(abstractTimer.displayName) started")
} catch {
Logger.error(error)
let dialog: IntentDialog = "\(error.localizedDescription)"
return .result(dialog: dialog)
}
}
return .result(dialog: "The timer has not been found in the app")
}
func timerEntities() -> [TimerIdentifierAppEntity] {
do {
let timers: [AbstractTimer] = try IntentDataProvider.main.timers()
return timers.map { TimerIdentifierAppEntity(id: $0.stringId, displayString: $0.displayName) }
} catch {
return []
}
}
}
//fileprivate extension IntentDialog {
// static func timerParameterPrompt(timer: TimerIdentifierAppEntity) -> Self {
// "This is the mandatory prompt for your \(timer)"
// }
// static func responseSuccess(timer: TimerIdentifierAppEntity) -> Self {
// "\(timer) has been launched"
// }
//}

@ -0,0 +1,44 @@
//
// TimerIdentifierAppEntity.swift
// LeCountdown
//
// Created by Laurent Morvillier on 07/03/2023.
//
import Foundation
import AppIntents
@available(iOS 16.0, macOS 13.0, watchOS 9.0, tvOS 16.0, *)
struct TimerIdentifierAppEntity: AppEntity {
static var typeDisplayRepresentation = TypeDisplayRepresentation(name: "Timer Identifier")
struct TimerIdentifierAppEntityQuery: EntityQuery {
func entities(for identifiers: [TimerIdentifierAppEntity.ID]) async throws -> [TimerIdentifierAppEntity] {
let timers = identifiers.compactMap { IntentDataProvider.main.timer(id: $0) }
return timers.map { TimerIdentifierAppEntity(id: $0.stringId, displayString: $0.displayName) }
// TODO: return TimerIdentifierAppEntity entities with the specified identifiers here.
}
func suggestedEntities() async throws -> [TimerIdentifierAppEntity] {
// TODO: return likely TimerIdentifierAppEntity entities here.
// This method is optional; the default implementation returns an empty array.
let timers = try IntentDataProvider.main.timers()
return timers.map { TimerIdentifierAppEntity(id: $0.stringId, displayString: $0.displayName) }
}
}
static var defaultQuery = TimerIdentifierAppEntityQuery()
var id: String // if your identifier is not a String, conform the entity to EntityIdentifierConvertible.
var displayString: String
var displayRepresentation: DisplayRepresentation {
DisplayRepresentation(title: "\(displayString)")
}
init(id: String, displayString: String) {
self.id = id
self.displayString = displayString
}
}

@ -0,0 +1,21 @@
//
// TimerShortcuts.swift
// LeCountdown
//
// Created by Laurent Morvillier on 07/03/2023.
//
import AppIntents
struct TimerShortcuts: AppShortcutsProvider {
static var appShortcuts: [AppShortcut] {
AppShortcut(intent: StartTimerIntent(), phrases: [
"Start \(.applicationName)",
"Launch \(.applicationName)",
"Start \(\.$timer) with \(.applicationName)",
"Launch \(\.$timer) with \(.applicationName)",
"\(.applicationName) \(\.$timer)"])
}
}

@ -9,6 +9,7 @@ import SwiftUI
import CoreData
import BackgroundTasks
import AVFoundation
import Combine
@main
struct LeCountdownApp: App {
@ -16,7 +17,7 @@ struct LeCountdownApp: App {
let persistenceController = PersistenceController.shared
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
init() {
UITabBar.appearance().backgroundColor = UIColor(white: 0.96, alpha: 1.0)
@ -24,6 +25,7 @@ struct LeCountdownApp: App {
UIPageControl.appearance().pageIndicatorTintColor = UIColor(white: 0.7, alpha: 1.0)
self._registerBackgroundRefreshes()
}
@Environment(\.scenePhase) var scenePhase
@ -68,10 +70,6 @@ struct LeCountdownApp: App {
}
// fileprivate func _willEnterForegroundNotification() {
// Conductor.maestro.cleanup()
// }
fileprivate func _onAppear() {
Logger.log("preferredLanguages = \(String(describing: Locale.preferredLanguages))")

@ -14,6 +14,7 @@ enum PreferenceKey: String {
case soundDurations
case lastSoundPlayed
case tips
case timerSiriTips
}
class Preferences {
@ -22,6 +23,7 @@ class Preferences {
@UserDefault(PreferenceKey.soundDurations.rawValue, defaultValue: [:]) static var soundDurations: [Int : TimeInterval]
@UserDefault(PreferenceKey.lastSoundPlayed.rawValue, defaultValue: [:]) static var lastSelectedSound: [String : Int]
@UserDefault(PreferenceKey.tips.rawValue, defaultValue: nil) static var lastShownTip: Int?
@UserDefault(PreferenceKey.timerSiriTips.rawValue, defaultValue: []) static var timerSiriTips: Set<String>
static var hideSilentModeAlerts: Bool {
return UserDefaults.standard.bool(forKey: PreferenceKey.showSilentModeAlert.rawValue)

@ -8,7 +8,7 @@
import Foundation
enum Tip: Int, CaseIterable, Identifiable {
case siri
// case siri
case widget
var id: Int { self.rawValue }
@ -16,13 +16,13 @@ enum Tip: Int, CaseIterable, Identifiable {
var localizedString: String {
switch self {
case .widget: return NSLocalizedString("You can add widget for your timers and countdowns by modifying your home or lock screen", comment: "")
case .siri: return NSLocalizedString("You can ask Siri to create and launch countdowns and stopwatches", comment: "")
// case .siri: return NSLocalizedString("You can ask Siri to create and launch countdowns and stopwatches", comment: "")
}
}
var pictoName: String {
switch self {
case .siri: return "wave.3.right" //"dot.radiowaves.right"
// case .siri: return "wave.3.right" //"dot.radiowaves.right"
case .widget: return "app.badge"
}
}

@ -8,6 +8,7 @@
import SwiftUI
import CoreData
import Combine
import _AppIntents_SwiftUI
class BoringContext : ObservableObject {
@ -57,6 +58,8 @@ struct ContentView<T : AbstractTimer>: View {
@State private var tipsShown: Bool = false
@State private var siriTipShown: Bool = false
fileprivate let itemSpacing: CGFloat = 10.0
var body: some View {
@ -75,9 +78,11 @@ struct ContentView<T : AbstractTimer>: View {
spacing: itemSpacing
) {
ReorderableForEach(items: Array(timers)) { timer in
ReorderableForEach(items: Array(self.timers)) { timer in
DialView(timer: timer, isEditingBinding: self.$isEditing, frameSize: width)
DialView(timer: timer, isEditingBinding: self.$isEditing, frameSize: width, handler: { id in
self._handleSiriTips(timerId: id)
})
.environment(\.managedObjectContext, viewContext)
.environmentObject(Conductor.maestro)
.environmentObject(boringContext)
@ -88,11 +93,14 @@ struct ContentView<T : AbstractTimer>: View {
}
}.padding(.horizontal, itemSpacing)
if !self.tipsShown, let tip = Preferences.tipToShow {
TipView(tip: tip) {
self._hideTip(tip)
}.padding()
}
// if !self.tipsShown, let tip = Preferences.tipToShow {
// TipView(tip: tip) {
// self._hideTip(tip)
// }.padding()
// }
SiriTipView(intent: StartTimerIntent(), isVisible: self.$siriTipShown)
.siriTipViewStyle(SiriTipViewStyle.dark).padding()
if !conductor.liveTimers.isEmpty {
LiveTimerListView()
@ -188,6 +196,13 @@ struct ContentView<T : AbstractTimer>: View {
}
}
fileprivate func _handleSiriTips(timerId: String) {
if !Preferences.timerSiriTips.contains(timerId) {
self.siriTipShown = true
Preferences.timerSiriTips.insert(timerId)
}
}
fileprivate func _performActionIfPossible(url: URL) {
// hide new window if launching a timer

@ -21,10 +21,14 @@ struct DialView: View {
var frameSize: CGFloat
var handler: ((String) -> ())? = nil
var body: some View {
ZStack {
Image(self.timer.imageName).resizable().saturation(self.isEditingBinding.wrappedValue ? 0.0 : 1.0)
Image(self.timer.imageName)
.resizable()
.saturation(self.isEditingBinding.wrappedValue ? 0.0 : 1.0)
switch self.isEditingBinding.wrappedValue {
case false:
@ -76,7 +80,8 @@ struct DialView: View {
.frame(width: frameSize, height: 80.0)
.cornerRadius(20.0)
.alert("Make sure your device is not on silent mode", isPresented: $showSilentModeAlert) {
Button("OK", role: .cancel) { self._launchTimer()
Button("OK", role: .cancel) {
self._launchTimer()
}
Button("Don't show this again", role: .destructive) {
self._launchTimer()
@ -126,6 +131,8 @@ struct DialView: View {
fileprivate func _launchTimer() {
self.handler?(self.timer.stringId)
TimerRouter.performAction(timer: self.timer) { result in
switch result {
case .success:

@ -7,6 +7,7 @@
import SwiftUI
import CoreSpotlight
import Combine
struct CompactHomeView: View {

@ -0,0 +1,12 @@
/*
AppShortcuts.strings
LeCountdown
Created by Laurent Morvillier on 07/03/2023.
*/
"Start ${applicationName}" = "Start ${applicationName}";
"Launch ${applicationName}" = "Launch ${applicationName}";
"Start ${timer} with ${applicationName}" = "Start ${timer} with ${applicationName}";
"Launch ${timer} with ${applicationName}" = "Launch ${timer} with ${applicationName}";
"${applicationName} ${timer}" = "${applicationName} ${timer}";

@ -0,0 +1,12 @@
/*
AppShortcuts.strings
LeCountdown
Created by Laurent Morvillier on 07/03/2023.
*/
"Start ${applicationName}" = "Démarre ${applicationName}";
"Launch ${applicationName}" = "Lance ${applicationName}";
"Start ${timer} with ${applicationName}" = "Démarre ${timer} avec ${applicationName}";
"Launch ${timer} with ${applicationName}" = "Lance ${timer} avec ${applicationName}";
"${applicationName} ${timer}" = "${applicationName} ${timer}";

@ -92,7 +92,7 @@
"Hard boiled eggs" = "Oeufs dur";
/* No comment provided by engineer. */
"Home" = "Kikai";
"Home" = "Home";
/* No comment provided by engineer. */
"It's time!" = "C'est l'heure !";
@ -243,3 +243,6 @@
"Launch" = "Démarrer";
"Launch %@" = "Lance %@";
"Timer %@ started" = "Le minuteur %@ a démarré";
"The timer has not been found in the app" = "Le minuteur n'a pas été trouvé dans l'app";

@ -1,7 +0,0 @@
"Launch Timer" = "Démarrer un compteur";
"Launch timers and stopwatches" = "Démarre des minuteurs et chronos";
"Timer" = "Compteur";
"Starts immediately" = "Démarre immédiatement";
"Launch ${timer}" = "Démarre ${timer}";
"${timer} has been launched" = "${timer} a démarré";
Loading…
Cancel
Save