├── Forest.xcodeproj ├── project.pbxproj └── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── Forest ├── App │ └── ForestApp.swift ├── Asset+LocalizedPointOfInterest.swift ├── Asset+VideoVariant.swift ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json ├── Collection+Identifiable.swift ├── Entries.swift ├── Forest.entitlements ├── Info.plist ├── Resources.swift ├── Tar.swift └── UI │ ├── AssetNavigationLink.swift │ ├── AssetView.swift │ ├── AssociatedAssetsView.swift │ ├── ContentView.swift │ ├── PlayerView.swift │ ├── PointsOfInterestTable.swift │ ├── ShuffleNavigationLink.swift │ ├── ShuffleSection.swift │ └── SidebarSectionHeader.swift ├── LICENSE.md ├── README.md └── Screenshots ├── iOS_detail-dark.png ├── iOS_detail-light.png ├── iOS_home-dark.png ├── iOS_home-light.png ├── macOS_single-dark.png ├── macOS_single-light.png ├── macOS_tabs-dark.png └── macOS_tabs-light.png /Forest.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 54; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | FA5B6AEA2662D3CA006461A0 /* AssetNavigationLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA5B6AE92662D3CA006461A0 /* AssetNavigationLink.swift */; }; 11 | FAB57F3826EA708200DEAD76 /* Collection+Identifiable.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAB57F3726EA708200DEAD76 /* Collection+Identifiable.swift */; }; 12 | FAB57F3B26EA806700DEAD76 /* PointsOfInterestTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAB57F3A26EA806700DEAD76 /* PointsOfInterestTable.swift */; }; 13 | FAB57F4126EBA06A00DEAD76 /* ShuffleSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAB57F4026EBA06A00DEAD76 /* ShuffleSection.swift */; }; 14 | FAB57F4426EBACF700DEAD76 /* ShuffleNavigationLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAB57F4326EBACF700DEAD76 /* ShuffleNavigationLink.swift */; }; 15 | FAB57F4726EC500800DEAD76 /* Asset+VideoVariant.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAB57F4626EC500800DEAD76 /* Asset+VideoVariant.swift */; }; 16 | FAB57F4A26ECE9CA00DEAD76 /* Asset+LocalizedPointOfInterest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAB57F4926ECE9CA00DEAD76 /* Asset+LocalizedPointOfInterest.swift */; }; 17 | FAB57F4D26ED069D00DEAD76 /* SidebarSectionHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAB57F4C26ED069D00DEAD76 /* SidebarSectionHeader.swift */; }; 18 | FAFE98FE2650907100FDDD2E /* ForestApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAFE98EB2650906F00FDDD2E /* ForestApp.swift */; }; 19 | FAFE99002650907100FDDD2E /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAFE98EC2650906F00FDDD2E /* ContentView.swift */; }; 20 | FAFE99022650907100FDDD2E /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = FAFE98ED2650907100FDDD2E /* Assets.xcassets */; }; 21 | FAFE990D265098FD00FDDD2E /* Resources.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAFE990C265098FD00FDDD2E /* Resources.swift */; }; 22 | FAFE99102650997500FDDD2E /* Tar.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAFE990F2650997500FDDD2E /* Tar.swift */; }; 23 | FAFE991326509B6500FDDD2E /* Entries.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAFE991226509B6500FDDD2E /* Entries.swift */; }; 24 | FAFE991626509C2D00FDDD2E /* PlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAFE991526509C2D00FDDD2E /* PlayerView.swift */; }; 25 | FAFE991E26509F9400FDDD2E /* AssociatedAssetsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAFE991D26509F9400FDDD2E /* AssociatedAssetsView.swift */; }; 26 | FAFE99212650A02900FDDD2E /* AssetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAFE99202650A02900FDDD2E /* AssetView.swift */; }; 27 | /* End PBXBuildFile section */ 28 | 29 | /* Begin PBXFileReference section */ 30 | FA5B6AE92662D3CA006461A0 /* AssetNavigationLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetNavigationLink.swift; sourceTree = ""; }; 31 | FAB57F3726EA708200DEAD76 /* Collection+Identifiable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Collection+Identifiable.swift"; sourceTree = ""; }; 32 | FAB57F3A26EA806700DEAD76 /* PointsOfInterestTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointsOfInterestTable.swift; sourceTree = ""; }; 33 | FAB57F4026EBA06A00DEAD76 /* ShuffleSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShuffleSection.swift; sourceTree = ""; }; 34 | FAB57F4326EBACF700DEAD76 /* ShuffleNavigationLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShuffleNavigationLink.swift; sourceTree = ""; }; 35 | FAB57F4626EC500800DEAD76 /* Asset+VideoVariant.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Asset+VideoVariant.swift"; sourceTree = ""; }; 36 | FAB57F4926ECE9CA00DEAD76 /* Asset+LocalizedPointOfInterest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Asset+LocalizedPointOfInterest.swift"; sourceTree = ""; }; 37 | FAB57F4C26ED069D00DEAD76 /* SidebarSectionHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarSectionHeader.swift; sourceTree = ""; }; 38 | FAFE98EB2650906F00FDDD2E /* ForestApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForestApp.swift; sourceTree = ""; }; 39 | FAFE98EC2650906F00FDDD2E /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 40 | FAFE98ED2650907100FDDD2E /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 41 | FAFE98F22650907100FDDD2E /* Forest.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Forest.app; sourceTree = BUILT_PRODUCTS_DIR; }; 42 | FAFE98F52650907100FDDD2E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 43 | FAFE98FD2650907100FDDD2E /* Forest.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Forest.entitlements; sourceTree = ""; }; 44 | FAFE990C265098FD00FDDD2E /* Resources.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Resources.swift; sourceTree = ""; }; 45 | FAFE990F2650997500FDDD2E /* Tar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tar.swift; sourceTree = ""; }; 46 | FAFE991226509B6500FDDD2E /* Entries.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Entries.swift; sourceTree = ""; }; 47 | FAFE991526509C2D00FDDD2E /* PlayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerView.swift; sourceTree = ""; }; 48 | FAFE991D26509F9400FDDD2E /* AssociatedAssetsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssociatedAssetsView.swift; sourceTree = ""; }; 49 | FAFE99202650A02900FDDD2E /* AssetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetView.swift; sourceTree = ""; }; 50 | /* End PBXFileReference section */ 51 | 52 | /* Begin PBXFrameworksBuildPhase section */ 53 | FAFE98EF2650907100FDDD2E /* Frameworks */ = { 54 | isa = PBXFrameworksBuildPhase; 55 | buildActionMask = 2147483647; 56 | files = ( 57 | ); 58 | runOnlyForDeploymentPostprocessing = 0; 59 | }; 60 | /* End PBXFrameworksBuildPhase section */ 61 | 62 | /* Begin PBXGroup section */ 63 | FAFE98E52650906F00FDDD2E = { 64 | isa = PBXGroup; 65 | children = ( 66 | FAFE98EA2650906F00FDDD2E /* Forest */, 67 | FAFE98F32650907100FDDD2E /* Products */, 68 | ); 69 | sourceTree = ""; 70 | }; 71 | FAFE98EA2650906F00FDDD2E /* Forest */ = { 72 | isa = PBXGroup; 73 | children = ( 74 | FAFE991826509D9B00FDDD2E /* App */, 75 | FAFE991926509DA300FDDD2E /* UI */, 76 | FAFE990C265098FD00FDDD2E /* Resources.swift */, 77 | FAFE991226509B6500FDDD2E /* Entries.swift */, 78 | FAB57F3726EA708200DEAD76 /* Collection+Identifiable.swift */, 79 | FAB57F4626EC500800DEAD76 /* Asset+VideoVariant.swift */, 80 | FAB57F4926ECE9CA00DEAD76 /* Asset+LocalizedPointOfInterest.swift */, 81 | FAFE990F2650997500FDDD2E /* Tar.swift */, 82 | FAFE98ED2650907100FDDD2E /* Assets.xcassets */, 83 | FAFE98F52650907100FDDD2E /* Info.plist */, 84 | FAFE98FD2650907100FDDD2E /* Forest.entitlements */, 85 | ); 86 | path = Forest; 87 | sourceTree = ""; 88 | }; 89 | FAFE98F32650907100FDDD2E /* Products */ = { 90 | isa = PBXGroup; 91 | children = ( 92 | FAFE98F22650907100FDDD2E /* Forest.app */, 93 | ); 94 | name = Products; 95 | sourceTree = ""; 96 | }; 97 | FAFE991826509D9B00FDDD2E /* App */ = { 98 | isa = PBXGroup; 99 | children = ( 100 | FAFE98EB2650906F00FDDD2E /* ForestApp.swift */, 101 | ); 102 | path = App; 103 | sourceTree = ""; 104 | }; 105 | FAFE991926509DA300FDDD2E /* UI */ = { 106 | isa = PBXGroup; 107 | children = ( 108 | FAFE98EC2650906F00FDDD2E /* ContentView.swift */, 109 | FAB57F4C26ED069D00DEAD76 /* SidebarSectionHeader.swift */, 110 | FAB57F4026EBA06A00DEAD76 /* ShuffleSection.swift */, 111 | FAB57F4326EBACF700DEAD76 /* ShuffleNavigationLink.swift */, 112 | FAFE991D26509F9400FDDD2E /* AssociatedAssetsView.swift */, 113 | FA5B6AE92662D3CA006461A0 /* AssetNavigationLink.swift */, 114 | FAFE99202650A02900FDDD2E /* AssetView.swift */, 115 | FAB57F3A26EA806700DEAD76 /* PointsOfInterestTable.swift */, 116 | FAFE991526509C2D00FDDD2E /* PlayerView.swift */, 117 | ); 118 | path = UI; 119 | sourceTree = ""; 120 | }; 121 | /* End PBXGroup section */ 122 | 123 | /* Begin PBXNativeTarget section */ 124 | FAFE98F12650907100FDDD2E /* Forest */ = { 125 | isa = PBXNativeTarget; 126 | buildConfigurationList = FAFE99062650907100FDDD2E /* Build configuration list for PBXNativeTarget "Forest" */; 127 | buildPhases = ( 128 | FAFE98EE2650907100FDDD2E /* Sources */, 129 | FAFE98EF2650907100FDDD2E /* Frameworks */, 130 | FAFE98F02650907100FDDD2E /* Resources */, 131 | ); 132 | buildRules = ( 133 | ); 134 | dependencies = ( 135 | ); 136 | name = Forest; 137 | productName = "Forest (iOS)"; 138 | productReference = FAFE98F22650907100FDDD2E /* Forest.app */; 139 | productType = "com.apple.product-type.application"; 140 | }; 141 | /* End PBXNativeTarget section */ 142 | 143 | /* Begin PBXProject section */ 144 | FAFE98E62650906F00FDDD2E /* Project object */ = { 145 | isa = PBXProject; 146 | attributes = { 147 | BuildIndependentTargetsInParallel = YES; 148 | LastSwiftUpdateCheck = 1250; 149 | LastUpgradeCheck = 1530; 150 | ORGANIZATIONNAME = Leptos; 151 | TargetAttributes = { 152 | FAFE98F12650907100FDDD2E = { 153 | CreatedOnToolsVersion = 12.5; 154 | }; 155 | }; 156 | }; 157 | buildConfigurationList = FAFE98E92650906F00FDDD2E /* Build configuration list for PBXProject "Forest" */; 158 | compatibilityVersion = "Xcode 9.3"; 159 | developmentRegion = en; 160 | hasScannedForEncodings = 0; 161 | knownRegions = ( 162 | en, 163 | Base, 164 | ); 165 | mainGroup = FAFE98E52650906F00FDDD2E; 166 | productRefGroup = FAFE98F32650907100FDDD2E /* Products */; 167 | projectDirPath = ""; 168 | projectRoot = ""; 169 | targets = ( 170 | FAFE98F12650907100FDDD2E /* Forest */, 171 | ); 172 | }; 173 | /* End PBXProject section */ 174 | 175 | /* Begin PBXResourcesBuildPhase section */ 176 | FAFE98F02650907100FDDD2E /* Resources */ = { 177 | isa = PBXResourcesBuildPhase; 178 | buildActionMask = 2147483647; 179 | files = ( 180 | FAFE99022650907100FDDD2E /* Assets.xcassets in Resources */, 181 | ); 182 | runOnlyForDeploymentPostprocessing = 0; 183 | }; 184 | /* End PBXResourcesBuildPhase section */ 185 | 186 | /* Begin PBXSourcesBuildPhase section */ 187 | FAFE98EE2650907100FDDD2E /* Sources */ = { 188 | isa = PBXSourcesBuildPhase; 189 | buildActionMask = 2147483647; 190 | files = ( 191 | FAFE99002650907100FDDD2E /* ContentView.swift in Sources */, 192 | FAFE990D265098FD00FDDD2E /* Resources.swift in Sources */, 193 | FAFE99212650A02900FDDD2E /* AssetView.swift in Sources */, 194 | FAFE991326509B6500FDDD2E /* Entries.swift in Sources */, 195 | FAB57F4726EC500800DEAD76 /* Asset+VideoVariant.swift in Sources */, 196 | FAB57F4126EBA06A00DEAD76 /* ShuffleSection.swift in Sources */, 197 | FAB57F4426EBACF700DEAD76 /* ShuffleNavigationLink.swift in Sources */, 198 | FAFE991E26509F9400FDDD2E /* AssociatedAssetsView.swift in Sources */, 199 | FAB57F3826EA708200DEAD76 /* Collection+Identifiable.swift in Sources */, 200 | FAFE991626509C2D00FDDD2E /* PlayerView.swift in Sources */, 201 | FA5B6AEA2662D3CA006461A0 /* AssetNavigationLink.swift in Sources */, 202 | FAB57F3B26EA806700DEAD76 /* PointsOfInterestTable.swift in Sources */, 203 | FAFE98FE2650907100FDDD2E /* ForestApp.swift in Sources */, 204 | FAB57F4D26ED069D00DEAD76 /* SidebarSectionHeader.swift in Sources */, 205 | FAFE99102650997500FDDD2E /* Tar.swift in Sources */, 206 | FAB57F4A26ECE9CA00DEAD76 /* Asset+LocalizedPointOfInterest.swift in Sources */, 207 | ); 208 | runOnlyForDeploymentPostprocessing = 0; 209 | }; 210 | /* End PBXSourcesBuildPhase section */ 211 | 212 | /* Begin XCBuildConfiguration section */ 213 | FAFE99042650907100FDDD2E /* Debug */ = { 214 | isa = XCBuildConfiguration; 215 | buildSettings = { 216 | ALWAYS_SEARCH_USER_PATHS = NO; 217 | CLANG_ANALYZER_NONNULL = YES; 218 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 219 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 220 | CLANG_CXX_LIBRARY = "libc++"; 221 | CLANG_ENABLE_MODULES = YES; 222 | CLANG_ENABLE_OBJC_ARC = YES; 223 | CLANG_ENABLE_OBJC_WEAK = YES; 224 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 225 | CLANG_WARN_BOOL_CONVERSION = YES; 226 | CLANG_WARN_COMMA = YES; 227 | CLANG_WARN_CONSTANT_CONVERSION = YES; 228 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 229 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 230 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 231 | CLANG_WARN_EMPTY_BODY = YES; 232 | CLANG_WARN_ENUM_CONVERSION = YES; 233 | CLANG_WARN_INFINITE_RECURSION = YES; 234 | CLANG_WARN_INT_CONVERSION = YES; 235 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 236 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 237 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 238 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 239 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 240 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 241 | CLANG_WARN_STRICT_PROTOTYPES = YES; 242 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 243 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 244 | CLANG_WARN_UNREACHABLE_CODE = YES; 245 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 246 | COPY_PHASE_STRIP = NO; 247 | CURRENT_PROJECT_VERSION = 1; 248 | DEAD_CODE_STRIPPING = YES; 249 | DEBUG_INFORMATION_FORMAT = dwarf; 250 | ENABLE_STRICT_OBJC_MSGSEND = YES; 251 | ENABLE_TESTABILITY = YES; 252 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 253 | GCC_C_LANGUAGE_STANDARD = gnu11; 254 | GCC_DYNAMIC_NO_PIC = NO; 255 | GCC_NO_COMMON_BLOCKS = YES; 256 | GCC_OPTIMIZATION_LEVEL = 0; 257 | GCC_PREPROCESSOR_DEFINITIONS = ( 258 | "DEBUG=1", 259 | "$(inherited)", 260 | ); 261 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 262 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 263 | GCC_WARN_UNDECLARED_SELECTOR = YES; 264 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 265 | GCC_WARN_UNUSED_FUNCTION = YES; 266 | GCC_WARN_UNUSED_VARIABLE = YES; 267 | MARKETING_VERSION = 1.0; 268 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 269 | MTL_FAST_MATH = YES; 270 | ONLY_ACTIVE_ARCH = YES; 271 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 272 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 273 | }; 274 | name = Debug; 275 | }; 276 | FAFE99052650907100FDDD2E /* Release */ = { 277 | isa = XCBuildConfiguration; 278 | buildSettings = { 279 | ALWAYS_SEARCH_USER_PATHS = NO; 280 | CLANG_ANALYZER_NONNULL = YES; 281 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 282 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 283 | CLANG_CXX_LIBRARY = "libc++"; 284 | CLANG_ENABLE_MODULES = YES; 285 | CLANG_ENABLE_OBJC_ARC = YES; 286 | CLANG_ENABLE_OBJC_WEAK = YES; 287 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 288 | CLANG_WARN_BOOL_CONVERSION = YES; 289 | CLANG_WARN_COMMA = YES; 290 | CLANG_WARN_CONSTANT_CONVERSION = YES; 291 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 292 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 293 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 294 | CLANG_WARN_EMPTY_BODY = YES; 295 | CLANG_WARN_ENUM_CONVERSION = YES; 296 | CLANG_WARN_INFINITE_RECURSION = YES; 297 | CLANG_WARN_INT_CONVERSION = YES; 298 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 299 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 300 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 301 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 302 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 303 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 304 | CLANG_WARN_STRICT_PROTOTYPES = YES; 305 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 306 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 307 | CLANG_WARN_UNREACHABLE_CODE = YES; 308 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 309 | COPY_PHASE_STRIP = NO; 310 | CURRENT_PROJECT_VERSION = 1; 311 | DEAD_CODE_STRIPPING = YES; 312 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 313 | ENABLE_NS_ASSERTIONS = NO; 314 | ENABLE_STRICT_OBJC_MSGSEND = YES; 315 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 316 | GCC_C_LANGUAGE_STANDARD = gnu11; 317 | GCC_NO_COMMON_BLOCKS = YES; 318 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 319 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 320 | GCC_WARN_UNDECLARED_SELECTOR = YES; 321 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 322 | GCC_WARN_UNUSED_FUNCTION = YES; 323 | GCC_WARN_UNUSED_VARIABLE = YES; 324 | MARKETING_VERSION = 1.0; 325 | MTL_ENABLE_DEBUG_INFO = NO; 326 | MTL_FAST_MATH = YES; 327 | SWIFT_COMPILATION_MODE = wholemodule; 328 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 329 | }; 330 | name = Release; 331 | }; 332 | FAFE99072650907100FDDD2E /* Debug */ = { 333 | isa = XCBuildConfiguration; 334 | buildSettings = { 335 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 336 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 337 | CODE_SIGN_ENTITLEMENTS = Forest/Forest.entitlements; 338 | CODE_SIGN_STYLE = Automatic; 339 | DEVELOPMENT_TEAM = 7P56K8K4MY; 340 | ENABLE_PREVIEWS = YES; 341 | GENERATE_INFOPLIST_FILE = YES; 342 | INFOPLIST_FILE = Forest/Info.plist; 343 | INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.video"; 344 | IPHONEOS_DEPLOYMENT_TARGET = 14.0; 345 | LD_RUNPATH_SEARCH_PATHS = ( 346 | "$(inherited)", 347 | "@executable_path/Frameworks", 348 | ); 349 | MACOSX_DEPLOYMENT_TARGET = 11.0; 350 | PRODUCT_BUNDLE_IDENTIFIER = null.leptos.Forest; 351 | PRODUCT_NAME = Forest; 352 | SDKROOT = iphoneos; 353 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator"; 354 | SUPPORTS_MACCATALYST = NO; 355 | SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; 356 | SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; 357 | SWIFT_VERSION = 5.0; 358 | TARGETED_DEVICE_FAMILY = "1,2,7"; 359 | XROS_DEPLOYMENT_TARGET = 1.0; 360 | }; 361 | name = Debug; 362 | }; 363 | FAFE99082650907100FDDD2E /* Release */ = { 364 | isa = XCBuildConfiguration; 365 | buildSettings = { 366 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 367 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 368 | CODE_SIGN_ENTITLEMENTS = Forest/Forest.entitlements; 369 | CODE_SIGN_STYLE = Automatic; 370 | DEVELOPMENT_TEAM = 7P56K8K4MY; 371 | ENABLE_PREVIEWS = YES; 372 | GENERATE_INFOPLIST_FILE = YES; 373 | INFOPLIST_FILE = Forest/Info.plist; 374 | INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.video"; 375 | IPHONEOS_DEPLOYMENT_TARGET = 14.0; 376 | LD_RUNPATH_SEARCH_PATHS = ( 377 | "$(inherited)", 378 | "@executable_path/Frameworks", 379 | ); 380 | MACOSX_DEPLOYMENT_TARGET = 11.0; 381 | PRODUCT_BUNDLE_IDENTIFIER = null.leptos.Forest; 382 | PRODUCT_NAME = Forest; 383 | SDKROOT = iphoneos; 384 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator"; 385 | SUPPORTS_MACCATALYST = NO; 386 | SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; 387 | SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; 388 | SWIFT_VERSION = 5.0; 389 | TARGETED_DEVICE_FAMILY = "1,2,7"; 390 | VALIDATE_PRODUCT = YES; 391 | XROS_DEPLOYMENT_TARGET = 1.0; 392 | }; 393 | name = Release; 394 | }; 395 | /* End XCBuildConfiguration section */ 396 | 397 | /* Begin XCConfigurationList section */ 398 | FAFE98E92650906F00FDDD2E /* Build configuration list for PBXProject "Forest" */ = { 399 | isa = XCConfigurationList; 400 | buildConfigurations = ( 401 | FAFE99042650907100FDDD2E /* Debug */, 402 | FAFE99052650907100FDDD2E /* Release */, 403 | ); 404 | defaultConfigurationIsVisible = 0; 405 | defaultConfigurationName = Release; 406 | }; 407 | FAFE99062650907100FDDD2E /* Build configuration list for PBXNativeTarget "Forest" */ = { 408 | isa = XCConfigurationList; 409 | buildConfigurations = ( 410 | FAFE99072650907100FDDD2E /* Debug */, 411 | FAFE99082650907100FDDD2E /* Release */, 412 | ); 413 | defaultConfigurationIsVisible = 0; 414 | defaultConfigurationName = Release; 415 | }; 416 | /* End XCConfigurationList section */ 417 | }; 418 | rootObject = FAFE98E62650906F00FDDD2E /* Project object */; 419 | } 420 | -------------------------------------------------------------------------------- /Forest.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Forest.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Forest/App/ForestApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ForestApp.swift 3 | // Forest 4 | // 5 | // Created by Leptos on 5/15/21. 6 | // Copyright © 2021 Leptos. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | @main 12 | struct ForestApp: App { 13 | var resourceDescriptor: Resources.Descriptor { 14 | let supportDirectories = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask) 15 | guard let supportDirectory = supportDirectories.first else { fatalError("Could not locate resource directory") } 16 | let resourceURL = supportDirectory.appendingPathComponent("Forest/Resources-15") 17 | return Resources.Descriptor(directory: resourceURL) 18 | } 19 | 20 | var body: some Scene { 21 | WindowGroup { 22 | ContentView(resourceDescriptor: resourceDescriptor) 23 | // set some minimum dimensions to avoid very small sizes, and odd re-sizing during the download flow 24 | .frame(minWidth: 360, minHeight: 240) 25 | } 26 | .commands { 27 | SidebarCommands() 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Forest/Asset+LocalizedPointOfInterest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Asset+LocalizedPointOfInterest.swift 3 | // Forest 4 | // 5 | // Created by Leptos on 9/11/21. 6 | // Copyright © 2021 Leptos. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension Entries.Asset { 12 | struct LocalizedPointOfInterest: Codable, Hashable, Identifiable { 13 | let id: String 14 | let timestamp: String 15 | let value: String 16 | 17 | var timeInterval: TimeInterval { 18 | TimeInterval(timestamp) ?? .nan 19 | } 20 | } 21 | 22 | func decodePointsOfInterest(from bundle: Bundle) -> [LocalizedPointOfInterest] { 23 | pointsOfInterest.map { (timestamp, localizationKey) in 24 | let localizedString = bundle 25 | .localizedString(forKey: localizationKey, value: nil, table: "Localizable.nocache") 26 | .replacingOccurrences(of: "\n", with: " ") /* there are some random line breaks that don't seem meaningful */ 27 | return LocalizedPointOfInterest(id: localizationKey, timestamp: timestamp, value: localizedString) 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Forest/Asset+VideoVariant.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Asset+VideoVariant.swift 3 | // Forest 4 | // 5 | // Created by Leptos on 9/10/21. 6 | // Copyright © 2021 Leptos. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension Entries.Asset { 12 | enum VideoVariant: CaseIterable, Identifiable { 13 | case c_1080_H264 14 | case c_1080_HDR 15 | case c_1080_SDR 16 | case c_4K_HDR 17 | case c_4K_SDR 18 | 19 | var name: String { 20 | switch self { 21 | case .c_1080_H264: return "1080 H264" 22 | case .c_1080_HDR: return "1080 HDR" 23 | case .c_1080_SDR: return "1080 SDR" 24 | case .c_4K_HDR: return "4K HDR" 25 | case .c_4K_SDR: return "4K SDR" 26 | } 27 | } 28 | 29 | var id: Self { self } 30 | } 31 | 32 | func url(for videoVariant: VideoVariant) -> URL { 33 | switch videoVariant { 34 | case .c_1080_H264: return url_1080_H264 35 | case .c_1080_HDR: return url_1080_HDR 36 | case .c_1080_SDR: return url_1080_SDR 37 | case .c_4K_HDR: return url_4K_HDR 38 | case .c_4K_SDR: return url_4K_SDR 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Forest/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Forest/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "1x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "76x76" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "scale" : "2x", 86 | "size" : "83.5x83.5" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "scale" : "1x", 91 | "size" : "1024x1024" 92 | }, 93 | { 94 | "idiom" : "mac", 95 | "scale" : "1x", 96 | "size" : "16x16" 97 | }, 98 | { 99 | "idiom" : "mac", 100 | "scale" : "2x", 101 | "size" : "16x16" 102 | }, 103 | { 104 | "idiom" : "mac", 105 | "scale" : "1x", 106 | "size" : "32x32" 107 | }, 108 | { 109 | "idiom" : "mac", 110 | "scale" : "2x", 111 | "size" : "32x32" 112 | }, 113 | { 114 | "idiom" : "mac", 115 | "scale" : "1x", 116 | "size" : "128x128" 117 | }, 118 | { 119 | "idiom" : "mac", 120 | "scale" : "2x", 121 | "size" : "128x128" 122 | }, 123 | { 124 | "idiom" : "mac", 125 | "scale" : "1x", 126 | "size" : "256x256" 127 | }, 128 | { 129 | "idiom" : "mac", 130 | "scale" : "2x", 131 | "size" : "256x256" 132 | }, 133 | { 134 | "idiom" : "mac", 135 | "scale" : "1x", 136 | "size" : "512x512" 137 | }, 138 | { 139 | "idiom" : "mac", 140 | "scale" : "2x", 141 | "size" : "512x512" 142 | } 143 | ], 144 | "info" : { 145 | "author" : "xcode", 146 | "version" : 1 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /Forest/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Forest/Collection+Identifiable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Collection+Identifiable.swift 3 | // Forest 4 | // 5 | // Created by Leptos on 9/9/21. 6 | // Copyright © 2021 Leptos. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension Collection where Element: Identifiable { 12 | var identifiableLookup: [Element.ID: Element] { 13 | reduce(into: [:]) { lookup, element in 14 | lookup[element.id] = element 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Forest/Entries.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Entries.swift 3 | // Forest 4 | // 5 | // Created by Leptos on 5/15/21. 6 | // Copyright © 2021 Leptos. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct Entries: Codable { 12 | struct Asset: Codable, Hashable, Identifiable { 13 | enum CodingKeys: String, CodingKey { 14 | case id, accessibilityLabel, pointsOfInterest 15 | 16 | case categories, shotID 17 | 18 | case url_1080_H264 = "url-1080-H264" 19 | case url_1080_HDR = "url-1080-HDR" 20 | case url_1080_SDR = "url-1080-SDR" 21 | case url_4K_HDR = "url-4K-HDR" 22 | case url_4K_SDR = "url-4K-SDR" 23 | } 24 | 25 | let id: UUID 26 | let accessibilityLabel: String 27 | let pointsOfInterest: [String: String] 28 | 29 | let categories: [UUID]? // introduced in tvOS 15 30 | let shotID: String? // introduced in tvOS 15 31 | 32 | let url_1080_H264: URL 33 | let url_1080_HDR: URL 34 | let url_1080_SDR: URL 35 | let url_4K_HDR: URL 36 | let url_4K_SDR: URL 37 | 38 | func decodeCategories(from entries: Entries) -> [Category]? { 39 | guard let entriesCategories = entries.categories, 40 | let assetCategories = categories else { return nil } 41 | let categoryLookup = entriesCategories.identifiableLookup 42 | return assetCategories.compactMap { categoryLookup[$0] } 43 | } 44 | } 45 | 46 | struct Category: Codable, Hashable, Identifiable { 47 | let id: UUID 48 | let localizedDescriptionKey: String 49 | let localizedNameKey: String 50 | let preferredOrder: Int 51 | let previewImage: URL 52 | let representativeAssetID: UUID 53 | 54 | func localizedDescription(from bundle: Bundle) -> String { 55 | bundle.localizedString(forKey: localizedDescriptionKey, value: nil, table: "Localizable.nocache") 56 | } 57 | func localizedName(from bundle: Bundle) -> String { 58 | bundle.localizedString(forKey: localizedNameKey, value: nil, table: "Localizable.nocache") 59 | } 60 | func representativeAsset(in entries: Entries) -> Entries.Asset? { 61 | entries.assets.identifiableLookup[representativeAssetID] 62 | } 63 | } 64 | 65 | let assets: [Asset] 66 | let categories: [Category]? // introduced in tvOS 15 67 | let initialAssetCount: UInt 68 | let version: UInt 69 | } 70 | -------------------------------------------------------------------------------- /Forest/Forest.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.network.client 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Forest/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | $(MARKETING_VERSION) 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | LSApplicationCategoryType 22 | public.app-category.video 23 | UIApplicationSceneManifest 24 | 25 | UIApplicationSupportsMultipleScenes 26 | 27 | 28 | UIApplicationSupportsIndirectInputEvents 29 | 30 | UILaunchScreen 31 | 32 | UISupportedInterfaceOrientations 33 | 34 | UIInterfaceOrientationPortrait 35 | UIInterfaceOrientationLandscapeLeft 36 | UIInterfaceOrientationLandscapeRight 37 | UIInterfaceOrientationPortraitUpsideDown 38 | 39 | UISupportedInterfaceOrientations~ipad 40 | 41 | UIInterfaceOrientationPortrait 42 | UIInterfaceOrientationPortraitUpsideDown 43 | UIInterfaceOrientationLandscapeLeft 44 | UIInterfaceOrientationLandscapeRight 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /Forest/Resources.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Resources.swift 3 | // Forest 4 | // 5 | // Created by Leptos on 5/15/21. 6 | // Copyright © 2021 Leptos. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct Resources { 12 | struct Descriptor { 13 | let directory: URL 14 | 15 | var remoteResourceLocation: URL { 16 | // thanks to https://github.com/JohnCoates/Aerial/blob/master/Documentation/OfflineMode.md 17 | guard let url = URL(string: "https://sylvan.apple.com/Aerials/resources-15.tar") else { 18 | fatalError("remoteResourceLocation must be able to be constructed") 19 | } 20 | return url 21 | } 22 | 23 | @discardableResult 24 | func downloadResources(completionHandler: @escaping (Result) -> Void) -> URLSessionDataTask { 25 | let task = URLSession.shared.dataTask(with: remoteResourceLocation) { [self] data, _, error in 26 | if let error = error { 27 | completionHandler(.failure(error)) 28 | return 29 | } 30 | guard let data = data else { fatalError("data is required if there is no error") } 31 | do { 32 | let tar = try Tar(data) 33 | try tar.write(to: self.directory) 34 | completionHandler(resourceResult) 35 | } catch { 36 | completionHandler(.failure(error)) 37 | } 38 | } 39 | task.resume() 40 | return task 41 | } 42 | 43 | var resourceResult: Result { 44 | Result { 45 | try Resources(url: directory) 46 | } 47 | } 48 | } 49 | 50 | let directory: URL 51 | 52 | let bundle: Bundle 53 | let entries: Entries 54 | 55 | init(url: URL) throws { 56 | directory = url 57 | 58 | guard let stringsBundle = Bundle(url: url.appendingPathComponent("TVIdleScreenStrings.bundle")) else { 59 | throw CocoaError(.fileReadUnknown) 60 | } 61 | bundle = stringsBundle 62 | 63 | let entryData = try Data(contentsOf: url.appendingPathComponent("entries.json")) 64 | let decoder = JSONDecoder() 65 | entries = try decoder.decode(Entries.self, from: entryData) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Forest/Tar.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Tar.swift 3 | // Forest 4 | // 5 | // Created by Leptos on 5/13/21. 6 | // Copyright © 2021 Leptos. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct Tar { 12 | enum ParsingError: Error { 13 | case stringDecodeFailed 14 | case integerDecodeFailed 15 | case unknown(typeflag: UInt8) 16 | case unexpectedEndOfFile 17 | } 18 | 19 | // https://www.gnu.org/software/tar/manual/html_node/Standard.html 20 | struct Entry { 21 | enum TypeFlag { 22 | case regular 23 | case link(to: String) 24 | case symbolicLink(to: String) 25 | case character(major: UInt, minor: UInt) 26 | case block(major: UInt, minor: UInt) 27 | case directory 28 | case fifo 29 | case contiguous 30 | 31 | var attributeType: FileAttributeType { 32 | switch self { 33 | case .regular: return .typeRegular 34 | case .link: return .typeRegular 35 | case .symbolicLink: return .typeSymbolicLink 36 | case .character: return .typeCharacterSpecial 37 | case .block: return .typeBlockSpecial 38 | case .directory: return .typeDirectory 39 | case .fifo: return .typeUnknown 40 | case .contiguous: return .typeRegular 41 | } 42 | } 43 | } 44 | 45 | let name: String 46 | let mode: mode_t 47 | let uid: uid_t 48 | let gid: gid_t 49 | let modificationDate: Date 50 | let chksum: UInt 51 | let typeFlag: TypeFlag 52 | let magic: String 53 | let version: String 54 | let uname: String 55 | let gname: String 56 | 57 | let data: Data 58 | 59 | 60 | private static func string(from bytes: S) throws -> String where S: Sequence, S.Element == UInt8 { 61 | guard let string = String(bytes: bytes, encoding: .ascii) else { 62 | throw ParsingError.stringDecodeFailed 63 | } 64 | let validPrefix = string.prefix { $0 != .null } 65 | return String(validPrefix) 66 | } 67 | private static func int(from bytes: S) throws -> R where S: Sequence, S.Element == UInt8, R: FixedWidthInteger { 68 | let string = try string(from: bytes) 69 | let numerals = string.trimmingCharacters(in: .whitespaces) 70 | guard let integer = R(numerals, radix: 8) else { 71 | throw ParsingError.integerDecodeFailed 72 | } 73 | return integer 74 | } 75 | 76 | fileprivate init(_ bufferPointer: ByteCollection) throws where ByteCollection: Collection, 77 | ByteCollection.Element == UInt8, 78 | ByteCollection.Index: BinaryInteger { 79 | let fullHeaderSize: ByteCollection.Index = 512 80 | let relativeIndex = bufferPointer.startIndex 81 | 82 | name = try Self.string(from: bufferPointer[relativeIndex+0.. (Int32, Int32) in 172 | let status = mkfifo(fileName, mode) 173 | let globalError = errno // copy errno since it may change before we access it again later 174 | return (status, globalError) 175 | } 176 | guard mkfifoResult == 0 else { 177 | guard let errorCode = POSIXError.Code(rawValue: errnoCopy) else { 178 | fatalError("errno (\(errnoCopy)) could not be translated to POSIXError.Code") 179 | } 180 | throw POSIXError(errorCode) 181 | } 182 | try fileManager.setAttributes(attributes, ofItemAtPath: writeLocation.path) 183 | } 184 | } 185 | } 186 | 187 | 188 | let entries: [Entry] 189 | 190 | init(_ data: Data) throws { 191 | var entries: [Entry] = [] 192 | 193 | try data.withUnsafeBytes { (bufferRawPointer: UnsafeRawBufferPointer) in 194 | let bufferPointer: UnsafeBufferPointer = bufferRawPointer.bindMemory(to: UInt8.self) 195 | let bufferLength: Int = bufferPointer.count 196 | 197 | var readBase: Int = 0 198 | while readBase < bufferLength { 199 | let blockSize = 512 200 | let readBaseBlockMod = readBase % blockSize 201 | if readBaseBlockMod != 0 { 202 | readBase += blockSize - readBaseBlockMod 203 | } 204 | 205 | guard (readBase + blockSize) < bufferLength else { 206 | // make sure reading this block won't go out of bounds 207 | // not an error, this may be the end of the archive 208 | break 209 | } 210 | 211 | let isNullBlock = bufferPointer[readBase.. Void)? 20 | 21 | init(asset: Entries.Asset, pointsOfInterest: [Entries.Asset.LocalizedPointOfInterest], 22 | shouldAutoPlay: Bool = false, 23 | videoVariant: State = State(initialValue: .c_1080_HDR), 24 | playerItemEndCallback: (() -> Void)? = nil) { 25 | self.asset = asset 26 | self.pointsOfInterest = pointsOfInterest 27 | self.shouldAutoPlay = shouldAutoPlay 28 | self._videoVariant = videoVariant 29 | self.playerItemEndCallback = playerItemEndCallback 30 | } 31 | 32 | private var playerMetadata: PlayerView.Metadata { 33 | let subtitle = pointOfInterestIndex.map { pointsOfInterest[$0].value } 34 | return PlayerView.Metadata(title: asset.accessibilityLabel, subtitle: subtitle, description: nil) 35 | } 36 | 37 | var body: some View { 38 | ScrollView { 39 | PlayerView( 40 | url: asset.url(for: videoVariant), 41 | timeStamps: pointsOfInterest.map(\.timeInterval), 42 | timeStampIndex: $pointOfInterestIndex, 43 | metadata: playerMetadata, 44 | shouldAutoPlay: shouldAutoPlay, 45 | playerItemEndCallback: playerItemEndCallback 46 | ) 47 | .aspectRatio(CGSize(width: 16, height: 9), contentMode: .fit) 48 | 49 | HStack(spacing: 16) { 50 | Picker("Video Variant", selection: $videoVariant) { 51 | ForEach(Entries.Asset.VideoVariant.allCases) { variant in 52 | Text(variant.name) 53 | } 54 | } 55 | .labelsHidden() // on macOS, hides the picker title 56 | .pickerStyle(.segmented) 57 | 58 | #if os(macOS) 59 | Link(destination: asset.url(for: videoVariant)) { 60 | Label("Open in Safari", systemImage: "safari") 61 | .labelStyle(.iconOnly) 62 | } 63 | #endif 64 | } 65 | .padding(.horizontal, 16) 66 | 67 | PointsOfInterestTable(pointsOfInterest, activeIndex: pointOfInterestIndex) 68 | .padding(.top, 8) 69 | .animation(.easeInOut(duration: 0.45), value: pointOfInterestIndex) 70 | } 71 | .navigationTitle(asset.accessibilityLabel) 72 | .toolbar { 73 | #if !os(macOS) 74 | ToolbarItem(placement: .navigationBarTrailing) { 75 | if #available(iOS 16.0, macOS 13.0, *) { 76 | ShareLink(item: asset.url(for: videoVariant), subject: Text(asset.accessibilityLabel)) 77 | } else { 78 | Link(destination: asset.url(for: videoVariant)) { 79 | Label("Open in Safari", systemImage: "safari") 80 | } 81 | } 82 | } 83 | #endif 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /Forest/UI/AssociatedAssetsView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AssociatedAssetsView.swift 3 | // Forest 4 | // 5 | // Created by Leptos on 5/15/21. 6 | // Copyright © 2021 Leptos. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct AssociatedAssetsView: View { 12 | let associatedAsset: Entries.AssociatedAssets 13 | let decodeBundle: Bundle 14 | 15 | var body: some View { 16 | Section(header: SidebarSectionHeader(associatedAsset.id)) { 17 | ForEach(associatedAsset.assets) { asset in 18 | AssetNavigationLink(asset: asset, decodeBundle: decodeBundle) 19 | } 20 | } 21 | } 22 | } 23 | 24 | extension Entries { 25 | struct AssociatedAssets: Identifiable { 26 | let id: String 27 | let assets: [Entries.Asset] 28 | } 29 | 30 | var associatedAssets: [AssociatedAssets] { 31 | Dictionary(grouping: assets, by: \.accessibilityLabel) 32 | .map { (key: String, value: [Entries.Asset]) in 33 | AssociatedAssets(id: key, assets: value) 34 | } 35 | .sorted { $0.id < $1.id } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Forest/UI/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // Forest 4 | // 5 | // Created by Leptos on 5/15/21. 6 | // Copyright © 2021 Leptos. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct ContentView: View { 12 | let resourceDescriptor: Resources.Descriptor 13 | 14 | @State private var resourceResult: Result 15 | @State private var downloadDataTask: URLSessionDataTask? 16 | 17 | var resources: Resources? { 18 | switch resourceResult { 19 | case .success(let resources): 20 | return resources 21 | default: 22 | return nil 23 | } 24 | } 25 | var errorString: String? { 26 | switch resourceResult { 27 | case .failure(let error): 28 | return error.localizedDescription 29 | default: 30 | return nil 31 | } 32 | } 33 | 34 | init(resourceDescriptor: Resources.Descriptor) { 35 | self.resourceDescriptor = resourceDescriptor 36 | self._resourceResult = State(initialValue: resourceDescriptor.resourceResult) 37 | } 38 | 39 | var body: some View { 40 | if let resources = resources { 41 | NavigationView { 42 | List { 43 | ShuffleSection(resources: resources) 44 | 45 | ForEach(resources.entries.associatedAssets) { associatedAsset in 46 | AssociatedAssetsView(associatedAsset: associatedAsset, decodeBundle: resources.bundle) 47 | } 48 | } 49 | .navigationTitle("Forest") 50 | .listStyle(.sidebar) 51 | } 52 | } else if let downloadDataTask = downloadDataTask { 53 | ProgressView(downloadDataTask.progress) 54 | .padding(.horizontal, 24) 55 | } else { 56 | VStack(spacing: 0) { 57 | if let errorString = errorString { 58 | Text(errorString) 59 | .font(.callout) 60 | .padding(16) 61 | .background(Color.red) 62 | .cornerRadius(8) 63 | .foregroundColor(.white) 64 | .padding(24) 65 | } 66 | 67 | Button("Download Resources") { 68 | downloadDataTask = resourceDescriptor.downloadResources { result in 69 | DispatchQueue.main.async { 70 | resourceResult = result 71 | downloadDataTask = nil 72 | } 73 | } 74 | } 75 | .font(.headline) 76 | .padding(12) 77 | .contentShape(RoundedRectangle(cornerRadius: 8)) 78 | .padding(12) 79 | } 80 | } 81 | } 82 | } 83 | 84 | struct ContentViewPreviews: PreviewProvider { 85 | static var previews: some View { 86 | ContentView( 87 | resourceDescriptor: Resources.Descriptor(directory: URL(fileURLWithPath: "/dev/null")) 88 | ) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /Forest/UI/PlayerView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PlayerView.swift 3 | // Forest 4 | // 5 | // Created by Leptos on 5/15/21. 6 | // Copyright © 2021 Leptos. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import AVKit 11 | 12 | struct PlayerView { 13 | struct Metadata { 14 | let title: String? 15 | let subtitle: String? 16 | let description: String? 17 | } 18 | 19 | let url: URL 20 | let timeStamps: [TimeInterval] // assume sorted 21 | 22 | @Binding var timeStampIndex: Int? 23 | 24 | let metadata: Metadata 25 | let showsPlaybackControls: Bool 26 | let videoGravity: AVLayerVideoGravity 27 | 28 | let shouldAutoPlay: Bool 29 | 30 | let playerItemEndCallback: (() -> Void)? 31 | 32 | // MARK: - 33 | 34 | init(url: URL, timeStamps: [TimeInterval], timeStampIndex: Binding, 35 | metadata: Metadata = Metadata(title: nil, subtitle: nil, description: nil), 36 | showsPlaybackControls: Bool = true, videoGravity: AVLayerVideoGravity = .resizeAspect, 37 | shouldAutoPlay: Bool = false, playerItemEndCallback: (() -> Void)? = nil) { 38 | self.url = url 39 | self.timeStamps = timeStamps 40 | self._timeStampIndex = timeStampIndex 41 | self.metadata = metadata 42 | self.showsPlaybackControls = showsPlaybackControls 43 | self.videoGravity = videoGravity 44 | self.shouldAutoPlay = shouldAutoPlay 45 | self.playerItemEndCallback = playerItemEndCallback 46 | } 47 | 48 | /// Common routine acting upon player object 49 | /// - Note: Should only be called from Representable `update` functions 50 | private func commonUpdatePlayer(_ player: AVPlayer, coordinator: Coordinator) { 51 | let currentAsset = player.currentItem?.asset as? AVURLAsset 52 | if currentAsset?.url != url { 53 | player.replaceCurrentItem(with: AVPlayerItem(url: url)) 54 | if shouldAutoPlay { 55 | player.play() 56 | } 57 | } 58 | coordinator.updatePlayer(player, timeStamps: timeStamps, metadata: metadata) 59 | } 60 | 61 | func makeCoordinator() -> Coordinator { 62 | Coordinator(timeStampIndex: _timeStampIndex, playerItemEndCallback: playerItemEndCallback) 63 | } 64 | 65 | class Coordinator { 66 | @Binding var timeStampIndex: Int? 67 | let playerItemEndCallback: (() -> Void)? 68 | 69 | init(timeStampIndex: Binding, playerItemEndCallback: (() -> Void)?) { 70 | self._timeStampIndex = timeStampIndex 71 | self.playerItemEndCallback = playerItemEndCallback 72 | } 73 | 74 | func updatePlayer(_ player: AVPlayer, timeStamps: [TimeInterval], metadata: Metadata) { 75 | if playerTimeObserver?.player != player || playerTimeObserver?.timeStamps != timeStamps { 76 | addPeriodicTimeObserver(for: timeStamps, to: player) 77 | } 78 | guard let playerItem = player.currentItem else { 79 | fatalError("player.currentItem must be valid at this point") 80 | } 81 | addEndTimeObserver(to: playerItem) 82 | #if os(iOS) || os(tvOS) || os(visionOS) 83 | // per https://developer.apple.com/wwdc22/10147 84 | var externalMetadata: [AVMetadataItem] = [] 85 | if let title = metadata.title { 86 | let item = AVMutableMetadataItem() 87 | item.identifier = .commonIdentifierTitle 88 | item.value = title as NSString 89 | externalMetadata.append(item) 90 | } 91 | if let subtitle = metadata.subtitle { 92 | let item = AVMutableMetadataItem() 93 | item.identifier = .iTunesMetadataTrackSubTitle 94 | item.value = subtitle as NSString 95 | externalMetadata.append(item) 96 | } 97 | if let description = metadata.description { 98 | let item = AVMutableMetadataItem() 99 | item.identifier = .commonIdentifierDescription 100 | item.value = description as NSString 101 | externalMetadata.append(item) 102 | } 103 | playerItem.externalMetadata = externalMetadata 104 | #endif 105 | } 106 | 107 | // MARK: - Periodic Time Observing 108 | 109 | private var playerTimeObserver: (player: AVPlayer, timeObserver: Any, timeStamps: [TimeInterval])? { 110 | didSet { 111 | guard let previous = oldValue else { return } 112 | previous.player.removeTimeObserver(previous.timeObserver) 113 | } 114 | } 115 | 116 | private func addPeriodicTimeObserver(for timeStamps: [TimeInterval], to player: AVPlayer) { 117 | guard !timeStamps.isEmpty else { return } 118 | 119 | let offsetStamps = timeStamps[1...] + [ .infinity ] 120 | let ranges = zip(timeStamps, offsetStamps).map { ($0)..<($1) } 121 | let observer = player.addPeriodicTimeObserver(forInterval: CMTime(seconds: 1, preferredTimescale: 1000), queue: nil) { [weak self] time in 122 | guard let self = self else { return } 123 | let currentSeconds = time.seconds 124 | self.timeStampIndex = ranges.firstIndex { $0.contains(currentSeconds) } 125 | } 126 | playerTimeObserver = (player, observer, timeStamps) 127 | } 128 | 129 | // MARK: - End Time Observing 130 | 131 | private let notificationCenter: NotificationCenter = .default 132 | private let notificationName: Notification.Name = .AVPlayerItemDidPlayToEndTime 133 | 134 | private var playerItemObserver: NSObjectProtocol? { 135 | didSet { 136 | guard let observer = oldValue else { return } 137 | notificationCenter.removeObserver(observer, name: notificationName, object: nil) 138 | } 139 | } 140 | 141 | private func addEndTimeObserver(to playerItem: AVPlayerItem) { 142 | playerItemObserver = notificationCenter.addObserver(forName: notificationName, object: playerItem, queue: .main) { [weak self] note in 143 | guard let self = self else { return } 144 | self.timeStampIndex = nil 145 | self.playerItemEndCallback?() 146 | } 147 | } 148 | // MARK: - 149 | deinit { 150 | playerTimeObserver = nil 151 | playerItemObserver = nil 152 | } 153 | } 154 | } 155 | 156 | #if canImport(AppKit) 157 | 158 | extension PlayerView: NSViewRepresentable { 159 | func makeNSView(context: Context) -> AVPlayerView { 160 | let playerView = AVPlayerView() 161 | playerView.updatesNowPlayingInfoCenter = false 162 | playerView.allowsPictureInPicturePlayback = true 163 | 164 | playerView.player = AVPlayer() 165 | 166 | return playerView 167 | } 168 | 169 | func updateNSView(_ playerView: AVPlayerView, context: Context) { 170 | guard let player = playerView.player else { 171 | fatalError("playerView.player should always be set in makeNSView") 172 | } 173 | commonUpdatePlayer(player, coordinator: context.coordinator) 174 | 175 | playerView.controlsStyle = showsPlaybackControls ? .inline : .none 176 | playerView.showsFullScreenToggleButton = showsPlaybackControls 177 | playerView.videoGravity = videoGravity 178 | } 179 | } 180 | 181 | #elseif canImport(UIKit) 182 | 183 | extension PlayerView: UIViewControllerRepresentable { 184 | func makeUIViewController(context: Context) -> AVPlayerViewController { 185 | let playerController = AVPlayerViewController() 186 | playerController.updatesNowPlayingInfoCenter = false 187 | playerController.allowsPictureInPicturePlayback = true 188 | 189 | playerController.player = AVPlayer() 190 | 191 | return playerController 192 | } 193 | 194 | func updateUIViewController(_ playerController: AVPlayerViewController, context: Context) { 195 | guard let player = playerController.player else { 196 | fatalError("playerController.player should always be set in makeUIViewController") 197 | } 198 | commonUpdatePlayer(player, coordinator: context.coordinator) 199 | 200 | playerController.showsPlaybackControls = showsPlaybackControls 201 | playerController.videoGravity = videoGravity 202 | } 203 | } 204 | 205 | #else 206 | #error("Unsupported UI framework") 207 | #endif 208 | -------------------------------------------------------------------------------- /Forest/UI/PointsOfInterestTable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PointsOfInterestTable.swift 3 | // Forest 4 | // 5 | // Created by Leptos on 9/9/21. 6 | // Copyright © 2021 Leptos. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct PointsOfInterestTable: View { 12 | let pointsOfInterest: [Entries.Asset.LocalizedPointOfInterest] 13 | let activeIndex: Int? 14 | 15 | private func stringFrom(seconds: TimeInterval) -> String { 16 | let secondsPerMinute: TimeInterval = 60 17 | let divided = seconds/secondsPerMinute 18 | let minutes = divided.rounded(.towardZero) 19 | let remainder = seconds - minutes * secondsPerMinute 20 | return String(format: "%.0f:%02.0f", minutes, remainder) 21 | } 22 | 23 | private func isPointOfInterestActive(_ pointOfInterest: Entries.Asset.LocalizedPointOfInterest) -> Bool { 24 | guard let pointOfInterestIndex = activeIndex else { return false } 25 | return pointOfInterest == pointsOfInterest[pointOfInterestIndex] 26 | } 27 | 28 | init(_ pointsOfInterest: [Entries.Asset.LocalizedPointOfInterest], activeIndex: Int? = nil) { 29 | self.pointsOfInterest = pointsOfInterest 30 | self.activeIndex = activeIndex 31 | } 32 | 33 | var body: some View { 34 | VStack(alignment: .leading, spacing: 0) { 35 | Text("Points of Interest") 36 | .font(.headline) 37 | .padding(.leading, 24) 38 | .padding(.bottom, 8) 39 | 40 | ForEach(pointsOfInterest) { pointOfInterest in 41 | HStack { 42 | Text(pointOfInterest.value) 43 | .fixedSize(horizontal: false, vertical: true) 44 | Spacer(minLength: 4) 45 | Text(stringFrom(seconds: pointOfInterest.timeInterval)) 46 | .font(.footnote) 47 | } 48 | .padding(8) 49 | .padding(.horizontal, 4) 50 | .background( 51 | (isPointOfInterestActive(pointOfInterest) ? Color.accentColor.opacity(0.5) : Color.clear) 52 | .cornerRadius(6) 53 | ) 54 | } 55 | .font(.body) 56 | .padding(.horizontal, 16) 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Forest/UI/ShuffleNavigationLink.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ShuffleNavigationLink.swift 3 | // Forest 4 | // 5 | // Created by Leptos on 9/10/21. 6 | // Copyright © 2021 Leptos. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct ShuffleNavigationLink: View { 12 | let assets: [Entries.Asset] 13 | let decodeBundle: Bundle 14 | 15 | @State private var asset: Entries.Asset? 16 | @State private var shouldAutoPlay: Bool 17 | @State private var videoVariant: Entries.Asset.VideoVariant 18 | 19 | @ViewBuilder var label: () -> Label 20 | 21 | init(assets: [Entries.Asset], decodeBundle: Bundle, 22 | shouldAutoPlay: State = State(initialValue: false), 23 | videoVariant: State = State(initialValue: .c_1080_HDR), 24 | @ViewBuilder label: @escaping () -> Label) { 25 | self.assets = assets 26 | self.decodeBundle = decodeBundle 27 | self._asset = State(initialValue: assets.randomElement()) 28 | self._shouldAutoPlay = shouldAutoPlay 29 | self._videoVariant = videoVariant 30 | self.label = label 31 | } 32 | 33 | private func pointsOfInterest(for asset: Entries.Asset) -> [Entries.Asset.LocalizedPointOfInterest] { 34 | asset.decodePointsOfInterest(from: decodeBundle) 35 | .sorted { $0.timeInterval < $1.timeInterval } 36 | } 37 | 38 | var body: some View { 39 | if let asset = asset { 40 | NavigationLink { 41 | AssetView(asset: asset, pointsOfInterest: pointsOfInterest(for: asset), shouldAutoPlay: shouldAutoPlay, videoVariant: _videoVariant) { 42 | self.asset = assets.randomElement() 43 | shouldAutoPlay = true 44 | } 45 | } label: { 46 | label() 47 | } 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Forest/UI/ShuffleSection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ShuffleSection.swift 3 | // Forest 4 | // 5 | // Created by Leptos on 9/10/21. 6 | // Copyright © 2021 Leptos. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct ShuffleSection: View { 12 | struct Row: View { 13 | let title: T 14 | let subtitle: S 15 | 16 | var body: some View { 17 | VStack(alignment: .leading, spacing: 4) { 18 | Label(title, systemImage: "shuffle") 19 | .font(.body) 20 | Text(subtitle) 21 | .font(.callout) 22 | .padding(.leading, 8) 23 | .lineLimit(nil) 24 | } 25 | } 26 | } 27 | 28 | let resources: Resources 29 | 30 | var body: some View { 31 | Section(header: SidebarSectionHeader("Shuffle")) { 32 | ShuffleNavigationLink(assets: resources.entries.assets, decodeBundle: resources.bundle) { 33 | Self.Row(title: "All", subtitle: "Shuffle all videos.") 34 | } 35 | 36 | if let categories = resources.entries.categories { 37 | ForEach(categories) { category in 38 | ShuffleNavigationLink(assets: resources.entries.assets.filter { asset in 39 | guard let assetCategories = asset.categories else { return false } 40 | return assetCategories.contains(category.id) 41 | }, decodeBundle: resources.bundle) { 42 | Self.Row( 43 | title: category.localizedName(from: resources.bundle), 44 | subtitle: category.localizedDescription(from: resources.bundle) 45 | ) 46 | } 47 | } 48 | } 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Forest/UI/SidebarSectionHeader.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SidebarSectionHeader.swift 3 | // Forest 4 | // 5 | // Created by Leptos on 9/11/21. 6 | // Copyright © 2021 Leptos. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct SidebarSectionHeader: View { 12 | let title: T 13 | 14 | init(_ title: T) { 15 | self.title = title 16 | } 17 | 18 | var body: some View { 19 | Text(title) 20 | .foregroundColor(.secondary) 21 | .font(.headline) 22 | } 23 | } 24 | 25 | struct SidebarSectionHeaderPreviews: PreviewProvider { 26 | static var previews: some View { 27 | Group { 28 | ForEach(ColorScheme.allCases, id: \.hashValue) { colorScheme in 29 | List { 30 | Section(header: SidebarSectionHeader("Title")) { 31 | Text("Content") 32 | } 33 | } 34 | .listStyle(.sidebar) 35 | .preferredColorScheme(colorScheme) 36 | .frame(width: 200, height: 80) 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Attribution-NonCommercial 4.0 International 2 | 3 | ======================================================================= 4 | 5 | Creative Commons Corporation ("Creative Commons") is not a law firm and 6 | does not provide legal services or legal advice. Distribution of 7 | Creative Commons public licenses does not create a lawyer-client or 8 | other relationship. Creative Commons makes its licenses and related 9 | information available on an "as-is" basis. Creative Commons gives no 10 | warranties regarding its licenses, any material licensed under their 11 | terms and conditions, or any related information. Creative Commons 12 | disclaims all liability for damages resulting from their use to the 13 | fullest extent possible. 14 | 15 | Using Creative Commons Public Licenses 16 | 17 | Creative Commons public licenses provide a standard set of terms and 18 | conditions that creators and other rights holders may use to share 19 | original works of authorship and other material subject to copyright 20 | and certain other rights specified in the public license below. The 21 | following considerations are for informational purposes only, are not 22 | exhaustive, and do not form part of our licenses. 23 | 24 | Considerations for licensors: Our public licenses are 25 | intended for use by those authorized to give the public 26 | permission to use material in ways otherwise restricted by 27 | copyright and certain other rights. Our licenses are 28 | irrevocable. Licensors should read and understand the terms 29 | and conditions of the license they choose before applying it. 30 | Licensors should also secure all rights necessary before 31 | applying our licenses so that the public can reuse the 32 | material as expected. Licensors should clearly mark any 33 | material not subject to the license. This includes other CC- 34 | licensed material, or material used under an exception or 35 | limitation to copyright. More considerations for licensors: 36 | wiki.creativecommons.org/Considerations_for_licensors 37 | 38 | Considerations for the public: By using one of our public 39 | licenses, a licensor grants the public permission to use the 40 | licensed material under specified terms and conditions. If 41 | the licensor's permission is not necessary for any reason--for 42 | example, because of any applicable exception or limitation to 43 | copyright--then that use is not regulated by the license. Our 44 | licenses grant only permissions under copyright and certain 45 | other rights that a licensor has authority to grant. Use of 46 | the licensed material may still be restricted for other 47 | reasons, including because others have copyright or other 48 | rights in the material. A licensor may make special requests, 49 | such as asking that all changes be marked or described. 50 | Although not required by our licenses, you are encouraged to 51 | respect those requests where reasonable. More considerations 52 | for the public: 53 | wiki.creativecommons.org/Considerations_for_licensees 54 | 55 | ======================================================================= 56 | 57 | Creative Commons Attribution-NonCommercial 4.0 International Public 58 | License 59 | 60 | By exercising the Licensed Rights (defined below), You accept and agree 61 | to be bound by the terms and conditions of this Creative Commons 62 | Attribution-NonCommercial 4.0 International Public License ("Public 63 | License"). To the extent this Public License may be interpreted as a 64 | contract, You are granted the Licensed Rights in consideration of Your 65 | acceptance of these terms and conditions, and the Licensor grants You 66 | such rights in consideration of benefits the Licensor receives from 67 | making the Licensed Material available under these terms and 68 | conditions. 69 | 70 | 71 | Section 1 -- Definitions. 72 | 73 | a. Adapted Material means material subject to Copyright and Similar 74 | Rights that is derived from or based upon the Licensed Material 75 | and in which the Licensed Material is translated, altered, 76 | arranged, transformed, or otherwise modified in a manner requiring 77 | permission under the Copyright and Similar Rights held by the 78 | Licensor. For purposes of this Public License, where the Licensed 79 | Material is a musical work, performance, or sound recording, 80 | Adapted Material is always produced where the Licensed Material is 81 | synched in timed relation with a moving image. 82 | 83 | b. Adapter's License means the license You apply to Your Copyright 84 | and Similar Rights in Your contributions to Adapted Material in 85 | accordance with the terms and conditions of this Public License. 86 | 87 | c. Copyright and Similar Rights means copyright and/or similar rights 88 | closely related to copyright including, without limitation, 89 | performance, broadcast, sound recording, and Sui Generis Database 90 | Rights, without regard to how the rights are labeled or 91 | categorized. For purposes of this Public License, the rights 92 | specified in Section 2(b)(1)-(2) are not Copyright and Similar 93 | Rights. 94 | d. Effective Technological Measures means those measures that, in the 95 | absence of proper authority, may not be circumvented under laws 96 | fulfilling obligations under Article 11 of the WIPO Copyright 97 | Treaty adopted on December 20, 1996, and/or similar international 98 | agreements. 99 | 100 | e. Exceptions and Limitations means fair use, fair dealing, and/or 101 | any other exception or limitation to Copyright and Similar Rights 102 | that applies to Your use of the Licensed Material. 103 | 104 | f. Licensed Material means the artistic or literary work, database, 105 | or other material to which the Licensor applied this Public 106 | License. 107 | 108 | g. Licensed Rights means the rights granted to You subject to the 109 | terms and conditions of this Public License, which are limited to 110 | all Copyright and Similar Rights that apply to Your use of the 111 | Licensed Material and that the Licensor has authority to license. 112 | 113 | h. Licensor means the individual(s) or entity(ies) granting rights 114 | under this Public License. 115 | 116 | i. NonCommercial means not primarily intended for or directed towards 117 | commercial advantage or monetary compensation. For purposes of 118 | this Public License, the exchange of the Licensed Material for 119 | other material subject to Copyright and Similar Rights by digital 120 | file-sharing or similar means is NonCommercial provided there is 121 | no payment of monetary compensation in connection with the 122 | exchange. 123 | 124 | j. Share means to provide material to the public by any means or 125 | process that requires permission under the Licensed Rights, such 126 | as reproduction, public display, public performance, distribution, 127 | dissemination, communication, or importation, and to make material 128 | available to the public including in ways that members of the 129 | public may access the material from a place and at a time 130 | individually chosen by them. 131 | 132 | k. Sui Generis Database Rights means rights other than copyright 133 | resulting from Directive 96/9/EC of the European Parliament and of 134 | the Council of 11 March 1996 on the legal protection of databases, 135 | as amended and/or succeeded, as well as other essentially 136 | equivalent rights anywhere in the world. 137 | 138 | l. You means the individual or entity exercising the Licensed Rights 139 | under this Public License. Your has a corresponding meaning. 140 | 141 | 142 | Section 2 -- Scope. 143 | 144 | a. License grant. 145 | 146 | 1. Subject to the terms and conditions of this Public License, 147 | the Licensor hereby grants You a worldwide, royalty-free, 148 | non-sublicensable, non-exclusive, irrevocable license to 149 | exercise the Licensed Rights in the Licensed Material to: 150 | 151 | a. reproduce and Share the Licensed Material, in whole or 152 | in part, for NonCommercial purposes only; and 153 | 154 | b. produce, reproduce, and Share Adapted Material for 155 | NonCommercial purposes only. 156 | 157 | 2. Exceptions and Limitations. For the avoidance of doubt, where 158 | Exceptions and Limitations apply to Your use, this Public 159 | License does not apply, and You do not need to comply with 160 | its terms and conditions. 161 | 162 | 3. Term. The term of this Public License is specified in Section 163 | 6(a). 164 | 165 | 4. Media and formats; technical modifications allowed. The 166 | Licensor authorizes You to exercise the Licensed Rights in 167 | all media and formats whether now known or hereafter created, 168 | and to make technical modifications necessary to do so. The 169 | Licensor waives and/or agrees not to assert any right or 170 | authority to forbid You from making technical modifications 171 | necessary to exercise the Licensed Rights, including 172 | technical modifications necessary to circumvent Effective 173 | Technological Measures. For purposes of this Public License, 174 | simply making modifications authorized by this Section 2(a) 175 | (4) never produces Adapted Material. 176 | 177 | 5. Downstream recipients. 178 | 179 | a. Offer from the Licensor -- Licensed Material. Every 180 | recipient of the Licensed Material automatically 181 | receives an offer from the Licensor to exercise the 182 | Licensed Rights under the terms and conditions of this 183 | Public License. 184 | 185 | b. No downstream restrictions. You may not offer or impose 186 | any additional or different terms or conditions on, or 187 | apply any Effective Technological Measures to, the 188 | Licensed Material if doing so restricts exercise of the 189 | Licensed Rights by any recipient of the Licensed 190 | Material. 191 | 192 | 6. No endorsement. Nothing in this Public License constitutes or 193 | may be construed as permission to assert or imply that You 194 | are, or that Your use of the Licensed Material is, connected 195 | with, or sponsored, endorsed, or granted official status by, 196 | the Licensor or others designated to receive attribution as 197 | provided in Section 3(a)(1)(A)(i). 198 | 199 | b. Other rights. 200 | 201 | 1. Moral rights, such as the right of integrity, are not 202 | licensed under this Public License, nor are publicity, 203 | privacy, and/or other similar personality rights; however, to 204 | the extent possible, the Licensor waives and/or agrees not to 205 | assert any such rights held by the Licensor to the limited 206 | extent necessary to allow You to exercise the Licensed 207 | Rights, but not otherwise. 208 | 209 | 2. Patent and trademark rights are not licensed under this 210 | Public License. 211 | 212 | 3. To the extent possible, the Licensor waives any right to 213 | collect royalties from You for the exercise of the Licensed 214 | Rights, whether directly or through a collecting society 215 | under any voluntary or waivable statutory or compulsory 216 | licensing scheme. In all other cases the Licensor expressly 217 | reserves any right to collect such royalties, including when 218 | the Licensed Material is used other than for NonCommercial 219 | purposes. 220 | 221 | 222 | Section 3 -- License Conditions. 223 | 224 | Your exercise of the Licensed Rights is expressly made subject to the 225 | following conditions. 226 | 227 | a. Attribution. 228 | 229 | 1. If You Share the Licensed Material (including in modified 230 | form), You must: 231 | 232 | a. retain the following if it is supplied by the Licensor 233 | with the Licensed Material: 234 | 235 | i. identification of the creator(s) of the Licensed 236 | Material and any others designated to receive 237 | attribution, in any reasonable manner requested by 238 | the Licensor (including by pseudonym if 239 | designated); 240 | 241 | ii. a copyright notice; 242 | 243 | iii. a notice that refers to this Public License; 244 | 245 | iv. a notice that refers to the disclaimer of 246 | warranties; 247 | 248 | v. a URI or hyperlink to the Licensed Material to the 249 | extent reasonably practicable; 250 | 251 | b. indicate if You modified the Licensed Material and 252 | retain an indication of any previous modifications; and 253 | 254 | c. indicate the Licensed Material is licensed under this 255 | Public License, and include the text of, or the URI or 256 | hyperlink to, this Public License. 257 | 258 | 2. You may satisfy the conditions in Section 3(a)(1) in any 259 | reasonable manner based on the medium, means, and context in 260 | which You Share the Licensed Material. For example, it may be 261 | reasonable to satisfy the conditions by providing a URI or 262 | hyperlink to a resource that includes the required 263 | information. 264 | 265 | 3. If requested by the Licensor, You must remove any of the 266 | information required by Section 3(a)(1)(A) to the extent 267 | reasonably practicable. 268 | 269 | 4. If You Share Adapted Material You produce, the Adapter's 270 | License You apply must not prevent recipients of the Adapted 271 | Material from complying with this Public License. 272 | 273 | 274 | Section 4 -- Sui Generis Database Rights. 275 | 276 | Where the Licensed Rights include Sui Generis Database Rights that 277 | apply to Your use of the Licensed Material: 278 | 279 | a. for the avoidance of doubt, Section 2(a)(1) grants You the right 280 | to extract, reuse, reproduce, and Share all or a substantial 281 | portion of the contents of the database for NonCommercial purposes 282 | only; 283 | 284 | b. if You include all or a substantial portion of the database 285 | contents in a database in which You have Sui Generis Database 286 | Rights, then the database in which You have Sui Generis Database 287 | Rights (but not its individual contents) is Adapted Material; and 288 | 289 | c. You must comply with the conditions in Section 3(a) if You Share 290 | all or a substantial portion of the contents of the database. 291 | 292 | For the avoidance of doubt, this Section 4 supplements and does not 293 | replace Your obligations under this Public License where the Licensed 294 | Rights include other Copyright and Similar Rights. 295 | 296 | 297 | Section 5 -- Disclaimer of Warranties and Limitation of Liability. 298 | 299 | a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE 300 | EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS 301 | AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF 302 | ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS, 303 | IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION, 304 | WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR 305 | PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS, 306 | ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT 307 | KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT 308 | ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU. 309 | 310 | b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE 311 | TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION, 312 | NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT, 313 | INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES, 314 | COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR 315 | USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN 316 | ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR 317 | DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR 318 | IN PART, THIS LIMITATION MAY NOT APPLY TO YOU. 319 | 320 | c. The disclaimer of warranties and limitation of liability provided 321 | above shall be interpreted in a manner that, to the extent 322 | possible, most closely approximates an absolute disclaimer and 323 | waiver of all liability. 324 | 325 | 326 | Section 6 -- Term and Termination. 327 | 328 | a. This Public License applies for the term of the Copyright and 329 | Similar Rights licensed here. However, if You fail to comply with 330 | this Public License, then Your rights under this Public License 331 | terminate automatically. 332 | 333 | b. Where Your right to use the Licensed Material has terminated under 334 | Section 6(a), it reinstates: 335 | 336 | 1. automatically as of the date the violation is cured, provided 337 | it is cured within 30 days of Your discovery of the 338 | violation; or 339 | 340 | 2. upon express reinstatement by the Licensor. 341 | 342 | For the avoidance of doubt, this Section 6(b) does not affect any 343 | right the Licensor may have to seek remedies for Your violations 344 | of this Public License. 345 | 346 | c. For the avoidance of doubt, the Licensor may also offer the 347 | Licensed Material under separate terms or conditions or stop 348 | distributing the Licensed Material at any time; however, doing so 349 | will not terminate this Public License. 350 | 351 | d. Sections 1, 5, 6, 7, and 8 survive termination of this Public 352 | License. 353 | 354 | 355 | Section 7 -- Other Terms and Conditions. 356 | 357 | a. The Licensor shall not be bound by any additional or different 358 | terms or conditions communicated by You unless expressly agreed. 359 | 360 | b. Any arrangements, understandings, or agreements regarding the 361 | Licensed Material not stated herein are separate from and 362 | independent of the terms and conditions of this Public License. 363 | 364 | 365 | Section 8 -- Interpretation. 366 | 367 | a. For the avoidance of doubt, this Public License does not, and 368 | shall not be interpreted to, reduce, limit, restrict, or impose 369 | conditions on any use of the Licensed Material that could lawfully 370 | be made without permission under this Public License. 371 | 372 | b. To the extent possible, if any provision of this Public License is 373 | deemed unenforceable, it shall be automatically reformed to the 374 | minimum extent necessary to make it enforceable. If the provision 375 | cannot be reformed, it shall be severed from this Public License 376 | without affecting the enforceability of the remaining terms and 377 | conditions. 378 | 379 | c. No term or condition of this Public License will be waived and no 380 | failure to comply consented to unless expressly agreed to by the 381 | Licensor. 382 | 383 | d. Nothing in this Public License constitutes or may be interpreted 384 | as a limitation upon, or waiver of, any privileges and immunities 385 | that apply to the Licensor or You, including from the legal 386 | processes of any jurisdiction or authority. 387 | 388 | ======================================================================= 389 | 390 | Creative Commons is not a party to its public 391 | licenses. Notwithstanding, Creative Commons may elect to apply one of 392 | its public licenses to material it publishes and in those instances 393 | will be considered the “Licensor.” The text of the Creative Commons 394 | public licenses is dedicated to the public domain under the CC0 Public 395 | Domain Dedication. Except for the limited purpose of indicating that 396 | material is shared under a Creative Commons public license or as 397 | otherwise permitted by the Creative Commons policies published at 398 | creativecommons.org/policies, Creative Commons does not authorize the 399 | use of the trademark "Creative Commons" or any other trademark or logo 400 | of Creative Commons without its prior written consent including, 401 | without limitation, in connection with any unauthorized modifications 402 | to any of its public licenses or any other arrangements, 403 | understandings, or agreements concerning use of licensed material. For 404 | the avoidance of doubt, this paragraph does not form part of the 405 | public licenses. 406 | 407 | Creative Commons may be contacted at creativecommons.org. 408 | 409 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### Forest 2 | 3 | Forest is an iOS, macOS, and visionOS SwiftUI app to view the videos used as screensavers on tvOS 15 4 | 5 | ### Screenshots 6 | 7 | ![macOS app using tabs](Screenshots/macOS_tabs-dark.png) 8 | 9 | iOS app main view iOS app detail view 10 | -------------------------------------------------------------------------------- /Screenshots/iOS_detail-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leptos-null/Forest/164df70b8b3fcda5a023375320ffa2059de73f4b/Screenshots/iOS_detail-dark.png -------------------------------------------------------------------------------- /Screenshots/iOS_detail-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leptos-null/Forest/164df70b8b3fcda5a023375320ffa2059de73f4b/Screenshots/iOS_detail-light.png -------------------------------------------------------------------------------- /Screenshots/iOS_home-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leptos-null/Forest/164df70b8b3fcda5a023375320ffa2059de73f4b/Screenshots/iOS_home-dark.png -------------------------------------------------------------------------------- /Screenshots/iOS_home-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leptos-null/Forest/164df70b8b3fcda5a023375320ffa2059de73f4b/Screenshots/iOS_home-light.png -------------------------------------------------------------------------------- /Screenshots/macOS_single-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leptos-null/Forest/164df70b8b3fcda5a023375320ffa2059de73f4b/Screenshots/macOS_single-dark.png -------------------------------------------------------------------------------- /Screenshots/macOS_single-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leptos-null/Forest/164df70b8b3fcda5a023375320ffa2059de73f4b/Screenshots/macOS_single-light.png -------------------------------------------------------------------------------- /Screenshots/macOS_tabs-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leptos-null/Forest/164df70b8b3fcda5a023375320ffa2059de73f4b/Screenshots/macOS_tabs-dark.png -------------------------------------------------------------------------------- /Screenshots/macOS_tabs-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leptos-null/Forest/164df70b8b3fcda5a023375320ffa2059de73f4b/Screenshots/macOS_tabs-light.png --------------------------------------------------------------------------------