From 3945ac883ca967bb8138f7190d3d52b61d3ea4ac Mon Sep 17 00:00:00 2001 From: Matt Mascarenhas Date: Mon, 25 Jan 2021 18:09:30 +0000 Subject: [PATCH] cinera v0.8.0: Mobile-friendly layout Major features: Search page: Subdivision grid layout Player page: Device orientation-specific layout Fixes: Strip slashes of the URLs: base_url, etc. Fix SnipeChecksumAndCloseFile() to not null-terminate the checksum string Fix hover background colouration of medium icons of current timestamp Fix DeriveLineageWithoutOriginOfProject() to call InitBookOfPointers() Fix SortAndAbbreviateSpeakers() to use the person's ID if Name is blank More directly display the "unit", if set, in the search results Fix VideoIsPrivate() to find the apparently relocated privacyStatus Make VideoIsPrivate() default to TRUE for non-youtube Output as keywords all topics that do not match "nullTopic" Deleted asset: cinera_search.js New assets: cinera_search_pre.js cinera_search_post.js --- cinera/cinera.c | 595 ++++-- cinera/cinera.css | 647 +++--- cinera/cinera_config.c | 4 +- cinera/cinera_player_post.js | 940 +++------ cinera/cinera_player_pre.js | 578 ++++-- cinera/cinera_pre.js | 251 +++ cinera/cinera_search.js | 642 ------ cinera/cinera_search_post.js | 62 + cinera/cinera_search_pre.js | 3654 ++++++++++++++++++++++++++++++++++ 9 files changed, 5493 insertions(+), 1880 deletions(-) delete mode 100644 cinera/cinera_search.js create mode 100644 cinera/cinera_search_post.js create mode 100644 cinera/cinera_search_pre.js diff --git a/cinera/cinera.c b/cinera/cinera.c index 7271a88..1ea305d 100644 --- a/cinera/cinera.c +++ b/cinera/cinera.c @@ -22,8 +22,8 @@ typedef struct version CINERA_APP_VERSION = { .Major = 0, - .Minor = 7, - .Patch = 18 + .Minor = 8, + .Patch = 0 }; #include // NOTE(matt): varargs @@ -119,6 +119,9 @@ clock_t TIMING_START; #define MIN(A, B) A < B ? A : B +typedef int32_t hash32; +typedef hash32 asset_hash; + void Clear(void *V, uint64_t Size) { @@ -2442,7 +2445,7 @@ typedef struct char BaseURL[MAX_BASE_URL_LENGTH]; char SearchLocation[MAX_RELATIVE_PAGE_LOCATION_LENGTH]; char PlayerLocation[MAX_RELATIVE_PAGE_LOCATION_LENGTH]; - char PlayerURLPrefix[MAX_PLAYER_URL_PREFIX_LENGTH]; // TODO(matt): Replace this with the OutputPath, when we add that + char PlayerURLPrefix[MAX_PLAYER_URL_PREFIX_LENGTH]; } db_header_entries4; typedef db_entry3 db_entry4; @@ -2553,7 +2556,7 @@ typedef struct { char Filename[MAX_ASSET_FILENAME_LENGTH]; enum8(asset_type) Type; - int32_t Hash; + hash32 Hash; uint32_t LandmarkCount; uint64_t Associated:1; uint64_t Variants:63; @@ -2639,7 +2642,7 @@ typedef struct asset { asset_type Type; char Filename[MAX_ASSET_FILENAME_LENGTH]; - int32_t Hash; + asset_hash Hash; vec2 Dimensions; sprite Sprite; uint64_t Variants:63; @@ -2659,7 +2662,8 @@ asset BuiltinAssets[] = { ASSET_IMG, "cinera_icon_filter.png" }, { ASSET_JS, "cinera_pre.js" }, { ASSET_JS, "cinera_post.js" }, - { ASSET_JS, "cinera_search.js" }, + { ASSET_JS, "cinera_search_pre.js" }, + { ASSET_JS, "cinera_search_post.js" }, { ASSET_JS, "cinera_player_pre.js" }, { ASSET_JS, "cinera_player_post.js" }, }; @@ -2671,7 +2675,8 @@ typedef enum ASSET_IMG_FILTER, ASSET_JS_CINERA_PRE, ASSET_JS_CINERA_POST, - ASSET_JS_SEARCH, + ASSET_JS_SEARCH_PRE, + ASSET_JS_SEARCH_POST, ASSET_JS_PLAYER_PRE, ASSET_JS_PLAYER_POST, BUILTIN_ASSETS_COUNT, @@ -3457,6 +3462,22 @@ CopyStringToBufferNoFormat_(int LineNumber, buffer *Dest, string String) *Dest->Ptr = '\0'; } +#define CopyStringToBufferNoTerminate(Dest, Src) CopyStringToBufferNoTerminate_(__LINE__, Dest, Src) +void +CopyStringToBufferNoTerminate_(int LineNumber, buffer *Dest, string Src) +{ + if((Dest->Ptr - Dest->Location + Src.Length) > Dest->Size) + { + fprintf(stderr, "CopyStringToBufferNoTerminate(%s) call on line %d cannot accommodate %ld-character string:\n" + "%.*s\n", BufferIDStrings[Dest->ID], LineNumber, Src.Length, (int)Src.Length, Src.Base); + __asm__("int3"); + } + for(int i = 0; i < Src.Length; ++i) + { + *Dest->Ptr++ = Src.Base[i]; + } +} + #define CopyStringToBufferNoFormatL(Dest, Length, String) CopyStringToBufferNoFormatL_(__LINE__, Dest, Length, String) void CopyStringToBufferNoFormatL_(int LineNumber, buffer *Dest, int Length, char *String) @@ -4569,7 +4590,7 @@ PrintAssetAndLandmarks(db_asset *A, uint16_t *Index) // TODO(matt): Almost definitely redo this using Locate*() functions... void -SnipeChecksumAndCloseFile(file *HTMLFile, db_asset *Asset, int LandmarksInFile, buffer *Checksum, int64_t *RunningLandmarkIndex) +SnipeChecksumAndCloseFile(file *HTMLFile, db_asset *Asset, int LandmarksInFile, asset_hash Checksum, int64_t *RunningLandmarkIndex) { db_landmark *FirstLandmark = LocateFirstLandmark(Asset); for(int j = 0; j < LandmarksInFile; ++j, ++*RunningLandmarkIndex) @@ -4577,7 +4598,9 @@ SnipeChecksumAndCloseFile(file *HTMLFile, db_asset *Asset, int LandmarksInFile, db_landmark *Landmark = FirstLandmark + *RunningLandmarkIndex; HTMLFile->Buffer.Ptr = HTMLFile->Buffer.Location + Landmark->Position; - CopyBufferSized(&HTMLFile->Buffer, Checksum, Checksum->Ptr - Checksum->Location); + char ChecksumString[16]; + ClearCopyString(ChecksumString, sizeof(ChecksumString), "%08x", Checksum); + CopyStringToBufferNoTerminate(&HTMLFile->Buffer, Wrap0(ChecksumString)); } HTMLFile->Handle = fopen(HTMLFile->Path, "w"); @@ -4996,7 +5019,7 @@ ReadPlayerPageIntoBuffer(file *File, string BaseDir, string PlayerLocation, stri } rc -SnipeChecksumIntoHTML(db_asset *Asset, buffer *Checksum) +SnipeChecksumIntoHTML(db_asset *Asset, asset_hash Checksum) { db_landmark *FirstLandmark = LocateFirstLandmark(Asset); @@ -5564,32 +5587,38 @@ UpdateAssetInDB(asset *Asset) if(StoredAsset->Hash != Asset->Hash) { + char OldChecksum[16]; + ClearCopyString(OldChecksum, sizeof(OldChecksum), "%08x", StoredAsset->Hash); + + char NewChecksum[16]; + ClearCopyString(NewChecksum, sizeof(NewChecksum), "%08x", Asset->Hash); + StoredAsset->Hash = Asset->Hash; char *Ptr = (char *)Asset; Ptr += sizeof(*Asset); - buffer Checksum = {}; - ClaimBuffer(&Checksum, BID_CHECKSUM, 16); - CopyStringToBuffer(&Checksum, "%08x", StoredAsset->Hash); - file AssetFile = {}; AssetFile.Path = ConstructAssetPath(&AssetFile, Wrap0i(StoredAsset->Filename, sizeof(StoredAsset->Filename)), StoredAsset->Type); ResolvePath(&AssetFile.Path); - string ChecksumL = Wrap0i(Checksum.Location, Checksum.Ptr - Checksum.Location); - string Message = MakeString("sssslsss", - ColourStrings[CS_ONGOING], "Updating", ColourStrings[CS_END], " checksum ", &ChecksumL, " of ", AssetFile.Path, " in HTML files"); - fprintf(stderr, "%.*s", (int)Message.Length, Message.Base); - uint64_t MessageLength = Message.Length; - FreeString(&Message); + string MessageEditType = MakeString("sss", ColourStrings[CS_ONGOING], "Updating", ColourStrings[CS_END]); + string Message = MakeString("sssssssssss", + " checksum ", + ColourStrings[CS_BLACK_BOLD], OldChecksum, ColourStrings[CS_END], + " → ", + ColourStrings[CS_BLUE_BOLD], NewChecksum, ColourStrings[CS_END], + " of ", AssetFile.Path, " in HTML files"); + fprintf(stderr, "%.*s%.*s", (int)MessageEditType.Length, MessageEditType.Base, (int)Message.Length, Message.Base); + uint64_t MessageLength = MessageEditType.Length + Message.Length; - if(SnipeChecksumIntoHTML(StoredAsset, &Checksum) == RC_SUCCESS) + if(SnipeChecksumIntoHTML(StoredAsset, Asset->Hash) == RC_SUCCESS) { ClearTerminalRow(MessageLength); - fprintf(stderr, "%sUpdated%s checksum %.*s of %s\n", ColourStrings[CS_REINSERTION], ColourStrings[CS_END], (int)ChecksumL.Length, ChecksumL.Base, AssetFile.Path); + PrintC(CS_REINSERTION, "Updated"); + fprintf(stderr, "%.*s\n", (int)Message.Length, Message.Base); } - DeclaimBuffer(&Checksum); + FreeString(&Message); DB.Metadata.File.Handle = fopen(DB.Metadata.File.Path, "w"); fwrite(DB.Metadata.File.Buffer.Location, DB.Metadata.File.Buffer.Size, 1, DB.Metadata.File.Handle); @@ -5625,7 +5654,7 @@ UpdateAssetInDB(asset *Asset) fwrite(&StoredAsset, sizeof(StoredAsset), 1, DB.Metadata.File.Handle); AccumulateFileEditSize(&DB.Metadata, sizeof(StoredAsset)); - printf("%sAppended%s %s asset: %s [%08x]\n", ColourStrings[CS_ADDITION], ColourStrings[CS_END], AssetTypeNames[StoredAsset.Type], StoredAsset.Filename, StoredAsset.Hash); + fprintf(stderr, "%sAppended%s %s asset: %s [%08x]\n", ColourStrings[CS_ADDITION], ColourStrings[CS_END], AssetTypeNames[StoredAsset.Type], StoredAsset.Filename, StoredAsset.Hash); fwrite(DB.Metadata.File.Buffer.Location + BytesIntoFile, DB.Metadata.File.Buffer.Size - BytesIntoFile, 1, DB.Metadata.File.Handle); @@ -5985,8 +6014,6 @@ AbbreviationsClash(memory_book *Speakers) void SortAndAbbreviateSpeakers(speakers *Speakers) { - // TODO(matt): Handle Abbreviation in its new form as a char *, rather than a fixed-sized char[], so probably doing - // MakeString0() or ExpandString0() or something for(int i = 0; i < Speakers->Speakers.ItemCount; ++i) { speaker *A = GetPlaceInBook(&Speakers->Speakers, i); @@ -6006,8 +6033,9 @@ SortAndAbbreviateSpeakers(speakers *Speakers) for(int i = 0; i < Speakers->Speakers.ItemCount; ++i) { speaker *This = GetPlaceInBook(&Speakers->Speakers, i); + string Name = This->Person->Name.Length > 0 ? This->Person->Name : This->Person->ID; StringToColourHash(&This->Colour, This->Person->ID); - This->Abbreviation = InitialString(&Speakers->Abbreviations, This->Person->Name); + This->Abbreviation = InitialString(&Speakers->Abbreviations, Name); } int Attempt = 0; @@ -6016,11 +6044,12 @@ SortAndAbbreviateSpeakers(speakers *Speakers) for(int i = 0; i < Speakers->Speakers.ItemCount; ++i) { speaker *This = GetPlaceInBook(&Speakers->Speakers, i); + string Name = This->Person->Name.Length > 0 ? This->Person->Name : This->Person->ID; switch(Attempt) { - case 0: This->Abbreviation = GetFirstSubstring(This->Person->Name); break; - case 1: This->Abbreviation = InitialAndGetFinalString(&Speakers->Abbreviations, This->Person->Name); break; - case 2: This->Abbreviation = This->Person->Name; break; + case 0: This->Abbreviation = GetFirstSubstring(Name); break; + case 1: This->Abbreviation = InitialAndGetFinalString(&Speakers->Abbreviations, Name); break; + case 2: This->Abbreviation = Name; break; } } ++Attempt; @@ -6405,6 +6434,8 @@ PushCredentials(buffer *CreditsMenu, memory_book *Speakers, person *Actor, role CopyStringToBuffer(CreditsMenu, " \n"); + + string Name = Actor->Name.Length > 0 ? Actor->Name : Actor->ID; if(Actor->Homepage.Length) { CopyStringToBuffer(CreditsMenu, @@ -6414,7 +6445,7 @@ PushCredentials(buffer *CreditsMenu, memory_book *Speakers, person *Actor, role " \n", (int)Actor->Homepage.Length, Actor->Homepage.Base, RoleStrings[Role], - (int)Actor->Name.Length, Actor->Name.Base); + (int)Name.Length, Name.Base); } else { @@ -6424,7 +6455,7 @@ PushCredentials(buffer *CreditsMenu, memory_book *Speakers, person *Actor, role "
%.*s
\n" " \n", RoleStrings[Role], - (int)Actor->Name.Length, Actor->Name.Base); + (int)Name.Length, Name.Base); } // TODO(matt): Handle multiple support platforms! @@ -6462,9 +6493,9 @@ ErrorCredentials(string HMMLFilepath, string Actor, role Role) } int -BuildCredits(string HMMLFilepath, buffer *CreditsMenu, HMML_VideoMetaData *Metadata, speakers *Speakers, bool *RequiresCineraJS) +BuildCredits(string HMMLFilepath, buffer *CreditsMenu, HMML_VideoMetaData *Metadata, person *Host, speakers *Speakers, bool *RequiresCineraJS) { - person *Host = GetPersonFromConfig(Wrap0(Metadata->member)); + //person *Host = GetPersonFromConfig(Wrap0(Metadata->member)); if(Host) { PushCredentials(CreditsMenu, &Speakers->Speakers, Host, R_HOST, RequiresCineraJS); @@ -8709,61 +8740,73 @@ ExamineDB(void) DeclaimMenuBuffers(&MenuBuffers) bool -VideoIsPrivate(char *VideoID) +VideoIsPrivate(string VODPlatform, char *VideoID) { + // TODO(matt): Redo this to only return once, at the end + // NOTE(matt): Currently only supports YouTube // NOTE(matt): Stack-string - char Message[128]; - CopyString(Message, sizeof(Message), "%sChecking%s privacy status of: https://youtube.com/watch?v=%s", ColourStrings[CS_ONGOING], ColourStrings[CS_END], VideoID); - fprintf(stderr, "%s", Message); - int MessageLength = StringLength(Message); - buffer VideoAPIResponse; - ClaimBuffer(&VideoAPIResponse, BID_VIDEO_API_RESPONSE, Kilobytes(1)); + if(StringsMatch(Wrap0("youtube"), VODPlatform)) + { + char Message[128]; + CopyString(Message, sizeof(Message), "%sChecking%s privacy status of: https://youtube.com/watch?v=%s", ColourStrings[CS_ONGOING], ColourStrings[CS_END], VideoID); + fprintf(stderr, "%s", Message); + int MessageLength = StringLength(Message); + buffer VideoAPIResponse; + ClaimBuffer(&VideoAPIResponse, BID_VIDEO_API_RESPONSE, Kilobytes(1)); - CURL *curl = curl_easy_init(); - if(curl) { - LastPrivacyCheck = time(0); + CURL *curl = curl_easy_init(); + if(curl) { + LastPrivacyCheck = time(0); #define APIKey "AIzaSyAdV2U8ivPk8PHMaPMId0gynksw_gdzr9k" - // NOTE(matt): Stack-string - char URL[1024] = {0}; - CopyString(URL, sizeof(URL), "https://www.googleapis.com/youtube/v3/videos?key=%s&part=status&id=%s", APIKey, VideoID); - CURLcode CurlReturnCode; - curl_easy_setopt(curl, CURLOPT_WRITEDATA, &VideoAPIResponse.Ptr); - curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlIntoBuffer); - curl_easy_setopt(curl, CURLOPT_URL, URL); - if((CurlReturnCode = curl_easy_perform(curl))) - { - fprintf(stderr, "%s\n", curl_easy_strerror(CurlReturnCode)); - } - curl_easy_cleanup(curl); + // NOTE(matt): Stack-string + char URL[1024] = {0}; + CopyString(URL, sizeof(URL), "https://www.googleapis.com/youtube/v3/videos?key=%s&part=status&id=%s", APIKey, VideoID); + CURLcode CurlReturnCode; + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &VideoAPIResponse.Ptr); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlIntoBuffer); + curl_easy_setopt(curl, CURLOPT_URL, URL); + // TODO(matt): Handle the case when our API quota has depleted / expired + if((CurlReturnCode = curl_easy_perform(curl))) + { + fprintf(stderr, "%s\n", curl_easy_strerror(CurlReturnCode)); + } + curl_easy_cleanup(curl); - VideoAPIResponse.Ptr = VideoAPIResponse.Location; - // TODO(matt): Parse this JSON - SeekBufferForString(&VideoAPIResponse, "{", C_SEEK_FORWARDS, C_SEEK_AFTER); - SeekBufferForString(&VideoAPIResponse, "\"totalResults\": ", C_SEEK_FORWARDS, C_SEEK_AFTER); - if(*VideoAPIResponse.Ptr == '0') - { - DeclaimBuffer(&VideoAPIResponse); - // printf("Private video: https://youtube.com/watch?v=%s\n", VideoID); - ClearTerminalRow(MessageLength); - return TRUE; - } - SeekBufferForString(&VideoAPIResponse, "{", C_SEEK_FORWARDS, C_SEEK_AFTER); - SeekBufferForString(&VideoAPIResponse, "\"privacyStatus\": \"", C_SEEK_FORWARDS, C_SEEK_AFTER); - // NOTE(matt): Stack-string - char Status[16]; - CopyStringNoFormatT(Status, sizeof(Status), VideoAPIResponse.Ptr, '\"'); - if(!StringsDiffer0(Status, "public")) - { - DeclaimBuffer(&VideoAPIResponse); - ClearTerminalRow(MessageLength); - return FALSE; + VideoAPIResponse.Ptr = VideoAPIResponse.Location; + // TODO(matt): Parse this JSON + SeekBufferForString(&VideoAPIResponse, "{", C_SEEK_FORWARDS, C_SEEK_AFTER); + SeekBufferForString(&VideoAPIResponse, "\"totalResults\": ", C_SEEK_FORWARDS, C_SEEK_AFTER); + if(*VideoAPIResponse.Ptr == '0') + { + DeclaimBuffer(&VideoAPIResponse); + // printf("Private video: https://youtube.com/watch?v=%s\n", VideoID); + ClearTerminalRow(MessageLength); + return TRUE; + } + + VideoAPIResponse.Ptr = VideoAPIResponse.Location; + SeekBufferForString(&VideoAPIResponse, "{", C_SEEK_FORWARDS, C_SEEK_AFTER); + SeekBufferForString(&VideoAPIResponse, "\"privacyStatus\": \"", C_SEEK_FORWARDS, C_SEEK_AFTER); + // NOTE(matt): Stack-string + char Status[16]; + CopyStringNoFormatT(Status, sizeof(Status), VideoAPIResponse.Ptr, '\"'); + if(!StringsDiffer0(Status, "public")) + { + DeclaimBuffer(&VideoAPIResponse); + ClearTerminalRow(MessageLength); + return FALSE; + } } + DeclaimBuffer(&VideoAPIResponse); + // printf("Unlisted video: https://youtube.com/watch?v=%s\n", VideoID); + ClearTerminalRow(MessageLength); + return TRUE; + } + else + { + return FALSE; } - DeclaimBuffer(&VideoAPIResponse); - // printf("Unlisted video: https://youtube.com/watch?v=%s\n", VideoID); - ClearTerminalRow(MessageLength); - return TRUE; } speaker * @@ -9210,7 +9253,7 @@ FreeReferences(_memory_book(ref_info) *References) rc ProcessTimecode(buffers *CollationBuffers, neighbourhood *N, string Filepath, memory_book *Strings, menu_buffers *MenuBuffers, index_buffers *IndexBuffers, player_buffers *PlayerBuffers, - medium *DefaultMedium, speakers *Speakers, string Author, + medium *DefaultMedium, speakers *Speakers, person *Host, string Author, _memory_book(ref_info) *ReferencesArray, bool *HasQuoteMenu, bool *HasReferenceMenu, bool *HasFilterMenu, bool *RequiresCineraJS, int *QuoteIdentifier, int *RefIdentifier, @@ -9247,7 +9290,7 @@ ProcessTimecode(buffers *CollationBuffers, neighbourhood *N, string Filepath, me CopyStringToBuffer(&IndexBuffers->Class, " class=\"marker"); - speaker *Speaker = GetSpeaker(&Speakers->Speakers, Anno->author ? Wrap0(Anno->author) : CurrentProject->StreamUsername.Length > 0 ? CurrentProject->StreamUsername : CurrentProject->Owner->ID); + speaker *Speaker = GetSpeaker(&Speakers->Speakers, Anno->author ? Wrap0(Anno->author) : CurrentProject->StreamUsername.Length > 0 ? CurrentProject->StreamUsername : Host->ID); if(!IsCategorisedAFK(Anno)) { if(Speakers->Speakers.ItemCount > 1 && Speaker && !IsCategorisedAuthored(Anno)) @@ -9617,12 +9660,46 @@ ProcessTimecode(buffers *CollationBuffers, neighbourhood *N, string Filepath, me return Result; } +string +GetNumberFromHMMLBaseFilename(string ProjectID, string HMMLBaseFilename) +{ + // TODO(matt): That rigorous notion of numbering, goddammit?! + string Result = {}; + if(HMMLBaseFilename.Length > ProjectID.Length) + { + Result = TrimString(HMMLBaseFilename, ProjectID.Length, 0); + } + return Result; +} + rc HMMLToBuffers(buffers *CollationBuffers, template *BespokeTemplate, string BaseFilename, neighbourhood *N) { MEM_TEST_TOP("HMMLToBuffers"); rc Result = RC_SUCCESS; + RewindCollationBuffers(CollationBuffers); + Clear(CollationBuffers->Custom0, sizeof(CollationBuffers->Custom0)); + Clear(CollationBuffers->Custom1, sizeof(CollationBuffers->Custom1)); + Clear(CollationBuffers->Custom2, sizeof(CollationBuffers->Custom2)); + Clear(CollationBuffers->Custom3, sizeof(CollationBuffers->Custom3)); + Clear(CollationBuffers->Custom4, sizeof(CollationBuffers->Custom4)); + Clear(CollationBuffers->Custom5, sizeof(CollationBuffers->Custom5)); + Clear(CollationBuffers->Custom6, sizeof(CollationBuffers->Custom6)); + Clear(CollationBuffers->Custom7, sizeof(CollationBuffers->Custom7)); + Clear(CollationBuffers->Custom8, sizeof(CollationBuffers->Custom8)); + Clear(CollationBuffers->Custom9, sizeof(CollationBuffers->Custom9)); + Clear(CollationBuffers->Custom10, sizeof(CollationBuffers->Custom10)); + Clear(CollationBuffers->Custom11, sizeof(CollationBuffers->Custom11)); + Clear(CollationBuffers->Custom12, sizeof(CollationBuffers->Custom12)); + Clear(CollationBuffers->Custom13, sizeof(CollationBuffers->Custom13)); + Clear(CollationBuffers->Custom14, sizeof(CollationBuffers->Custom14)); + Clear(CollationBuffers->Custom15, sizeof(CollationBuffers->Custom15)); + Clear(CollationBuffers->Title, sizeof(CollationBuffers->Title)); + Clear(CollationBuffers->URLPlayer, sizeof(CollationBuffers->URLPlayer)); + Clear(CollationBuffers->URLSearch, sizeof(CollationBuffers->URLSearch)); + Clear(CollationBuffers->VODPlatform, sizeof(CollationBuffers->VODPlatform)); + // TODO(matt): A "MakeString0OnStack()" sort of function? // NOTE(matt): Stack-string int NullTerminationBytes = 1; @@ -9656,30 +9733,11 @@ HMMLToBuffers(buffers *CollationBuffers, template *BespokeTemplate, string BaseF // NOTE(matt): WorkingThis.Size > 0 means that this entry was previously processed as non-private. Would we rather // reregister such an entry as newly private? This test lets us avoid calling VideoIsPrivate() for all // entries, on every cinera invocation, when !CurrentProject->IgnorePrivacy - if(N->WorkingThis.Size > 0 || CurrentProject->IgnorePrivacy || !VideoIsPrivate(HMML.metadata.id)) - { - RewindCollationBuffers(CollationBuffers); - Clear(CollationBuffers->Custom0, sizeof(CollationBuffers->Custom0)); - Clear(CollationBuffers->Custom1, sizeof(CollationBuffers->Custom1)); - Clear(CollationBuffers->Custom2, sizeof(CollationBuffers->Custom2)); - Clear(CollationBuffers->Custom3, sizeof(CollationBuffers->Custom3)); - Clear(CollationBuffers->Custom4, sizeof(CollationBuffers->Custom4)); - Clear(CollationBuffers->Custom5, sizeof(CollationBuffers->Custom5)); - Clear(CollationBuffers->Custom6, sizeof(CollationBuffers->Custom6)); - Clear(CollationBuffers->Custom7, sizeof(CollationBuffers->Custom7)); - Clear(CollationBuffers->Custom8, sizeof(CollationBuffers->Custom8)); - Clear(CollationBuffers->Custom9, sizeof(CollationBuffers->Custom9)); - Clear(CollationBuffers->Custom10, sizeof(CollationBuffers->Custom10)); - Clear(CollationBuffers->Custom11, sizeof(CollationBuffers->Custom11)); - Clear(CollationBuffers->Custom12, sizeof(CollationBuffers->Custom12)); - Clear(CollationBuffers->Custom13, sizeof(CollationBuffers->Custom13)); - Clear(CollationBuffers->Custom14, sizeof(CollationBuffers->Custom14)); - Clear(CollationBuffers->Custom15, sizeof(CollationBuffers->Custom15)); - Clear(CollationBuffers->Title, sizeof(CollationBuffers->Title)); - Clear(CollationBuffers->URLPlayer, sizeof(CollationBuffers->URLPlayer)); - Clear(CollationBuffers->URLSearch, sizeof(CollationBuffers->URLSearch)); - Clear(CollationBuffers->VODPlatform, sizeof(CollationBuffers->VODPlatform)); + if(N->WorkingThis.Size > 0 || CurrentProject->IgnorePrivacy || !VideoIsPrivate(CurrentProject->VODPlatform, HMML.metadata.id)) + { + // TODO(matt): Do a catch-all function that checks for missing info at the head, such as everything from here + // to the loop and additionally an indexer if(BaseFilename.Length > MAX_BASE_FILENAME_LENGTH) { IndexingErrorSizing(&FilepathL, 0, "Base filename", BaseFilename, MAX_BASE_FILENAME_LENGTH); @@ -9702,15 +9760,24 @@ HMMLToBuffers(buffers *CollationBuffers, template *BespokeTemplate, string BaseF Title = Wrap0(HMML.metadata.title); } - if(!HMML.metadata.member) + person *Host = CurrentProject->Owner; + if(HMML.metadata.member) { - IndexingError(&FilepathL, 0, S_ERROR, "The [video] node lacks a \"member\"", 0); - Result = RC_ERROR_HMML; + Host = GetPersonFromConfig(Wrap0(HMML.metadata.member)); + if(!Host && CurrentProject->Owner) + { + fprintf(stderr, "Falling back to the owner set in the config: "); + PrintStringCN(CS_MAGENTA_BOLD, CurrentProject->Owner->ID, FALSE, TRUE); + Host = CurrentProject->Owner; + } } - else if(!GetPersonFromConfig(Wrap0(HMML.metadata.member))) + + if(!Host) { - ErrorCredentials(FilepathL, Wrap0(HMML.metadata.member), R_HOST); + IndexingError(&FilepathL, 0, S_ERROR, "No known owner set in the config, or member in the [video] node", 0); Result = RC_ERROR_HMML; + //ErrorCredentials(FilepathL, Wrap0(HMML.metadata.member), R_HOST); + //Result = RC_ERROR_HMML; } if(!HMML.metadata.id) @@ -9928,14 +9995,14 @@ HMMLToBuffers(buffers *CollationBuffers, template *BespokeTemplate, string BaseF "
\n"); bool RequiresCineraJS = FALSE; - Result = BuildCredits(FilepathL, &MenuBuffers.Credits, &HMML.metadata, &Speakers, &RequiresCineraJS); + Result = BuildCredits(FilepathL, &MenuBuffers.Credits, &HMML.metadata, Host, &Speakers, &RequiresCineraJS); if(Result == RC_SUCCESS) { - CopyStringToBuffer(&CollationBuffers->SearchEntry, "name: \""); - CopyStringToBuffer(&CollationBuffers->SearchEntry, "%.*s", (int)OutputLocation.Length, OutputLocation.Base); + CopyStringToBuffer(&CollationBuffers->SearchEntry, "location: \"%.*s\"\n", (int)OutputLocation.Length, OutputLocation.Base); + string Number = GetNumberFromHMMLBaseFilename(Wrap0i(N->Project->ID, sizeof(N->Project->ID)), BaseFilename); + CopyStringToBuffer(&CollationBuffers->SearchEntry, "number: \"%.*s\"\n", (int)Number.Length, Number.Base); - CopyStringToBuffer(&CollationBuffers->SearchEntry, "\"\n" - "title: \""); + CopyStringToBuffer(&CollationBuffers->SearchEntry, "title: \""); CopyStringToBufferNoFormat(&CollationBuffers->SearchEntry, Wrap0(HMML.metadata.title)); CopyStringToBuffer(&CollationBuffers->SearchEntry, "\"\n" "markers:\n"); @@ -9964,12 +10031,12 @@ HMMLToBuffers(buffers *CollationBuffers, template *BespokeTemplate, string BaseF if(Anno->quote.author) { Author = Wrap0(Anno->quote.author); } else if(HMML.metadata.stream_username) { Author = Wrap0(HMML.metadata.stream_username); } else if(CurrentProject->StreamUsername.Length > 0) { Author = CurrentProject->StreamUsername; } - else { Author = CurrentProject->Owner->ID; } + else { Author = Host->ID; } /* */ MEM_TEST_MID("HMMLToBuffers"); /* +MEM */ Result = ProcessTimecode(CollationBuffers, N, Wrap0(Filepath), &Strings, /* */ &MenuBuffers, &IndexBuffers, &PlayerBuffers, - /* */ DefaultMedium, &Speakers, Author, &ReferencesArray, + /* */ DefaultMedium, &Speakers, Host, Author, &ReferencesArray, /* */ &HasQuoteMenu, &HasReferenceMenu, &HasFilterMenu, &RequiresCineraJS, /* */ &QuoteIdentifier, &RefIdentifier, /* */ &Topics, &Media, @@ -10167,7 +10234,7 @@ HMMLToBuffers(buffers *CollationBuffers, template *BespokeTemplate, string BaseF } CopyStringToBuffer(&PlayerBuffers.Menus, - "
\n" + "
\n" " ?\n" "
\n" " ?

Keyboard Navigation

\n" @@ -10347,7 +10414,8 @@ HMMLToBuffers(buffers *CollationBuffers, template *BespokeTemplate, string BaseF CopyStringToBuffer(&CollationBuffers->IncludesPlayer, "\n" - " \n", + " \n" + " \n", CINERA_APP_VERSION.Major, CINERA_APP_VERSION.Minor, CINERA_APP_VERSION.Patch); @@ -10362,7 +10430,7 @@ HMMLToBuffers(buffers *CollationBuffers, template *BespokeTemplate, string BaseF for(int i = 0; i < Topics.ItemCount; ++i) { category_info *This = GetPlaceInBook(&Topics, i); - if(StringsMatch(This->Marker, Wrap0("nullTopic"))) + if(!StringsMatch(This->Marker, Wrap0("nullTopic"))) { CopyStringToBuffer(&CollationBuffers->IncludesPlayer, "%.*s, ", (int)This->Marker.Length, This->Marker.Base); } @@ -10477,7 +10545,7 @@ HMMLToBuffers(buffers *CollationBuffers, template *BespokeTemplate, string BaseF CopyStringToBuffer(&PlayerBuffers.Script, "\">"); - CopyStringToBuffer(&CollationBuffers->Player, "
\n" + CopyStringToBuffer(&CollationBuffers->Player, "
\n" " "); CopyLandmarkedBuffer(&CollationBuffers->Player, &PlayerBuffers.Menus, 0, PAGE_PLAYER); CopyStringToBuffer(&CollationBuffers->Player, "\n" @@ -10555,12 +10623,16 @@ typedef struct html_element HTMLElements[] = { { "a", FALSE }, + { "br", TRUE }, { "div", FALSE }, + { "h1", FALSE }, + { "h2", FALSE }, { "img", TRUE }, { "input", TRUE }, { "label", FALSE }, { "li", FALSE }, { "nav", FALSE }, + { "p", FALSE }, { "script", FALSE }, { "span", FALSE }, { "ul", FALSE }, @@ -10569,12 +10641,16 @@ html_element HTMLElements[] = typedef enum { NODE_A, + NODE_BR, NODE_DIV, + NODE_H1, + NODE_H2, NODE_IMG, NODE_INPUT, NODE_LABEL, NODE_LI, NODE_NAV, + NODE_P, NODE_SCRIPT, NODE_SPAN, NODE_UL, @@ -11619,10 +11695,9 @@ InsertIntoDB(neighbourhood *N, buffers *CollationBuffers, template *BespokeTempl CycleFile(&DB.File); } - string EntryTitle = Wrap0(CollationBuffers->Title); - if(!VideoIsPrivate || !RecheckingPrivacy) { + string EntryTitle = Wrap0(CollationBuffers->Title); LogEdit(EditType, CurrentProject->Lineage, BaseFilename, &EntryTitle, VideoIsPrivate); PrintEdit(EditType, CurrentProject->Lineage, BaseFilename, &EntryTitle, VideoIsPrivate, TRUE); } @@ -12825,7 +12900,7 @@ GenerateFilterOfProjectAndChildren(buffer *Filter, db_header_project *StoredP, p if(!TopLevel || StoredP->EntryCount > 0) { OpenNodeNewLine(Filter, &Filter->IndentLevel, NODE_DIV, 0); - AppendStringToBuffer(Filter, Wrap0(" class=\"cineraFilterProject\" data-baseURL=\"")); + AppendStringToBuffer(Filter, Wrap0(" class=\"cineraMenuItem cineraFilterProject\" data-baseURL=\"")); AppendStringToBuffer(Filter, P->BaseURL); AppendStringToBuffer(Filter, Wrap0("\" data-searchLocation=\"")); AppendStringToBuffer(Filter, P->SearchLocation); @@ -12872,6 +12947,8 @@ GenerateIndexOfProjectAndChildren(buffer *Index, db_header_project *StoredP, pro AppendStringToBuffer(Index, P->SearchLocation); AppendStringToBuffer(Index, Wrap0("\" data-playerLocation=\"")); AppendStringToBuffer(Index, P->PlayerLocation); + AppendStringToBuffer(Index, Wrap0("\" data-unit=\"")); + AppendStringToBuffer(Index, P->Unit); AppendStringToBuffer(Index, Wrap0("\">")); OpenNodeNewLine(Index, &Index->IndentLevel, NODE_DIV, 0); @@ -12905,27 +12982,28 @@ GenerateIndexOfProjectAndChildren(buffer *Index, db_header_project *StoredP, pro if(*StoredP->Unit) { - // TODO(matt): That rigorous notion of numbering, goddammit?! - string NumberL = TrimString(Wrap0i(Entry->HMMLBaseFilename, sizeof(Entry->HMMLBaseFilename)), - P->ID.Length, 0); - char Number[NumberL.Length + 1]; - ClearCopyStringNoFormat(Number, sizeof(Number), NumberL); - - if(CurrentProject->NumberingScheme == NS_LINEAR) + string NumberL = GetNumberFromHMMLBaseFilename(P->ID, Wrap0i(Entry->HMMLBaseFilename, sizeof(Entry->HMMLBaseFilename))); + if(NumberL.Length > 0) { - for(int i = 0; Number[i]; ++i) + char Number[NumberL.Length + 1]; + ClearCopyStringNoFormat(Number, sizeof(Number), NumberL); + + if(CurrentProject->NumberingScheme == NS_LINEAR) { - if(Number[i] == '_') + for(int i = 0; Number[i]; ++i) { - Number[i] = '.'; + if(Number[i] == '_') + { + Number[i] = '.'; + } } } - } - AppendStringToBuffer(Index, P->Unit); - AppendStringToBuffer(Index, Wrap0(" ")); - AppendStringToBuffer(Index, Wrap0i(Number, sizeof(Number))); - AppendStringToBuffer(Index, Wrap0(": ")); + AppendStringToBuffer(Index, P->Unit); + AppendStringToBuffer(Index, Wrap0(" ")); + AppendStringToBuffer(Index, Wrap0i(Number, sizeof(Number))); + AppendStringToBuffer(Index, Wrap0(": ")); + } } // HERE @@ -13097,6 +13175,45 @@ GenerateThemeLinks(buffer *IncludesSearch, project *P) FreeBook(&UniqueThemes); } +void +AppendHelpKeyToBuffer(buffer *B, uint32_t *IndentationLevel, string Key, bool Available) +{ + OpenNodeNewLine(B, IndentationLevel, NODE_SPAN, 0); + AppendStringToBuffer(B, Wrap0(" class=\"help_key")); + if(!Available) + { + AppendStringToBuffer(B, Wrap0(" unavailable")); + } + AppendStringToBuffer(B, Wrap0("\">")); + AppendStringToBuffer(B, Key); + CloseNode(B, IndentationLevel, NODE_SPAN); +} + +void +AppendHelpKeyToBufferNewLine(buffer *B, uint32_t *IndentationLevel, string Key, bool Available) +{ + AppendHelpKeyToBuffer(B, IndentationLevel, Key, Available); + OpenNodeC(B, IndentationLevel, NODE_BR, 0); +} + +void +AppendHelpKeyAndTextToBuffer(buffer *B, uint32_t *IndentationLevel, string Key, string Text, bool Available) +{ + AppendHelpKeyToBuffer(B, IndentationLevel, Key, Available); + AppendStringToBuffer(B, Wrap0(" ")); + + OpenNodeNewLine(B, IndentationLevel, NODE_SPAN, 0); + AppendStringToBuffer(B, Wrap0(" class=\"help_text")); + if(!Available) + { + AppendStringToBuffer(B, Wrap0(" unavailable")); + } + AppendStringToBuffer(B, Wrap0("\">")); + AppendStringToBuffer(B, Text); + CloseNode(B, IndentationLevel, NODE_SPAN); + AppendStringToBuffer(B, Wrap0("\n")); +} + int SearchToBuffer(buffers *CollationBuffers, db_header_project *StoredP, project *P, string Theme, bool RequiresCineraJS) // NOTE(matt): This guy malloc's CollationBuffers->Search { @@ -13134,6 +13251,7 @@ SearchToBuffer(buffers *CollationBuffers, db_header_project *StoredP, project *P CopyStringToBuffer(&CollationBuffers->IncludesSearch, "\n" " \n" + " \n" "\n" " IncludesSearch, "\">"); - asset *JSSearch = GetAsset(Wrap0(BuiltinAssets[ASSET_JS_SEARCH].Filename), ASSET_JS); - ConstructResolvedAssetURL(&URL, JSSearch, PAGE_SEARCH); - uint32_t IndentationLevel = 1; ++IndentationLevel; - buffer Script = {}; - Script.ID = BID_SCRIPT; - OpenNodeNewLine(&Script, &IndentationLevel, NODE_SCRIPT, 0); - AppendStringToBuffer(&Script, Wrap0(" type=\"text/javascript\" src=\"")); - AppendStringToBuffer(&Script, Wrap0i(URL.Location, URL.Ptr - URL.Location)); - DeclaimBuffer(&URL); - PushAssetLandmark(&Script, JSSearch, PAGE_SEARCH, TRUE); - AppendStringToBuffer(&Script, Wrap0("\">")); - CloseNode(&Script, &IndentationLevel, NODE_SCRIPT); - bool SearchRequired = FALSE; //IndentBuffer(&CollationBuffers->Search, ++IndentationLevel); buffer *B = &CollationBuffers->Search; - OpenNode(B, &IndentationLevel, NODE_DIV, "cineraIndexControl"); + OpenNodeNewLine(B, &IndentationLevel, NODE_DIV, "cineraIndex"); AppendStringToBuffer(B, Wrap0(" class=\"")); AppendStringToBuffer(B, Theme); AppendStringToBuffer(B, Wrap0("\">")); + OpenNodeC(B, &IndentationLevel, NODE_DIV, "cineraIndexControl"); - OpenNodeCNewLine(B, &IndentationLevel, NODE_DIV, "cineraIndexSort"); + OpenNodeNewLine(B, &IndentationLevel, NODE_DIV, 0); + AppendStringToBuffer(B, Wrap0(" class=\"cineraMenu cineraMenuTitle ViewSettings\">")); + OpenNodeCNewLine(B, &IndentationLevel, NODE_SPAN, 0); + AppendStringToBuffer(B, Wrap0("Settings")); + CloseNodeNewLine(B, &IndentationLevel, NODE_SPAN); + + OpenNodeNewLine(B, &IndentationLevel, NODE_DIV, 0); + AppendStringToBuffer(B, Wrap0(" class=\"cineraMenuContainer\">")); + + OpenNodeNewLine(B, &IndentationLevel, NODE_DIV, 0); + AppendStringToBuffer(B, Wrap0(" class=\"cineraMenuItem sort\">")); AppendStringToBuffer(B, Wrap0("Sort: Old to New ⏶")); - CloseNode(B, &IndentationLevel, NODE_DIV); + OpenNodeNewLine(B, &IndentationLevel, NODE_DIV, 0); + AppendStringToBuffer(B, Wrap0(" class=\"cineraMenuItem view\">")); + AppendStringToBuffer(B, Wrap0("View: Grid")); + CloseNode(B, &IndentationLevel, NODE_DIV); + + OpenNodeNewLine(B, &IndentationLevel, NODE_DIV, 0); + AppendStringToBuffer(B, Wrap0(" class=\"cineraMenuItem anim\">")); + AppendStringToBuffer(B, Wrap0("Animations: ✔")); + CloseNode(B, &IndentationLevel, NODE_DIV); + + OpenNodeNewLine(B, &IndentationLevel, NODE_DIV, 0); + AppendStringToBuffer(B, Wrap0(" class=\"cineraMenuItem save\">")); + AppendStringToBuffer(B, Wrap0("Save Settings: ✔")); + CloseNode(B, &IndentationLevel, NODE_DIV); + + CloseNodeNewLine(B, &IndentationLevel, NODE_DIV); // cineraMenuContainer + + CloseNodeNewLine(B, &IndentationLevel, NODE_DIV); // cineraMenu ViewSettings + bool FilterRequired = FALSE; if(StoredP) { @@ -13221,7 +13355,7 @@ SearchToBuffer(buffers *CollationBuffers, db_header_project *StoredP, project *P OpenNodeNewLine(B, &IndentationLevel, NODE_DIV, 0); - AppendStringToBuffer(B, Wrap0(" class=\"cineraIndexFilter\">")); + AppendStringToBuffer(B, Wrap0(" class=\"cineraMenu cineraMenuTitle IndexFilter\">")); OpenNodeCNewLine(B, &IndentationLevel, NODE_SPAN, 0); @@ -13237,7 +13371,7 @@ SearchToBuffer(buffers *CollationBuffers, db_header_project *StoredP, project *P CloseNode(B, &IndentationLevel, NODE_SPAN); OpenNodeNewLine(B, &IndentationLevel, NODE_DIV, 0); - AppendStringToBuffer(B, Wrap0(" class=\"filter_container\">")); + AppendStringToBuffer(B, Wrap0(" class=\"cineraMenuContainer\">")); } // NOTE(matt): What I think the rules should be are: @@ -13271,8 +13405,8 @@ SearchToBuffer(buffers *CollationBuffers, db_header_project *StoredP, project *P { AppendLandmarkedBuffer(B, &Filter, PAGE_SEARCH); FreeBuffer(&Filter); - CloseNodeNewLine(B, &IndentationLevel, NODE_DIV); - CloseNodeNewLine(B, &IndentationLevel, NODE_DIV); + CloseNodeNewLine(B, &IndentationLevel, NODE_DIV); // cineraMenuContainer + CloseNodeNewLine(B, &IndentationLevel, NODE_DIV); // cineraMenu IndexFilter } asset *JSCineraPre = GetAsset(Wrap0(BuiltinAssets[ASSET_JS_CINERA_PRE].Filename), ASSET_JS); @@ -13298,6 +13432,26 @@ SearchToBuffer(buffers *CollationBuffers, db_header_project *StoredP, project *P "\" defer>"); } + asset *JSSearchPre = GetAsset(Wrap0(BuiltinAssets[ASSET_JS_SEARCH_PRE].Filename), ASSET_JS); + ConstructResolvedAssetURL(&URL, JSSearchPre, PAGE_SEARCH); + CopyStringToBuffer(&CollationBuffers->IncludesSearch, + "\n "); + + asset *JSSearchPost = GetAsset(Wrap0(BuiltinAssets[ASSET_JS_SEARCH_POST].Filename), ASSET_JS); + ConstructResolvedAssetURL(&URL, JSSearchPost, PAGE_SEARCH); + CopyStringToBuffer(&CollationBuffers->IncludesSearch, + "\n "); + OpenNodeNewLine(B, &IndentationLevel, NODE_DIV, 0); AppendStringToBuffer(B, Wrap0(" class=\"cineraQueryContainer\">")); @@ -13320,10 +13474,82 @@ SearchToBuffer(buffers *CollationBuffers, db_header_project *StoredP, project *P IndentBuffer(B, IndentationLevel); AppendStringToBuffer(B, Wrap0("Downloading data...")); - CloseNodeNewLine(B, &IndentationLevel, NODE_DIV); - CloseNodeNewLine(B, &IndentationLevel, NODE_DIV); - CloseNodeNewLine(B, &IndentationLevel, NODE_DIV); - CloseNodeNewLine(B, &IndentationLevel, NODE_DIV); + CloseNodeNewLine(B, &IndentationLevel, NODE_DIV); // spinner + CloseNodeNewLine(B, &IndentationLevel, NODE_DIV); // inputContainer + CloseNodeNewLine(B, &IndentationLevel, NODE_DIV); // cineraQueryContainer + + OpenNodeNewLine(B, &IndentationLevel, NODE_DIV, 0); + AppendStringToBuffer(B, Wrap0(" class=\"cineraHelp\">")); + OpenNodeCNewLine(B, &IndentationLevel, NODE_SPAN, 0); + AppendStringToBuffer(B, Wrap0("?")); + CloseNode(B, &IndentationLevel, NODE_SPAN); + + OpenNodeNewLine(B, &IndentationLevel, NODE_DIV, 0); + AppendStringToBuffer(B, Wrap0(" class=\"help_container\">")); + + AppendHelpKeyToBuffer(B, &IndentationLevel, Wrap0("?"), TRUE); + OpenNodeC(B, &IndentationLevel, NODE_H1, 0); + AppendStringToBuffer(B, Wrap0("Keyboard Navigation")); + CloseNode(B, &IndentationLevel, NODE_H1); + + OpenNodeC(B, &IndentationLevel, NODE_H2, 0); + AppendStringToBuffer(B, Wrap0("Grid Traversal")); + CloseNode(B, &IndentationLevel, NODE_H2); + AppendHelpKeyAndTextToBuffer(B, &IndentationLevel, Wrap0("h"), Wrap0("Shift leftwards"), TRUE); + AppendHelpKeyAndTextToBuffer(B, &IndentationLevel, Wrap0("k"), Wrap0("Ascend (zoom out)"), TRUE); + AppendHelpKeyAndTextToBuffer(B, &IndentationLevel, Wrap0("l"), Wrap0("Shift rightwards"), TRUE); + + OpenNodeCNewLine(B, &IndentationLevel, NODE_BR, 0); + + OpenNodeNewLine(B, &IndentationLevel, NODE_DIV, 0); + AppendStringToBuffer(B, Wrap0(" class=\"help_paragraph\">")); + + OpenNodeNewLine(B, &IndentationLevel, NODE_DIV, 0); + AppendStringToBuffer(B, Wrap0(" class=\"key_block\">")); + + OpenNodeCNewLine(B, &IndentationLevel, NODE_DIV, 0); + AppendHelpKeyToBufferNewLine(B, &IndentationLevel, Wrap0("1"), TRUE); + AppendHelpKeyToBufferNewLine(B, &IndentationLevel, Wrap0("q"), TRUE); + AppendHelpKeyToBufferNewLine(B, &IndentationLevel, Wrap0("a"), TRUE); + AppendHelpKeyToBuffer(B, &IndentationLevel, Wrap0("z"), TRUE); + CloseNodeNewLine(B, &IndentationLevel, NODE_DIV); + + OpenNodeCNewLine(B, &IndentationLevel, NODE_DIV, 0); + AppendHelpKeyToBufferNewLine(B, &IndentationLevel, Wrap0("2"), TRUE); + AppendHelpKeyToBufferNewLine(B, &IndentationLevel, Wrap0("w"), TRUE); + AppendHelpKeyToBufferNewLine(B, &IndentationLevel, Wrap0("s"), TRUE); + AppendHelpKeyToBuffer(B, &IndentationLevel, Wrap0("x"), TRUE); + CloseNodeNewLine(B, &IndentationLevel, NODE_DIV); + + OpenNodeCNewLine(B, &IndentationLevel, NODE_DIV, 0); + AppendHelpKeyToBufferNewLine(B, &IndentationLevel, Wrap0("3"), TRUE); + AppendHelpKeyToBufferNewLine(B, &IndentationLevel, Wrap0("e"), TRUE); + AppendHelpKeyToBufferNewLine(B, &IndentationLevel, Wrap0("d"), TRUE); + AppendHelpKeyToBuffer(B, &IndentationLevel, Wrap0("c"), TRUE); + CloseNodeNewLine(B, &IndentationLevel, NODE_DIV); + + OpenNodeCNewLine(B, &IndentationLevel, NODE_DIV, 0); + AppendHelpKeyToBufferNewLine(B, &IndentationLevel, Wrap0("4"), TRUE); + AppendHelpKeyToBufferNewLine(B, &IndentationLevel, Wrap0("r"), TRUE); + AppendHelpKeyToBufferNewLine(B, &IndentationLevel, Wrap0("f"), TRUE); + AppendHelpKeyToBuffer(B, &IndentationLevel, Wrap0("v"), TRUE); + CloseNodeNewLine(B, &IndentationLevel, NODE_DIV); + + CloseNodeNewLine(B, &IndentationLevel, NODE_DIV); // key_block + CloseNodeNewLine(B, &IndentationLevel, NODE_DIV); // help_paragraph + + OpenNodeC(B, &IndentationLevel, NODE_H2, 0); + AppendStringToBuffer(B, Wrap0("Settings")); + CloseNode(B, &IndentationLevel, NODE_H2); + AppendHelpKeyAndTextToBuffer(B, &IndentationLevel, Wrap0("t"), Wrap0("Toggle sort order"), TRUE); + AppendHelpKeyAndTextToBuffer(B, &IndentationLevel, Wrap0("y"), Wrap0("Toggle view style"), TRUE); + AppendHelpKeyAndTextToBuffer(B, &IndentationLevel, Wrap0("m"), Wrap0("Toggle animations"), TRUE); + + CloseNodeNewLine(B, &IndentationLevel, NODE_DIV); // help_container + + CloseNodeNewLine(B, &IndentationLevel, NODE_DIV); // cineraHelp + + CloseNodeNewLine(B, &IndentationLevel, NODE_DIV); // cineraIndexControl OpenNodeCNewLine(B, &IndentationLevel, NODE_DIV, "cineraResultsSummary"); @@ -13340,17 +13566,50 @@ SearchToBuffer(buffers *CollationBuffers, db_header_project *StoredP, project *P AppendStringToBuffer(B, Wrap0("\n")); - OpenNodeCNewLine(B, &IndentationLevel, NODE_DIV, "cineraIndex"); + OpenNodeCNewLine(B, &IndentationLevel, NODE_DIV, "cineraIndexList"); AppendLandmarkedBuffer(B, &Index, PAGE_SEARCH); FreeBuffer(&Index); - CloseNodeNewLine(B, &IndentationLevel, NODE_DIV); + CloseNodeNewLine(B, &IndentationLevel, NODE_DIV); // cineraIndexList - AppendLandmarkedBuffer(B, &Script, PAGE_SEARCH); - FreeBuffer(&Script); + OpenNodeNewLine(B, &IndentationLevel, NODE_DIV, 0); + AppendStringToBuffer(B, Wrap0(" class=\"cineraIndexGridContainer\">")); + + OpenNodeNewLine(B, &IndentationLevel, NODE_DIV, 0); + AppendStringToBuffer(B, Wrap0(" class=\"cineraTraversalContainer\">")); + + OpenNodeNewLine(B, &IndentationLevel, NODE_DIV, 0); + AppendStringToBuffer(B, Wrap0(" class=\"cineraTraversal\">")); + + OpenNodeNewLine(B, &IndentationLevel, NODE_DIV, 0); + AppendStringToBuffer(B, Wrap0(" class=\"cineraButton prev\">")); + OpenNodeC(B, &IndentationLevel, NODE_P, 0); + AppendStringToBuffer(B, Wrap0("←")); + CloseNode(B, &IndentationLevel, NODE_P); + CloseNode(B, &IndentationLevel, NODE_DIV); + + OpenNodeNewLine(B, &IndentationLevel, NODE_DIV, 0); + AppendStringToBuffer(B, Wrap0(" class=\"cineraButton ascension\">")); + OpenNodeC(B, &IndentationLevel, NODE_P, 0); + AppendStringToBuffer(B, Wrap0("↑")); + CloseNode(B, &IndentationLevel, NODE_P); + CloseNode(B, &IndentationLevel, NODE_DIV); + + OpenNodeNewLine(B, &IndentationLevel, NODE_DIV, 0); + AppendStringToBuffer(B, Wrap0(" class=\"cineraButton next\">")); + OpenNodeC(B, &IndentationLevel, NODE_P, 0); + AppendStringToBuffer(B, Wrap0("→")); + CloseNode(B, &IndentationLevel, NODE_P); + CloseNode(B, &IndentationLevel, NODE_DIV); + + CloseNodeNewLine(B, &IndentationLevel, NODE_DIV); // cineraTraversal + CloseNodeNewLine(B, &IndentationLevel, NODE_DIV); // cineraTraversalContainer + CloseNodeNewLine(B, &IndentationLevel, NODE_DIV); // cineraIndexGridContainer + + CloseNodeNewLine(B, &IndentationLevel, NODE_DIV); // cineraIndex if(!SearchRequired) { return RC_NOOP; } else { return RC_SUCCESS; } diff --git a/cinera/cinera.css b/cinera/cinera.css index 03804a8..aa721ad 100644 --- a/cinera/cinera.css +++ b/cinera/cinera.css @@ -58,7 +58,7 @@ nav.cineraNavDropdown .cineraNavTitle { nav.cineraNavDropdown ul.cineraNavHorizontal { display: none; width: 100%; - z-index: 8; + z-index: 4096; position: absolute; } @@ -85,12 +85,124 @@ ul.cineraNavPlain li.current > a { /* Index */ -#cineraIndexControl { +#cineraIndex +{ display: flex; - margin: 16px auto; - max-width: 1024px; + flex-direction: column; +} + +#cineraIndex #cineraIndexControl +{ + display: flex; + position: -webkit-sticky; + position: sticky; + top: 0px; + z-index: 64; + + user-select: none; + -moz-user-select: none; + -webkit-user-select: none; +} + +#cineraIndex .cineraIndexGridContainer +{ + display: flex; + flex-direction: column; align-items: center; - padding: 8px; + justify-content: center; +} + +#cineraIndex .cineraIndexGridContainer.Portrait +{ + flex-direction: column-reverse; +} + +#cineraIndex .cineraIndexGridContainer.Landscape.Left +{ + flex-direction: row-reverse; +} + +#cineraIndex .cineraIndexGridContainer.Landscape.Right +{ + flex-direction: row; +} + +#cineraIndex .cineraIndexGridContainer.Landscape .cineraTraversal +{ + flex-direction: column; +} + +#cineraIndex .cineraIndexGridContainer.Landscape .cineraTraversal .cineraButton.prev +{ + order: 1; +} + +#cineraIndex .cineraIndexGridContainer.Landscape .cineraTraversal .cineraButton.next +{ + order: 2; +} + +#cineraIndex .cineraTraversal .cineraButton +{ + /* TODO(matt): Do the cineraTraversal as a flex box, to auto-size these buttons? */ + height: 42px; + width: 42px; + + padding: 2px; + margin: 2px; + + display: inline-flex; + align-items: center; + justify-content: center; +} + +#cineraIndex.anim .cineraTraversal, +#cineraIndex.anim .cineraTraversal * p +{ + transition: transform .32s; +} + +#cineraIndex .cineraTraversalContainer +{ + overflow: hidden; +} + +#cineraIndex .cineraTraversal +{ + display: flex; + justify-content: center; + transform: rotate(0deg); +} + +#cineraIndex.reversed .cineraTraversal +{ + transform: rotate(180deg); +} + +#cineraIndex .cineraTraversal * p +{ + transform: rotate(0deg); +} + +#cineraIndex.reversed .cineraTraversal .ascension p +{ + transform: rotate(-180deg); +} + +#cineraIndex.reversed .cineraTraversal .prev p, +#cineraIndex.reversed .cineraTraversal .next p +{ + transform: rotate(-360deg); +} + +#cineraIndex .cineraTraversal .next.ascension p +{ + transform: rotate(-45deg); +} + +#cineraIndex.reversed .cineraTraversal .next.ascension p +{ + transform: rotate(-315deg); } .cineraFilterProject .cineraText, @@ -99,15 +211,30 @@ ul.cineraNavPlain li.current > a { display: flex; } -.cineraIndexFilter { +.cineraMenu.cineraMenuTitle { padding: 10px; + cursor: default; } -.cineraIndexFilter .filter_container +.cineraMenuContainer { display: none; position: absolute; + top: 100%; z-index: 64; + + cursor: pointer; + user-select: none; + -moz-user-select: none; + -webkit-user-select: none; + + border: 1px solid; + border-top: none; +} + +.cineraMenuContainer.visible +{ + display: block; } .cineraFilterProject, @@ -120,7 +247,7 @@ ul.cineraNavPlain li.current > a { } .cineraIndexProject .cineraProjectTitle, -.cineraFilterProject +.cineraMenuItem { font-weight: bold; font-size: 12px; @@ -130,7 +257,6 @@ ul.cineraNavPlain li.current > a { flex-grow: 1; padding-left: 16px; display: flex; - flex-direction: horizontal; margin: auto; } @@ -141,19 +267,21 @@ ul.cineraNavPlain li.current > a { } .cineraQueryContainer .inputContainer { + display: flex; flex-grow: 1; position: relative; + padding: .5em; } .cineraQueryContainer #query { - width: 100%; + flex-grow: 1; } .cineraQueryContainer .inputContainer .spinner { position: absolute; top: 2px; right: 5px; - color: black; + color: #000000; height: 100%; display: none; } @@ -163,7 +291,7 @@ ul.cineraNavPlain li.current > a { } #cineraResults, -#cineraIndex { +#cineraIndexList { margin: 0 auto; max-width: 800px; } @@ -173,22 +301,94 @@ ul.cineraNavPlain li.current > a { flex-flow: column; } -#cineraIndexControl #cineraIndexSort { - padding: 4px; +#cineraIndexList .cineraIndexEntries { + display: flex; + flex-flow: column; + width: 100%; } -#cineraIndexControl #cineraIndexSort, -#cineraIndexControl .cineraIndexFilter .filter_container { - cursor: pointer; +#cineraIndex #cineraIndexGrid +{ + display: flex; + overflow: hidden; + position: relative; + perspective-origin: center; + -webkit-perspective-origin: center; +} + +#cineraIndex #cineraIndexGrid .cineraButtons +{ + flex-shrink: 0; + display: grid; + grid-gap: 2px; + backface-visibility: hidden; + -webkit-backface-visibility: hidden; +} + +#cineraIndex .cineraButton +{ + border: 1px solid; + + text-align: center; + font-size: 16px; + font-weight: bold; + + flex-direction: column; user-select: none; -moz-user-select: none; -webkit-user-select: none; } -#cineraIndex .cineraIndexEntries { +#cineraIndex #cineraIndexGrid .cineraButton.subdivision +{ + box-sizing: border-box; display: flex; - flex-flow: column; + backface-visibility: hidden; + -webkit-backface-visibility: hidden; +} + +#cineraIndex #cineraIndexGrid .cineraButton.subdivision .head-item, +#cineraIndex #cineraIndexGrid .cineraButton.subdivision .tail-item +{ width: 100%; + height: 50%; + display: flex; +} + +#cineraIndex #cineraIndexGrid .cineraButton.subdivision.leaf +{ + justify-content: center; +} + +#cineraIndex #cineraIndexGrid .cineraButton.subdivision.leaf a +{ + text-decoration: none; +} + +#cineraIndex #cineraIndexGrid .cineraButton.subdivision.leaf a:hover { + cursor: default; +} + +#cineraIndex #cineraIndexGrid .cineraButton.subdivision .head-item, +#cineraIndex.reversed #cineraIndexGrid .cineraButton.subdivision .tail-item +{ + align-items: flex-start; + justify-content: flex-start; + text-align: left; +} + +#cineraIndex #cineraIndexGrid .cineraButton.subdivision .tail-item, +#cineraIndex.reversed #cineraIndexGrid .cineraButton.subdivision .head-item +{ + align-items: flex-end; + justify-content: flex-end; + text-align: right; +} + +#cineraIndexList.hidden, +#cineraIndex .cineraIndexGridContainer.hidden +{ + display: none; } #cineraResults .dayContainer { @@ -203,7 +403,7 @@ ul.cineraNavPlain li.current > a { line-height: 16px; padding: 5px; vertical-align: top; - width: 200px; + max-width: 200px; } .cineraIndexEntries div a { @@ -243,36 +443,6 @@ ul.cineraNavPlain li.current > a { display: none; } -@media (max-width: 720px), (max-height: 512px) -{ - #cineraIndexControl { - margin: 4px auto; - } - - #cineraIndexControl, - #cineraResultsSummary { - font-size: 64%; - } - - #cineraResults .dayContainer { - flex-direction: column; - } - - #cineraResults .dayContainer .dayName { - font-weight: bold; - text-align: center; - width: 100%; - } - - #cineraResults .dayContainer .markerList { - max-width: 100%; - } - - .cineraIndexEntries div a { - font-size: 80%; - } -} - /* Player */ /* Player / Structure */ @@ -285,7 +455,7 @@ ul.cineraNavPlain li.current > a { text-decoration: none; } -.cineraMenus > *:not(.views), +.cineraMenus > *:not(.views, .cineraHelp), .cineraMenus > .menu > .refs .ref, .cineraMenus > .menu > .view, .cineraMenus > .menu > .views_container .view, @@ -299,7 +469,14 @@ ul.cineraNavPlain li.current > a { .cineraMenus > .menu { position: relative; - align-self: center; + min-height: 1em; + min-width: 1em; + display: flex; + align-items: center; + justify-content: center; + user-select: none; + -moz-user-select: none; + -webkit-user-select: none; } .cineraMenus > .menu.filter.responsible, @@ -312,67 +489,7 @@ ul.cineraNavPlain li.current > a { animation-iteration-count: 1; } -.cineraMenus .help .help_container .help_key { - font-family: Inconsolata; - font-size: 16px; - border: 1px solid; - display: inline-block; - background-color: #111; /* Per project */ - border-radius: 4px; - height: 16px; - width: 16px; - padding: 4px; - line-height: 16px; - margin: 2px; -} - -.cineraMenus .help .help_container .help_key.word { - width: auto; -} - -.cineraMenus .help .help_container .help_key.modifer { - margin-right: 0; -} - -.cineraMenus .help .help_container .help_key.unavailable, -.cineraMenus .help .help_container .help_text.unavailable, -.cineraMenus .help .help_container h2 .unavailable { - opacity: 0.32; -} - -.cineraMenus .help .help_container .key_block { - display: inline-flex; - align-items: flex-end; - flex-direction: row; - margin: 8px; -} - -.cineraMenus .help .help_container .help_text { - margin: 0 8px 0 2px; -} - - -.cineraMenus .help .help_container h1 { - display: inline; - margin-left: 4px; -} - -.cineraMenus .help .help_container h1:after { - content: "\a"; -} - - -.cineraMenus .help .help_container h2 { - font-size: 16px; - margin-bottom: 8px; -} - -.cineraMenus .help .help_container .help_paragraph { - display: inline-flex; - align-items: center; -} - -.cineraMenus > .help { +.cineraHelp { cursor: pointer; border: 1px solid; border-radius: 4px; @@ -387,32 +504,87 @@ ul.cineraNavPlain li.current > a { z-index: 64; } -.cineraMenus .help .help_container .help_grid { - display: inline-flex; - flex-direction: column; -} - -.cineraMenus .help .help_container { - background-color: black; /* Per project */ - color: #EEE; /* Per project */ +.cineraHelp .help_container { + background-color: #000000; /* Per project */ + color: #EEEEEE; /* Per project */ display: none; font-weight: normal; line-height: 12px; opacity: 0.9; padding: 8px; position: fixed; - right: 612px; - top: 42px; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); } -.cineraMenus .help .help_container.visible { +.cineraHelp .help_container.visible { display: block; } -#cineraIndexControl .cineraIndexFilter .filter_container -{ +.cineraHelp .help_container .help_key { + font-family: Inconsolata; + font-size: 16px; border: 1px solid; - border-top: none; + display: inline-block; + background-color: #111111; /* Per project */ + border-radius: 4px; + height: 16px; + width: 16px; + padding: 4px; + line-height: 16px; + margin: 2px; +} + +.cineraHelp .help_container .help_key.word { + width: auto; +} + +.cineraHelp .help_container .help_key.modifer { + margin-right: 0; +} + +.cineraHelp .help_container .help_key.unavailable, +.cineraHelp .help_container .help_text.unavailable, +.cineraHelp .help_container h2 .unavailable { + opacity: 0.32; +} + +.cineraHelp .help_container .key_block { + display: inline-flex; + align-items: flex-end; + flex-direction: row; + margin: 8px; +} + +.cineraHelp .help_container .help_text { + margin: 0 8px 0 2px; +} + + +.cineraHelp .help_container h1 { + display: inline; + margin-left: 4px; +} + +.cineraHelp .help_container h1:after { + content: "\a"; +} + + +.cineraHelp .help_container h2 { + font-size: 16px; + margin-bottom: 8px; +} + +.cineraHelp .help_container .help_paragraph { + display: inline-flex; + align-items: center; +} + +.cineraHelp .help_container .help_grid { + display: inline-flex; + flex-direction: column; } .cineraMenus > .menu .quotes_container, @@ -430,11 +602,13 @@ ul.cineraNavPlain li.current > a { position: absolute; right: 0; top: 100%; - z-index: 1; + z-index: 8; } .cineraMenus > .menu .refs, -.cineraMenus > .menu .link_container { +.cineraMenus > .menu .filter_container, +.cineraMenus > .menu .link_container, +.cineraMenus > .menu .credits_container { width: 350px; } @@ -442,12 +616,9 @@ ul.cineraNavPlain li.current > a { overflow-x: hidden; } -.cineraMenus > .menu .filter_container { - min-width: 350px; -} - -.cineraMenus > .menu .credits_container { - min-width: 240px; +.cineraMenus > .menu .sizing { + z-index: -1; + display: block; } .cineraMenus > .menu .visible { @@ -639,7 +810,7 @@ ul.cineraNavPlain li.current > a { } .cineraPlayerContainer .markers_container { - flex-shrink: 0; + flex-shrink: 1; overflow-y: scroll; position: relative; } @@ -651,6 +822,16 @@ ul.cineraNavPlain li.current > a { font-weight: bold; } +.cineraPlayerContainer { + background-color: #000000; +} + +.cineraPlayerContainer .video_container { + display: flex; + justify-content: center; + align-self: center; +} + .cineraPlayerContainer .markers_container > a.episodeMarker { cursor: pointer; } @@ -683,7 +864,7 @@ ul.cineraNavPlain li.current > a { border-bottom: 1px solid; position: relative; cursor: pointer; - max-height: 320px; + max-height: 320px; /* NOTE(matt): Required for the transition */ transition: max-height .32s; } @@ -709,7 +890,7 @@ ul.cineraNavPlain li.current > a { font-size: 14px; } -.cineraPlayerContainer .markers_container > .markers .marker.skip { +.cinera:not(.mobile) .cineraPlayerContainer .markers_container > .markers .marker.skip { max-height: 0; transition: max-height .32s; overflow: hidden; @@ -781,97 +962,109 @@ ul.cineraNavPlain li.current > a { margin-right: 8px; } -@media (max-width: 720px), (max-height: 512px) -{ - .cineraMenus .episode_name, - .cineraMenus > .menu > .view, - .cineraMenus > .menu > .views_container .view, - .cineraMenus > .help, - .markers_container > .episodeMarker div:nth-child(2), - .markers_container > .episodeMarker div:nth-child(3), - .markers_container > .markers .marker:not(.current) { - display: none; - } +/* NOTE(matt): Mobile Style */ - .cineraMenus { - justify-content: center; - } - - .cineraMenus .menu .quotes_container, - .cineraMenus .menu .references_container, - .cineraMenus .menu .filter_container, - .cineraMenus .menu .link_container, - .cineraMenus .menu .credits_container { - position: fixed; - left: 0; - margin: auto; - max-height: 80%; - max-width: 97%; - } - - - .cineraPlayerContainer { - display: flex; - flex-direction: column; - } - - .cineraPlayerContainer .markers_container { - display: flex; - flex-flow: row; - overflow-y: auto; - } - - .cineraPlayerContainer .markers_container > .episodeMarker { - user-select: none; - -moz-user-select: none; - -webkit-user-select: none; - } - - .cineraPlayerContainer .markers_container > .markers .marker .cineraContent { - width: 456px; - } - - @media (max-width: 580px) - { - .cineraPlayerContainer .markers_container > .markers .marker .cineraContent { - width: 320px; - } - - } - @media (max-width: 450px) - { - .cineraPlayerContainer .markers_container > .markers .marker .cineraContent { - width: 256px; - } - - } - @media (max-width: 375px) - { - .cineraPlayerContainer .markers_container > .markers .marker .cineraContent { - width: 180px; - } - - } - - .cineraPlayerContainer .markers_container > .episodeMarker.first, - .cineraPlayerContainer .markers_container > .episodeMarker.prev, - .cineraPlayerContainer .markers_container > .markers .marker, - .cineraPlayerContainer .markers_container > .episodeMarker.last, - .cineraPlayerContainer .markers_container > .episodeMarker.next { - border: 0; - } - - .cineraPlayerContainer .markers_container > .episodeMarker.first, - .cineraPlayerContainer .markers_container > .episodeMarker.prev { - border-right: 3px double; - } - - .cineraPlayerContainer .markers_container > .episodeMarker.last, - .cineraPlayerContainer .markers_container > .episodeMarker.next { - border-left: 3px double; - } +#cineraIndex.mobile #cineraResults .dayContainer { + flex-direction: column; } +#cineraIndex.mobile #cineraResults .dayContainer .dayName { + font-weight: bold; + text-align: center; + max-width: 100%; +} + +#cineraIndex.mobile #cineraResults .dayContainer .markerList { + max-width: 100%; +} + +.cinera.mobile +{ + display: flex; + flex-direction: column; + flex-grow: 0; + -webkit-text-size-adjust: none; +} + +.cinera.mobile .cineraMenus { + justify-content: center; + display: flex; + flex-direction: row; + position: relative; + border: 0; +} + +.cinera.mobile .cineraMenus, +.cinera.mobile .player_container { + flex-grow: 0; +} + +.cinera.mobile .cineraMenus .episode_name, +.cinera.mobile .cineraMenus > .menu > .view, +.cinera.mobile .cineraMenus > .menu > .views_container .view, +.cinera.mobile .cineraHelp, +#cineraIndex.mobile .cineraHelp, +.cinera.mobile .markers_container > .episodeMarker div:nth-child(2), +.cinera.mobile .markers_container > .episodeMarker div:nth-child(3), +.cinera.mobile .markers_container > .markers .marker:not(.current) { + display: none; +} + +.cinera.mobile .cineraMenus .menu { + position: static; +} + +.cinera.mobile .cineraMenus .menu .quotes_container, +.cinera.mobile .cineraMenus .menu .references_container, +.cinera.mobile .cineraMenus .menu .filter_container, +.cinera.mobile .cineraMenus .menu .link_container, +.cinera.mobile .cineraMenus .menu .credits_container { + box-sizing: border-box; + position: absolute; +} + + +.cinera.mobile .cineraPlayerContainer { + display: flex; + flex-direction: column; +} + +.cinera.mobile .cineraPlayerContainer .markers_container { + display: flex; + flex-flow: row; + overflow-y: hidden; +} + +.cinera.mobile .cineraPlayerContainer .markers_container > .episodeMarker { + width: 32px; + user-select: none; + -moz-user-select: none; + -webkit-user-select: none; +} + +.cinera.mobile .cineraPlayerContainer .markers_container > .markers { + flex-grow: 1; +} + +.cinera.mobile .cineraPlayerContainer .markers_container > .episodeMarker.first, +.cinera.mobile .cineraPlayerContainer .markers_container > .episodeMarker.prev, +.cinera.mobile .cineraPlayerContainer .markers_container > .markers .marker, +.cinera.mobile .cineraPlayerContainer .markers_container > .episodeMarker.last, +.cinera.mobile .cineraPlayerContainer .markers_container > .episodeMarker.next { + border: 0; +} + +.cinera.mobile .cineraPlayerContainer .markers_container > .episodeMarker.first, +.cinera.mobile .cineraPlayerContainer .markers_container > .episodeMarker.prev { + border-right: 3px double; +} + +.cinera.mobile .cineraPlayerContainer .markers_container > .episodeMarker.last, +.cinera.mobile .cineraPlayerContainer .markers_container > .episodeMarker.next { + border-left: 3px double; +} +/* Mobile Style End */ + /* CUSTOM PAGE STYLE */ /* @@ -889,4 +1082,4 @@ Open menu: ▾ ▾ Open link in new tab: ⤻ or &10559; or &8599; or ⭷ Play from timecode: ⏵ (or, if ↓ ▸) Playable from timecode: ▹ - */ +*/ diff --git a/cinera/cinera_config.c b/cinera/cinera_config.c index c22a4d9..2aac7b6 100644 --- a/cinera/cinera_config.c +++ b/cinera/cinera_config.c @@ -516,7 +516,7 @@ typedef struct project asset *IconAsset; string StreamUsername; - string VODPlatform; + string VODPlatform; // TODO(matt): Make this an enum bool DenyBespokeTemplates; bool SingleBrowserTab; @@ -2615,7 +2615,7 @@ string DeriveLineageWithoutOriginOfProject(config *C, scope_tree *Project) { string Result = {}; - memory_book StringList = {}; + memory_book StringList = InitBookOfPointers(MBT_STRING_PTR, 4); if(Project->Parent && Project->Parent->ID.Key == IDENT_PROJECT) { string **Writer = MakeSpaceInBook(&StringList); diff --git a/cinera/cinera_player_post.js b/cinera/cinera_player_post.js index 7e3a200..d15a671 100644 --- a/cinera/cinera_player_post.js +++ b/cinera/cinera_player_post.js @@ -1,3 +1,6 @@ +var cinera = document.querySelector(".cinera"); +var baseURL = location.hash ? (location.toString().substr(0, location.toString().length - location.hash.length)) : location; + var originalTextContent = { TitleQuotes: null, TitleReferences: null, @@ -7,118 +10,16 @@ var originalTextContent = { }; var menuState = []; -var titleBar = document.querySelector(".cineraMenus"); -var quotesMenu = titleBar.querySelector(".quotes_container"); -if(quotesMenu) -{ - originalTextContent.TitleQuotes = quotesMenu.previousElementSibling.textContent; - menuState.push(quotesMenu); - var quoteItems = quotesMenu.querySelectorAll(".ref"); - if(quoteItems) - { - for(var i = 0; i < quoteItems.length; ++i) - { - quoteItems[i].addEventListener("mouseenter", function(ev) { - mouseOverQuotes(this); - }) - }; - } - var quoteTimecodes = quotesMenu.querySelectorAll(".refs .ref .ref_indices .timecode"); - for (var i = 0; i < quoteTimecodes.length; ++i) { - quoteTimecodes[i].addEventListener("click", function(ev) { - if (player) { - var time = ev.currentTarget.getAttribute("data-timestamp"); - mouseSkipToTimecode(player, time, ev); - } - }); - } - var lastFocusedQuote = null; -} - -var referencesMenu = titleBar.querySelector(".references_container"); -if(referencesMenu) -{ - originalTextContent.TitleReferences = referencesMenu.previousElementSibling.textContent; - menuState.push(referencesMenu); - var referenceItems = referencesMenu.querySelectorAll(".ref"); - if(referenceItems) - { - for(var i = 0; i < referenceItems.length; ++i) - { - referenceItems[i].addEventListener("mouseenter", function(ev) { - mouseOverReferences(this); - }) - }; - var lastFocusedReference = null; - var lastFocusedIdentifier = null; - } - - var refTimecodes = referencesMenu.querySelectorAll(".refs .ref .ref_indices .timecode"); - for (var i = 0; i < refTimecodes.length; ++i) { - refTimecodes[i].addEventListener("click", function(ev) { - if (player) { - var time = ev.currentTarget.getAttribute("data-timestamp"); - mouseSkipToTimecode(player, time, ev); - } - }); - } -} - -if(referencesMenu || quotesMenu) -{ - var refSources = titleBar.querySelectorAll(".refs .ref"); // This is for both quotes and refs - for (var i = 0; i < refSources.length; ++i) { - refSources[i].addEventListener("click", function(ev) { - if (player) { - player.pause(); - } - }); - } -} - -var filterMenu = titleBar.querySelector(".filter_container"); -if(filterMenu) -{ - menuState.push(filterMenu); - var lastFocusedCategory = null; - var lastFocusedTopic = null; - var lastFocusedMedium = null; - - var filter = filterMenu.parentNode; - - var filterModeElement = filter.querySelector(".filter_mode"); - filterModeElement.addEventListener("click", function(ev) { - toggleFilterMode(); - }); - - var filterMode = filterModeElement.classList[1]; - var filterItems = filter.querySelectorAll(".filter_content"); - - var filterInitState = new Object(); - var filterState = new Object(); - for(var i = 0; i < filterItems.length; ++i) - { - filterItems[i].addEventListener("mouseenter", function(ev) { - navigateFilter(this); - }) - - filterItems[i].addEventListener("click", function(ev) { - filterItemToggle(this); - }); - - var filterItemName = filterItems[i].classList.item(1); - if(filterItems[i].parentNode.classList.contains("filter_topics")) - { - filterInitState[filterItemName] = { "type" : "topic", "off": (filterItems[i].classList.item(2) == "off") }; - filterState[filterItemName] = { "type" : "topic", "off": (filterItems[i].classList.item(2) == "off") }; - } - else - { - filterInitState[filterItemName] = { "type" : "medium", "off": (filterItems[i].classList.item(2) == "off") }; - filterState[filterItemName] = { "type" : "medium", "off": (filterItems[i].classList.item(2) == "off") }; - } - } -} +var titleBar = cinera.querySelector(".cineraMenus"); +var quotesMenu = null; +var referencesMenu = null; +var filterMenu = null; +var viewsMenu = null; +var linkMenu = null; +var creditsMenu = null; +var sourceMenus = null; +var helpButton = null; +var helpDocumentation = null; var views = { REGULAR: 0, @@ -142,131 +43,257 @@ var cineraProps = { H: null, mH: null, P: null, - D: devices.DESKTOP, + Display: null, + FlexDirection: null, + JustifyContent: null, + O: window.orientation, + D: IsMobile() ? devices.MOBILE : devices.DESKTOP, + ScrollX: null, + ScrollY: null, }; -var viewsMenu = titleBar.querySelector(".views"); -if(viewsMenu) +if(titleBar) { - menuState.push(viewsMenu); - var viewsContainer = viewsMenu.querySelector(".views_container"); - viewsMenu.addEventListener("mouseenter", function(ev) { - handleMouseOverViewsMenu(); - }); - viewsMenu.addEventListener("mouseleave", function(ev) { - viewsContainer.style.display = "none"; - }); - - var viewItems = viewsMenu.querySelectorAll(".view"); - for(var i = 0; i < viewItems.length; ++i) + quotesMenu = titleBar.querySelector(".quotes_container"); + if(quotesMenu) { - viewItems[i].addEventListener("click", function(ev) { - switch(this.getAttribute("data-id")) + originalTextContent.TitleQuotes = quotesMenu.previousElementSibling.textContent; + menuState.push(quotesMenu); + var quoteItems = quotesMenu.querySelectorAll(".ref"); + if(quoteItems) + { + for(var i = 0; i < quoteItems.length; ++i) { - case "regular": - case "theatre": - { - toggleTheatreMode(); - } break; - case "super": - { - toggleSuperTheatreMode(); - } break; + quoteItems[i].addEventListener("mouseenter", function(ev) { + mouseOverQuotes(this); + }) + }; + } + var quoteTimecodes = quotesMenu.querySelectorAll(".refs .ref .ref_indices .timecode"); + for (var i = 0; i < quoteTimecodes.length; ++i) { + quoteTimecodes[i].addEventListener("click", function(ev) { + if (player) { + var time = ev.currentTarget.getAttribute("data-timestamp"); + mouseSkipToTimecode(player, time, ev); + } + }); + } + var lastFocusedQuote = null; + } + + referencesMenu = titleBar.querySelector(".references_container"); + if(referencesMenu) + { + originalTextContent.TitleReferences = referencesMenu.previousElementSibling.textContent; + menuState.push(referencesMenu); + var referenceItems = referencesMenu.querySelectorAll(".ref"); + if(referenceItems) + { + for(var i = 0; i < referenceItems.length; ++i) + { + referenceItems[i].addEventListener("mouseenter", function(ev) { + mouseOverReferences(this); + }) + }; + var lastFocusedReference = null; + var lastFocusedIdentifier = null; + } + + var refTimecodes = referencesMenu.querySelectorAll(".refs .ref .ref_indices .timecode"); + for (var i = 0; i < refTimecodes.length; ++i) { + refTimecodes[i].addEventListener("click", function(ev) { + if (player) { + var time = ev.currentTarget.getAttribute("data-timestamp"); + mouseSkipToTimecode(player, time, ev); + } + }); + } + } + + if(referencesMenu || quotesMenu) + { + var refSources = titleBar.querySelectorAll(".refs .ref"); // This is for both quotes and refs + for (var i = 0; i < refSources.length; ++i) { + refSources[i].addEventListener("click", function(ev) { + if (player) { + player.pause(); + } + }); + } + } + + filterMenu = titleBar.querySelector(".filter_container"); + if(filterMenu) + { + menuState.push(filterMenu); + var lastFocusedCategory = null; + var lastFocusedTopic = null; + var lastFocusedMedium = null; + + var filter = filterMenu.parentNode; + + var filterModeElement = filter.querySelector(".filter_mode"); + filterModeElement.addEventListener("click", function(ev) { + ev.stopPropagation(); + toggleFilterMode(); + }); + + var filterMode = filterModeElement.classList[1]; + var filterItems = filter.querySelectorAll(".filter_content"); + + var filterInitState = new Object(); + var filterState = new Object(); + for(var i = 0; i < filterItems.length; ++i) + { + filterItems[i].addEventListener("mouseenter", function(ev) { + navigateFilter(this); + }) + + filterItems[i].addEventListener("click", function(ev) { + ev.stopPropagation(); + filterItemToggle(this); + }); + + var filterItemName = filterItems[i].classList.item(1); + if(filterItems[i].parentNode.classList.contains("filter_topics")) + { + filterInitState[filterItemName] = { "type" : "topic", "off": (filterItems[i].classList.item(2) == "off") }; + filterState[filterItemName] = { "type" : "topic", "off": (filterItems[i].classList.item(2) == "off") }; } + else + { + filterInitState[filterItemName] = { "type" : "medium", "off": (filterItems[i].classList.item(2) == "off") }; + filterState[filterItemName] = { "type" : "medium", "off": (filterItems[i].classList.item(2) == "off") }; + } + } + } + + viewsMenu = titleBar.querySelector(".views"); + if(viewsMenu && cineraProps.D !== devices.MOBILE) + { + menuState.push(viewsMenu); + var viewsContainer = viewsMenu.querySelector(".views_container"); + viewsMenu.addEventListener("mouseenter", function(ev) { + handleMouseOverViewsMenu(); + }); + viewsMenu.addEventListener("mouseleave", function(ev) { + viewsContainer.style.display = "none"; + }); + + var viewItems = viewsMenu.querySelectorAll(".view"); + for(var i = 0; i < viewItems.length; ++i) + { + viewItems[i].addEventListener("click", function(ev) { + switch(this.getAttribute("data-id")) + { + case "regular": + case "theatre": + { + toggleTheatreMode(); + } break; + case "super": + { + toggleSuperTheatreMode(); + } break; + } + }); + } + } + + linkMenu = titleBar.querySelector(".link_container"); + linkAnnotation = true; + if(linkMenu) + { + menuState.push(linkMenu); + + var linkMode = linkMenu.querySelector("#cineraLinkMode"); + var link = linkMenu.querySelector("#cineraLink"); + + linkMode.addEventListener("click", function(ev) { + ev.stopPropagation(); + toggleLinkMode(linkMode, link); + }); + + link.addEventListener("click", function(ev) { + CopyToClipboard(link); + toggleMenuVisibility(linkMenu); }); } -} -var baseURL = location.hash ? (location.toString().substr(0, location.toString().length - location.hash.length)) : location; -var linkMenu = titleBar.querySelector(".link_container"); -linkAnnotation = true; -if(linkMenu) -{ - menuState.push(linkMenu); - - var linkMode = linkMenu.querySelector("#cineraLinkMode"); - var link = linkMenu.querySelector("#cineraLink"); - - linkMode.addEventListener("click", function(ev) { - toggleLinkMode(linkMode, link); - }); - - link.addEventListener("click", function(ev) { - CopyToClipboard(link); - toggleMenuVisibility(linkMenu); - }); -} - -var creditsMenu = titleBar.querySelector(".credits_container"); -if(creditsMenu) -{ - originalTextContent.TitleCredits = creditsMenu.previousElementSibling.textContent; - menuState.push(creditsMenu); - var lastFocusedCreditItem = null; - - var creditItems = creditsMenu.querySelectorAll(".person, .support"); - for(var i = 0; i < creditItems.length; ++i) + creditsMenu = titleBar.querySelector(".credits_container"); + if(creditsMenu) { - creditItems[i].addEventListener("mouseenter", function(ev) { - if(this != lastFocusedCreditItem) - { - lastFocusedCreditItem.classList.remove("focused"); - unfocusSprite(lastFocusedCreditItem); - if(lastFocusedCreditItem.classList.contains("support")) + originalTextContent.TitleCredits = creditsMenu.previousElementSibling.textContent; + menuState.push(creditsMenu); + var lastFocusedCreditItem = null; + + var creditItems = creditsMenu.querySelectorAll(".person, .support"); + for(var i = 0; i < creditItems.length; ++i) + { + creditItems[i].addEventListener("mouseenter", function(ev) { + if(this != lastFocusedCreditItem) { - setSpriteLightness(lastFocusedCreditItem.firstChild); + lastFocusedCreditItem.classList.remove("focused"); + unfocusSprite(lastFocusedCreditItem); + if(lastFocusedCreditItem.classList.contains("support")) + { + setSpriteLightness(lastFocusedCreditItem.firstChild); + } + lastFocusedCreditItem = this; + focusedElement = lastFocusedCreditItem; + focusedElement.classList.add("focused"); + focusSprite(focusedElement); + if(focusedElement.classList.contains("support")) + { + setSpriteLightness(focusedElement.firstChild); + } } - lastFocusedCreditItem = this; - focusedElement = lastFocusedCreditItem; - focusedElement.classList.add("focused"); - focusSprite(focusedElement); - if(focusedElement.classList.contains("support")) - { - setSpriteLightness(focusedElement.firstChild); - } - } - }) + }) + } } + + sourceMenus = titleBar.querySelectorAll(".menu"); + + helpButton = titleBar.querySelector(".cineraHelp"); + helpDocumentation = helpButton.querySelector(".help_container"); + BindHelp(helpButton, helpDocumentation); } -var sourceMenus = titleBar.querySelectorAll(".menu"); - -var helpButton = titleBar.querySelector(".help"); -window.addEventListener("blur", function(){ - helpButton.firstElementChild.innerText = "¿"; - helpButton.firstElementChild.title = "Keypresses will not pass through to Cinera because focus is currently elsewhere.\n\nTo regain focus, please press Tab / Shift-Tab (multiple times) or click somewhere related to Cinera other than the video, e.g. this button"; -}); - -window.addEventListener("focus", function(){ - helpButton.firstElementChild.innerText = "?"; - helpButton.firstElementChild.title = "" -}); - -var helpDocumentation = helpButton.querySelector(".help_container"); -helpButton.addEventListener("click", function(ev) { - handleMouseOverMenu(this, ev.type); -}) - var focusedElement = null; var focusedIdentifier = null; -var playerContainer = document.querySelector(".cineraPlayerContainer") +var playerContainer = cinera.querySelector(".cineraPlayerContainer") var prevEpisode = playerContainer.querySelector(".episodeMarker.prev"); if(prevEpisode) { originalTextContent.EpisodePrev = prevEpisode.firstChild.textContent; } var nextEpisode = playerContainer.querySelector(".episodeMarker.next"); if(nextEpisode) { originalTextContent.EpisodeNext = nextEpisode.firstChild.textContent; } var testMarkers = playerContainer.querySelectorAll(".marker"); -var cinera = playerContainer.parentNode; // NOTE(matt): All the originalTextContent values must be set by this point, because the player's construction may need them +var MobileCineraContentRuleSelector = ".cinera.mobile .cineraPlayerContainer .markers_container > .markers .marker .cineraContent"; +var MobileCineraContentRule = GetOrSetRule(MobileCineraContentRuleSelector); + +if(cineraProps.D == devices.MOBILE) +{ + InitMobileStyle(); +} + var player = new Player(playerContainer, onRefChanged); +if(cineraProps.D == devices.MOBILE) +{ + ConnectMobileControls(player); +} + var cineraViewStorageItem = "cineraView"; + if(viewsMenu && localStorage.getItem(cineraViewStorageItem)) { toggleTheatreMode(); } +InitScrollEventListener(cinera); + window.addEventListener("resize", function() { player.updateSize(); }); document.addEventListener("keydown", function(ev) { var key = ev.key; @@ -284,10 +311,13 @@ document.addEventListener("keydown", function(ev) { for(var i = 0; i < sourceMenus.length; ++i) { sourceMenus[i].addEventListener("mouseenter", function(ev) { - handleMouseOverMenu(this, ev.type); + handleMenuTogglerInteraction(this, ev.type); }) sourceMenus[i].addEventListener("mouseleave", function(ev) { - handleMouseOverMenu(this, ev.type); + handleMenuTogglerInteraction(this, ev.type); + }) + sourceMenus[i].addEventListener("click", function(ev) { + handleMenuTogglerInteraction(this, ev.type); }) }; @@ -297,7 +327,7 @@ for(i = 0; i < colouredItems.length; ++i) setTextLightness(colouredItems[i]); } -var topicDots = document.querySelectorAll(".category"); +var topicDots = cinera.querySelectorAll(".category"); for(var i = 0; i < topicDots.length; ++i) { setDotLightness(topicDots[i]); @@ -313,494 +343,8 @@ else if(lastAnnotation = localStorage.getItem(lastAnnotationStorageItem)) player.setTime(lastAnnotation); } -function handleKey(key) { - var gotKey = true; - switch (key) { - case "q": { - if(quotesMenu) - { - toggleMenuVisibility(quotesMenu) - } - } break; - case "r": { - if(referencesMenu) - { - toggleMenuVisibility(referencesMenu) - } - } break; - case "f": { - if(filterMenu) - { - toggleMenuVisibility(filterMenu) - } - } break; - case "y": { - if(linkMenu) - { - toggleMenuVisibility(linkMenu) - } - break; - } - case "c": { - if(creditsMenu) - { - toggleMenuVisibility(creditsMenu) - } - } break; - case "t": { - if(cinera) - { - toggleTheatreMode(); - } - } break; - case "T": { - if(cinera) - { - toggleSuperTheatreMode(); - } - } break; - - case "Enter": { - if(focusedElement) - { - if(focusedElement.parentNode.classList.contains("quotes_container")) - { - var time = focusedElement.querySelector(".timecode").getAttribute("data-timestamp"); - player.setTime(parseInt(time, 10)); - player.play(); - } - else if(focusedElement.parentNode.classList.contains("references_container")) - { - var time = focusedIdentifier.getAttribute("data-timestamp"); - player.setTime(parseInt(time, 10)); - player.play(); - } - else if(focusedElement.parentNode.classList.contains("credit")) - { - if(focusedElement.hasAttribute) - { - var url = focusedElement.getAttribute("href"); - window.open(url, "_blank"); - } - } - } - else - { - console.log("TODO(matt): Implement me, perhaps?\n"); - } - } break; - - case "o": { - if(focusedElement) - { - if(focusedElement.parentNode.classList.contains("references_container") || - focusedElement.parentNode.classList.contains("quotes_container")) - { - var url = focusedElement.getAttribute("href"); - window.open(url, "_blank"); - } - else if(focusedElement.parentNode.classList.contains("credit")) - { - if(focusedElement.hasAttribute("href")) - { - var url = focusedElement.getAttribute("href"); - window.open(url, "_blank"); - } - } - } - } break; - - case "w": case "k": case "ArrowUp": { - if(focusedElement) - { - if(focusedElement.parentNode.classList.contains("quotes_container")) - { - if(focusedElement.previousElementSibling) - { - focusedElement.classList.remove("focused"); - unfocusSprite(focusedElement); - - lastFocusedQuote = focusedElement.previousElementSibling; - focusedElement = lastFocusedQuote; - focusedElement.classList.add("focused"); - focusSprite(focusedElement); - } - } - else if(focusedElement.parentNode.classList.contains("references_container")) - { - if(focusedElement.previousElementSibling) - { - focusedElement.classList.remove("focused"); - unfocusSprite(focusedElement); - focusedIdentifier.classList.remove("focused"); - unfocusSprite(focusedIdentifier); - - lastFocusedReference = focusedElement.previousElementSibling; - focusedElement = lastFocusedReference; - focusedElement.classList.add("focused"); - focusSprite(focusedElement); - - lastFocusedIdentifier = focusedElement.querySelector(".ref_indices").firstElementChild; - focusedIdentifier = lastFocusedIdentifier; - focusedIdentifier.classList.add("focused"); - focusSprite(focusedIdentifier); - } - } - else if(focusedElement.parentNode.parentNode.classList.contains("filters")) - { - if(focusedElement.previousElementSibling && - focusedElement.previousElementSibling.classList.contains("filter_content")) - { - focusedElement.classList.remove("focused"); - unfocusSprite(focusedElement); - - lastFocusedCategory = focusedElement.previousElementSibling; - focusedElement = lastFocusedCategory; - focusedElement.classList.add("focused"); - focusSprite(focusedElement); - } - } - else if(focusedElement.parentNode.classList.contains("credit")) - { - if(focusedElement.parentNode.previousElementSibling) - { - focusedElement.classList.remove("focused"); - unfocusSprite(focusedElement); - if(focusedElement.parentNode.previousElementSibling.querySelector(".support") && - focusedElement.classList.contains("support")) - { - setSpriteLightness(focusedElement.firstChild); - lastFocusedCreditItem = focusedElement.parentNode.previousElementSibling.querySelector(".support"); - focusedElement = lastFocusedCreditItem; - focusedElement.classList.add("focused"); - focusSprite(focusedElement); - } - else - { - lastFocusedCreditItem = focusedElement.parentNode.previousElementSibling.querySelector(".person"); - focusedElement = lastFocusedCreditItem; - focusedElement.classList.add("focused"); - focusSprite(focusedElement); - } - } - } - } - } break; - - case "s": case "j": case "ArrowDown": { - if(focusedElement) - { - if(focusedElement.parentNode.classList.contains("quotes_container")) - { - if(focusedElement.nextElementSibling) - { - focusedElement.classList.remove("focused"); - unfocusSprite(focusedElement); - - lastFocusedQuote = focusedElement.nextElementSibling; - focusedElement = lastFocusedQuote; - focusedElement.classList.add("focused"); - focusSprite(focusedElement); - } - } - else if(focusedElement.parentNode.classList.contains("references_container")) - { - if(focusedElement.nextElementSibling) - { - focusedElement.classList.remove("focused"); - unfocusSprite(focusedElement); - focusedIdentifier.classList.remove("focused"); - unfocusSprite(focusedIdentifier); - - lastFocusedReference = focusedElement.nextElementSibling; - focusedElement = lastFocusedReference; - focusedElement.classList.add("focused"); - focusSprite(focusedElement); - - lastFocusedIdentifier = focusedElement.querySelector(".ref_indices").firstElementChild; - focusedIdentifier = lastFocusedIdentifier; - focusedIdentifier.classList.add("focused"); - focusSprite(focusedIdentifier); - } - } - else if(focusedElement.parentNode.parentNode.classList.contains("filters")) - { - if(focusedElement.nextElementSibling && - focusedElement.nextElementSibling.classList.contains("filter_content")) - { - focusedElement.classList.remove("focused"); - unfocusSprite(focusedElement); - - lastFocusedCategory = focusedElement.nextElementSibling; - focusedElement = lastFocusedCategory; - focusedElement.classList.add("focused"); - focusSprite(focusedElement); - } - } - else if(focusedElement.parentNode.classList.contains("credit")) - { - if(focusedElement.parentNode.nextElementSibling) - { - focusedElement.classList.remove("focused"); - unfocusSprite(focusedElement); - if(focusedElement.parentNode.nextElementSibling.querySelector(".support") && - focusedElement.classList.contains("support")) - { - setSpriteLightness(focusedElement.firstChild); - lastFocusedCreditItem = focusedElement.parentNode.nextElementSibling.querySelector(".support"); - focusedElement = lastFocusedCreditItem; - focusedElement.classList.add("focused"); - focusSprite(focusedElement); - } - else - { - lastFocusedCreditItem = focusedElement.parentNode.nextElementSibling.querySelector(".person"); - focusedElement = lastFocusedCreditItem; - focusedElement.classList.add("focused"); - focusSprite(focusedElement); - } - } - } - } - } break; - - case "a": case "h": case "ArrowLeft": { - if(focusedElement) - { - if(focusedElement.parentNode.classList.contains("references_container")) - { - if(focusedIdentifier.previousElementSibling) - { - focusedIdentifier.classList.remove("focused"); - unfocusSprite(focusedIdentifier); - lastFocusedIdentifier = focusedIdentifier.previousElementSibling; - focusedIdentifier = lastFocusedIdentifier; - focusedIdentifier.classList.add("focused"); - focusSprite(focusedIdentifier); - } - else if(focusedIdentifier.parentNode.previousElementSibling.classList.contains("ref_indices")) - { - focusedIdentifier.classList.remove("focused"); - unfocusSprite(focusedIdentifier); - lastFocusedIdentifier = focusedIdentifier.parentNode.previousElementSibling.lastElementChild; - focusedIdentifier = lastFocusedIdentifier; - focusedIdentifier.classList.add("focused"); - focusSprite(focusedIdentifier); - } - } - else if(focusedElement.classList.contains("filter_content")) - { - if(focusedElement.parentNode.classList.contains("filter_media") && - focusedElement.parentNode.previousElementSibling) - { - focusedElement.classList.remove("focused"); - unfocusSprite(focusedElement); - lastFocusedMedium = focusedElement; - - if(!lastFocusedTopic) - { - lastFocusedTopic = focusedElement.parentNode.previousElementSibling.children[1]; - } - lastFocusedCategory = lastFocusedTopic; - focusedElement = lastFocusedCategory; - focusedElement.classList.add("focused"); - focusSprite(focusedElement); - } - } - else if(focusedElement.parentNode.classList.contains("credit")) - { - if(focusedElement.classList.contains("support")) - { - focusedElement.classList.remove("focused"); - console.log(focusedElement); - unfocusSprite(focusedElement); - - lastFocusedCreditItem = focusedElement.previousElementSibling; - setSpriteLightness(focusedElement.firstChild); - focusedElement = lastFocusedCreditItem; - focusedElement.classList.add("focused"); - focusSprite(focusedElement); - } - } - } - } break; - - case "d": case "l": case "ArrowRight": { - if(focusedElement) - { - if(focusedElement.parentNode.classList.contains("references_container")) - { - if(focusedIdentifier.nextElementSibling) - { - focusedIdentifier.classList.remove("focused"); - unfocusSprite(focusedIdentifier); - - lastFocusedIdentifier = focusedIdentifier.nextElementSibling; - focusedIdentifier = lastFocusedIdentifier; - focusedIdentifier.classList.add("focused"); - focusSprite(focusedIdentifier); - } - else if(focusedIdentifier.parentNode.nextElementSibling) - { - focusedIdentifier.classList.remove("focused"); - unfocusSprite(focusedIdentifier); - lastFocusedIdentifier = focusedIdentifier.parentNode.nextElementSibling.firstElementChild; - focusedIdentifier = lastFocusedIdentifier; - focusedIdentifier.classList.add("focused"); - focusSprite(focusedIdentifier); - } - } - else if(focusedElement.classList.contains("filter_content")) - { - if(focusedElement.parentNode.classList.contains("filter_topics") && - focusedElement.parentNode.nextElementSibling) - { - focusedElement.classList.remove("focused"); - unfocusSprite(focusedElement); - lastFocusedTopic = focusedElement; - - if(!lastFocusedMedium) - { - lastFocusedMedium = focusedElement.parentNode.nextElementSibling.children[1]; - } - lastFocusedCategory = lastFocusedMedium; - focusedElement = lastFocusedCategory; - focusedElement.classList.add("focused"); - focusSprite(focusedElement); - } - } - else if(focusedElement.parentNode.classList.contains("credit")) - { - if(focusedElement.classList.contains("person") && - focusedElement.nextElementSibling) - { - focusedElement.classList.remove("focused"); - unfocusSprite(focusedElement); - - lastFocusedCreditItem = focusedElement.nextElementSibling; - focusedElement = lastFocusedCreditItem; - focusedElement.classList.add("focused"); - focusSprite(focusedElement); - } - } - } - } break; - - case "x": case " ": { - if(focusedElement && focusedElement.classList.contains("filter_content")) - { - filterItemToggle(focusedElement); - if(focusedElement.nextElementSibling && - focusedElement.nextElementSibling.classList.contains("filter_content")) - { - focusedElement.classList.remove("focused"); - unfocusSprite(focusedElement); - if(focusedElement.parentNode.classList.contains("filter_topics")) - { - lastFocusedTopic = focusedElement.nextElementSibling; - lastFocusedCategory = lastFocusedTopic; - } - else - { - lastFocusedMedium = focusedElement.nextElementSibling; - lastFocusedCategory = lastFocusedMedium; - } - lastFocusedElement = lastFocusedCategory; - focusedElement = lastFocusedElement; - focusedElement.classList.add("focused"); - focusSprite(focusedElement); - } - } - } break; - - case "X": case "capitalSpace": { - if(focusedElement && focusedElement.classList.contains("filter_content")) - { - filterItemToggle(focusedElement); - if(focusedElement.previousElementSibling && - focusedElement.previousElementSibling.classList.contains("filter_content")) - { - focusedElement.classList.remove("focused"); - unfocusSprite(focusedElement); - if(focusedElement.parentNode.classList.contains("filter_topics")) - { - lastFocusedTopic = focusedElement.previousElementSibling; - lastFocusedCategory = lastFocusedTopic; - } - else - { - lastFocusedMedium = focusedElement.previousElementSibling; - lastFocusedCategory = lastFocusedMedium; - } - lastFocusedElement = lastFocusedCategory; - focusedElement = lastFocusedElement; - focusedElement.classList.add("focused"); - focusSprite(focusedElement); - } - } - } break; - - case "z": { - toggleFilterOrLinkMode(); - } break; - - case "v": { - if(focusedElement && focusedElement.classList.contains("filter_content")) - { - invertFilter(focusedElement) - } - } break; - - case "V": { - resetFilter(); - } break; - - case "?": { - helpDocumentation.classList.toggle("visible"); - } break; - - case 'N': - case 'J': - case 'S': { - player.jumpToNextMarker(); - } break; - - case 'P': - case 'K': - case 'W': { - player.jumpToPrevMarker(); - } break; - case '[': - case '<': { - if(prevEpisode) - { - location = prevEpisode.href; - } - } break; - case ']': - case '>': { - if(nextEpisode) - { - location = nextEpisode.href; - } - } break; - case 'Y': { - if(cineraLink) - { - if(linkAnnotation == false && player.playing) - { - player.pause(); - } - if(linkMenu && !linkMenu.classList.contains("visible")) - { - toggleMenuVisibility(linkMenu); - } - SelectText(cineraLink); - } - } - default: { - gotKey = false; - } break; - } - return gotKey; -} +window.onorientationchange = function() +{ + cineraProps.O = window.orientation; + player.updateSize(); +}; diff --git a/cinera/cinera_player_pre.js b/cinera/cinera_player_pre.js index 48a6d42..795a8c4 100644 --- a/cinera/cinera_player_pre.js +++ b/cinera/cinera_player_pre.js @@ -54,7 +54,8 @@ function Player(htmlContainer, refsCallback) { }.bind(this)); Player.initializeYoutube(this.onYoutubeReady.bind(this)); - this.updateSize(); + var PendingMobileStyleInitialisation = true; + this.updateSize(PendingMobileStyleInitialisation); this.resume(); } @@ -99,6 +100,11 @@ Player.prototype.setTime = function(time) { Player.prototype.jumpToNextMarker = function() { var targetMarkerIdx = Math.min((this.currentMarkerIdx === null ? 0 : this.currentMarkerIdx + 1), this.markers.length-1); var targetTime = this.markers[targetMarkerIdx].timestamp; + while(targetMarkerIdx < this.markers.length && this.markers[targetMarkerIdx].el.classList.contains("skip")) + { + ++targetMarkerIdx; + targetTime = this.markers[targetMarkerIdx].timestamp; + } this.setTime(targetTime); this.play(); }; @@ -106,35 +112,310 @@ Player.prototype.jumpToNextMarker = function() { Player.prototype.jumpToPrevMarker = function() { var targetMarkerIdx = Math.max(0, (this.currentMarkerIdx === null ? 0 : this.currentMarkerIdx - 1)); var targetTime = this.markers[targetMarkerIdx].timestamp; + while(targetMarkerIdx >= 0 && this.markers[targetMarkerIdx].el.classList.contains("skip")) + { + --targetMarkerIdx; + targetTime = this.markers[targetMarkerIdx].timestamp; + } this.setTime(targetTime); this.play(); }; -function switchToMobileView(player) +function +GetHeightOfHideableElement(Element, UnhidingClass) +{ + var Result = 0; + if(Element.classList.contains(UnhidingClass)) + { + Result = Element.offsetHeight; + } + else + { + var ZOffset = Element.style.zOffset; + Element.style.zOffset = -1; + Element.classList.add(UnhidingClass); + + Result = Element.offsetHeight; + + Element.classList.remove(UnhidingClass); + Element.style.zOffset = ZOffset; + } + return Result; +} + +function +GetWidthOfHideableElement(Element, UnhidingClass) +{ + var Result = 0; + if(Element.classList.contains(UnhidingClass)) + { + Result = Element.offsetWidth; + } + else + { + var ZOffset = Element.style.zOffset; + Element.style.zOffset = -1; + Element.classList.add(UnhidingClass); + + Result = Element.offsetWidth; + + Element.classList.remove(UnhidingClass); + Element.style.zOffset = ZOffset; + } + return Result; +} + +function +ComputeVerticalOffsetForMenu(Menu, Toggler, VideoContainerDimY) +{ + console.log("ComputeVerticalOffsetForMenu()"); + var Result = 0; + var MenuHeight = GetHeightOfHideableElement(Menu, "visible"); + var TogglerHeight = Toggler.offsetHeight; + + var TogglerOffset = Toggler.offsetTop; + var Result = TogglerOffset + (TogglerHeight / 2) - (MenuHeight / 2); + + console.log("Lower Point (0): " + (Result + MenuHeight)); + console.log("VideoContainerDimY (0): " + VideoContainerDimY); + var LowerProtrusion = MenuHeight + Result - VideoContainerDimY; + if(LowerProtrusion > 0) + { + Result -= LowerProtrusion; + } + console.log("Lower Point (1): " + (Result + MenuHeight)); + console.log("VideoContainerDimY (1): " + VideoContainerDimY); + + if(Result < 0) + { + Result = 0; + } + return Result; +} + +function sizeAndPositionMenuContainer(TitleBar, SizerElement, Menu) +{ + if(Menu) + { + var Toggler = Menu.parentElement; + + var TitleBarDimX = TitleBar.offsetWidth; + var TitleBarDimY = TitleBar.offsetHeight; + var SizerElementDimX = SizerElement.offsetWidth; + var SizerElementDimY = SizerElement.offsetHeight; + + var ContainerDimX = SizerElementDimX; + var ContainerDimY = SizerElementDimY; + + switch(cineraProps.O) + { + case orientations.PORTRAIT: + { + Menu.style.borderTopWidth = 0; + Menu.style.borderTopStyle = "none"; + Menu.style.borderRightWidth = "1px"; + Menu.style.borderRightStyle = "solid"; + Menu.style.borderLeftWidth = "1px"; + Menu.style.borderLeftStyle = "solid"; + + Menu.style.maxWidth = ContainerDimX + "px"; + ContainerDimY -= TitleBarDimY; + Menu.style.maxHeight = ContainerDimY + "px"; + + Menu.style.top = TitleBarDimY + "px"; + Menu.style.left = 0 + "px"; + } break; + case orientations.LANDSCAPE_LEFT: + { + Menu.style.borderTopWidth = "1px"; + Menu.style.borderTopStyle = "solid"; + Menu.style.borderRightWidth = "1px"; + Menu.style.borderRightStyle = "solid"; + Menu.style.borderLeftWidth = 0; + Menu.style.borderLeftStyle = "none"; + + ContainerDimX -= TitleBarDimX; + Menu.style.maxWidth = ContainerDimX + "px"; + Menu.style.maxHeight = ContainerDimY + "px"; + + var MenuVerticalOffset = ComputeVerticalOffsetForMenu(Menu, Toggler, SizerElementDimY); + + Menu.style.top = MenuVerticalOffset + "px"; + Menu.style.left = TitleBarDimX + "px"; + } break; + case orientations.LANDSCAPE_RIGHT: + { + Menu.style.borderTopWidth = "1px"; + Menu.style.borderTopStyle = "solid"; + Menu.style.borderRightWidth = 0; + Menu.style.borderRightStyle = "none"; + Menu.style.borderLeftWidth = "1px"; + Menu.style.borderLeftStyle = "solid"; + + ContainerDimX -= TitleBarDimX; + Menu.style.maxWidth = ContainerDimX + "px"; + Menu.style.maxHeight = ContainerDimY + "px"; + + var MenuVerticalOffset = ComputeVerticalOffsetForMenu(Menu, Toggler, SizerElementDimY); + + Menu.style.top = MenuVerticalOffset + "px"; + var MenuWidth = GetWidthOfHideableElement(Menu, "visible"); + Menu.style.left = -MenuWidth + "px"; + } break; + + } + } +} + +function +ComputeTallest(Elements, UnhidingClass) +{ + var Result = null; + for(var i = 0; i < Elements.length; ++i) + { + var This = Elements[i]; + var Height = UnhidingClass ? GetHeightOfHideableElement(This, UnhidingClass) : This.offsetHeight; + if(Height > Result) + { + Result = Height; + } + } + return Result; +} + +function +ComputeAndSetTallest(Selector, Elements, UnhidingClass) +{ + Selector.style.height = "unset"; + Selector.style.height = ComputeTallest(Elements, UnhidingClass) + "px"; +} + +function ApplyMobileStyle(player) +{ + var MaxWidth = MaxWidthOfElement(cinera); + var MaxHeight = MaxHeightOfElement(cinera); + + var IndicesBar = playerContainer.querySelector(".markers_container"); + var Markers = IndicesBar.querySelector(".markers"); + var CineraContentWidth = MaxWidth; + + var EpisodeMarkers = IndicesBar.querySelectorAll(".episodeMarker"); + for(var i = 0; i < EpisodeMarkers.length; ++i) + { + CineraContentWidth -= EpisodeMarkers[i].offsetWidth; + } + + switch(cineraProps.O) + { + case orientations.PORTRAIT: + { + cinera.style.flexDirection = "column"; + titleBar.style.flexDirection = "row"; + } break; + case orientations.LANDSCAPE_LEFT: + { + cinera.style.flexDirection = "row"; + titleBar.style.flexDirection = "column-reverse"; + CineraContentWidth -= titleBar.offsetWidth; + } break; + case orientations.LANDSCAPE_RIGHT: + { + cinera.style.flexDirection = "row-reverse"; + titleBar.style.flexDirection = "column"; + CineraContentWidth -= titleBar.offsetWidth; + } break; + } + + if(MobileCineraContentRule !== undefined) + { + MobileCineraContentRule.style.width = CineraContentWidth + "px"; + var MarkerList = Markers.querySelectorAll(".marker"); + ComputeAndSetTallest(MobileCineraContentRule, MarkerList, "current"); + } + + var VideoMaxDimX = null; + var VideoMaxDimY = null; + + switch(cineraProps.O) + { + case orientations.PORTRAIT: + { + VideoMaxDimX = MaxWidth; + VideoMaxDimY = MaxHeight - IndicesBar.offsetHeight - titleBar.offsetHeight; + } break; + case orientations.LANDSCAPE_LEFT: + case orientations.LANDSCAPE_RIGHT: + { + VideoMaxDimX = MaxWidth - titleBar.offsetWidth; + VideoMaxDimY = MaxHeight - IndicesBar.offsetHeight; + } break; + } + + var VideoDimYFromMaxX = VideoMaxDimX * 9 / 16; + var VideoDimXFromMaxY = VideoMaxDimY * 16 / 9; + + var VideoDimX = 0; + var VideoDimY = 0; + if(VideoDimXFromMaxY > VideoMaxDimX) + { + VideoDimX = Math.floor(VideoMaxDimX); + VideoDimY = Math.floor(VideoDimYFromMaxX); + } + else if(VideoDimYFromMaxX > VideoMaxDimY) + { + VideoDimY = Math.floor(VideoMaxDimY); + VideoDimX = Math.floor(VideoDimXFromMaxY); + } + else + { + VideoDimX = Math.floor(VideoMaxDimX); + VideoDimY = Math.floor(VideoDimYFromMaxX); + } + + var VideoContainer = cinera.querySelector(".video_container"); + + VideoContainer.style.width = VideoDimX + "px"; + VideoContainer.style.height = VideoDimY + "px"; + + sizeAndPositionMenuContainer(titleBar, cinera, quotesMenu); + sizeAndPositionMenuContainer(titleBar, cinera, referencesMenu); + sizeAndPositionMenuContainer(titleBar, cinera, filterMenu); + sizeAndPositionMenuContainer(titleBar, cinera, linkMenu); + sizeAndPositionMenuContainer(titleBar, cinera, creditsMenu); +} + +function +IconifyMenuTogglers() { - var menuContainerOffset = getElementYOffsetFromPage(titleBar) + parseInt(window.getComputedStyle(titleBar).height); if(quotesMenu) { quotesMenu.previousElementSibling.textContent = '\u{1F5E9}'; - quotesMenu.style.top = menuContainerOffset + "px"; } + if(referencesMenu) { referencesMenu.previousElementSibling.textContent = '\u{1F4D6}'; - referencesMenu.style.top = menuContainerOffset + "px"; } - if(filterMenu) { filterMenu.style.top = menuContainerOffset + "px"; } - if(linkMenu) { linkMenu.style.top = menuContainerOffset + "px"; } - - if(creditsMenu) { + if(creditsMenu) + { creditsMenu.previousElementSibling.textContent = '\u{1F46A}'; - creditsMenu.style.top = menuContainerOffset + "px"; } + if(viewsMenu) + { + viewsMenu.remove(); + viewsMenu = null; + } +} + +function +InitMobileControls() +{ var rightmost = {}; - var markersContainer = player.markersContainer; + var markersContainer = cinera.querySelector(".markers_container"); markersContainer.style.height = "auto"; + var episodeMarkerFirst = markersContainer.querySelector(".episodeMarker.first"); var episodeMarkerPrev = markersContainer.querySelector(".episodeMarker.prev"); var episodeMarkerNext = markersContainer.querySelector(".episodeMarker.next"); var episodeMarkerLast = markersContainer.querySelector(".episodeMarker.last"); @@ -143,94 +424,70 @@ function switchToMobileView(player) if(episodeMarkerNext) { episodeMarkerNext.firstChild.textContent = '\u{23ED}'; rightmost = episodeMarkerNext; } else if (episodeMarkerLast) { rightmost = episodeMarkerLast; } + var controlPrevTimestamp = document.createElement("a"); + controlPrevTimestamp.classList.add("episodeMarker"); + controlPrevTimestamp.classList.add("prevTimestamp"); + var controlPrevTimestampContent = document.createElement("div"); + controlPrevTimestampContent.appendChild(document.createTextNode('\u{25C0}')); + controlPrevTimestamp.appendChild(controlPrevTimestampContent); + var markers = markersContainer.querySelector(".markers"); + markersContainer.insertBefore(controlPrevTimestamp, markers); - var controlPrevAnnotation = document.createElement("a"); - controlPrevAnnotation.classList.add("episodeMarker"); - controlPrevAnnotation.classList.add("prevAnnotation"); - controlPrevAnnotation.addEventListener("click", function(ev) { - player.jumpToPrevMarker(); - }); - var controlPrevAnnotationContent = document.createElement("div"); - controlPrevAnnotationContent.appendChild(document.createTextNode('\u{23F4}')); - controlPrevAnnotation.appendChild(controlPrevAnnotationContent); - - markersContainer.insertBefore(controlPrevAnnotation, markers); - - var controlNextAnnotation = document.createElement("a"); - controlNextAnnotation.classList.add("episodeMarker"); - controlNextAnnotation.classList.add("nextAnnotation"); - controlNextAnnotation.addEventListener("click", function(ev) { - player.jumpToNextMarker(); - }); - var controlNextAnnotationContent = document.createElement("div"); - controlNextAnnotationContent.appendChild(document.createTextNode('\u{23F5}')); - controlNextAnnotation.appendChild(controlNextAnnotationContent); + var controlNextTimestamp = document.createElement("a"); + controlNextTimestamp.classList.add("episodeMarker"); + controlNextTimestamp.classList.add("nextTimestamp"); + var controlNextTimestampContent = document.createElement("div"); + controlNextTimestampContent.appendChild(document.createTextNode('\u{25B6}')); + controlNextTimestamp.appendChild(controlNextTimestampContent); if(rightmost) { - markersContainer.insertBefore(controlNextAnnotation, rightmost); + markersContainer.insertBefore(controlNextTimestamp, rightmost); } else { - markersContainer.appendChild(controlNextAnnotation); + markersContainer.appendChild(controlNextTimestamp); } - - cineraProps.D = devices.MOBILE; } -function switchToDesktopView(player) +function InitMobileStyle() { - if(quotesMenu) - { - quotesMenu.previousElementSibling.textContent = originalTextContent.TitleQuotes; - quotesMenu.style.top = "100%"; - } - if(referencesMenu) - { - referencesMenu.previousElementSibling.textContent = originalTextContent.TitleReferences; - referencesMenu.style.top = "100%"; - } - if(filterMenu) { filterMenu.style.top = "100%"; } - if(linkMenu) { linkMenu.style.top = "100%"; } - if(creditsMenu) - { - creditsMenu.previousElementSibling.textContent = originalTextContent.TitleCredits; - creditsMenu.style.top = "100%"; - } + cinera.classList.add("mobile"); + IconifyMenuTogglers(); + InitMobileControls(); + ApplyMobileStyle(); +} +function +ConnectMobileControls(player) +{ var markersContainer = player.markersContainer; - - var episodeMarkerPrev = markersContainer.querySelector(".episodeMarker.prev"); - if(episodeMarkerPrev) { episodeMarkerPrev.firstChild.textContent = originalTextContent.EpisodePrev; } - var episodeMarkerNext = markersContainer.querySelector(".episodeMarker.next"); - if(episodeMarkerNext) { episodeMarkerNext.firstChild.textContent = originalTextContent.EpisodeNext; } - - var prevAnnotation = markersContainer.querySelector(".episodeMarker.prevAnnotation"); - markersContainer.removeChild(prevAnnotation); - var nextAnnotation = markersContainer.querySelector(".episodeMarker.nextAnnotation"); - markersContainer.removeChild(nextAnnotation); - cineraProps.D = devices.DESKTOP; + var ControlPrevTimestamp = markersContainer.querySelector(".episodeMarker.prevTimestamp"); + ControlPrevTimestamp.addEventListener("click", function(ev) { + player.jumpToPrevMarker(); + }); + var ControlNextTimestamp = markersContainer.querySelector(".episodeMarker.nextTimestamp"); + ControlNextTimestamp.addEventListener("click", function(ev) { + player.jumpToNextMarker(); + }); } // Call this after changing the size of the video container in order to update the youtube player. Player.prototype.updateSize = function() { - var width = this.videoContainer.offsetWidth; - var height = width / 16 * 9; - if(window.innerHeight > 512 && window.innerWidth > 720) + var width = 0; + var height = 0; + if(cineraProps.D === devices.DESKTOP) { - if(cineraProps.D == devices.MOBILE) - { - switchToDesktopView(this); - } - this.markersContainer.style.height = height + "px"; // NOTE(matt): This was the original line here + width = this.videoContainer.offsetWidth; + height = width / 16 * 9; // TODO(matt): Get the aspect ratio from the video itself? + this.markersContainer.style.height = height + "px"; } else { - if(cineraProps.D == devices.DESKTOP) - { - switchToMobileView(this); - } + ApplyMobileStyle(); + width = this.videoContainer.offsetWidth; + height = this.videoContainer.offsetHeight; } if (this.youtubePlayerReady) { @@ -288,25 +545,10 @@ Player.prototype.onMarkerClick = function(marker, ev) { this.play(); }; -function getElementXOffsetFromPage(el) { - var left = 0; - do { - left += el.offsetLeft; - } while (el = el.offsetParent); - return left; -} - -function getElementYOffsetFromPage(el) { - var top = 0; - do { - top += el.offsetTop; - } while (el = el.offsetParent); - return top; -} - Player.prototype.onMarkerMouseMove = function(marker, ev) { if (this.currentMarker == marker) { - marker.hoverx = (ev.pageX - getElementXOffsetFromPage(marker.el)) / marker.el.offsetWidth; + var CineraContent = this.currentMarker.el.querySelector(".cineraContent"); + marker.hoverx = (ev.pageX - getElementXOffsetFromPage(CineraContent)) / CineraContent.offsetWidth; } }; @@ -329,7 +571,8 @@ Player.prototype.updateProgress = function() { } if (this.currentMarker) { - var totalWidth = this.currentMarker.el.offsetWidth; + var CineraContent = this.currentMarker.el.querySelector(".cineraContent"); + var totalWidth = CineraContent.offsetWidth; var progress = (this.currentTime - this.currentMarker.timestamp) / (this.currentMarker.endTime - this.currentMarker.timestamp); if (this.currentMarker.hoverx === null) { var pixelWidth = progress * totalWidth; @@ -364,9 +607,6 @@ Player.prototype.updateProgress = function() { this.currentMarker.el.classList.add("current"); this.scrollTo = this.currentMarker.el.offsetTop + this.currentMarker.el.offsetHeight/2.0; this.scrollPosition = this.markersContainer.scrollTop; - } - - if (this.currentMarker) { this.refsCallback(this.currentMarker.ref, this.currentMarker.el, this); } else if (prevMarker && prevMarker.ref) { this.refsCallback(null); @@ -445,6 +685,7 @@ Player.prototype.onYoutubeReady = function() { videoId: this.videoContainer.getAttribute("data-videoId"), width: this.videoContainer.offsetWidth, height: this.videoContainer.offsetWidth / 16 * 9, + playerVars: { 'playsinline': 1 }, //playerVars: { disablekb: 1 }, events: { "onReady": this.onYoutubePlayerReady.bind(this), @@ -636,13 +877,13 @@ function enterFullScreen_() { if(!document.mozFullScreen && !document.webkitFullScreen) { - if(cinera.mozRequestFullScreen) + if(document.mozRequestFullScreen) { - cinera.mozRequestFullScreen(); + document.mozRequestFullScreen(); } else { - cinera.webkitRequestFullScreen(Element.ALLOW_KEYBOARD_INPUT); + document.webkitRequestFullScreen(Element.ALLOW_KEYBOARD_INPUT); } } } @@ -673,6 +914,11 @@ function toggleTheatreMode() { cineraProps.H = cinera.style.height; cineraProps.mH = cinera.style.maxHeight; cineraProps.P = cinera.style.position; + cineraProps.Display = cinera.style.display; + cineraProps.FlexDirection = cinera.style.flexDirection; + cineraProps.JustifyContent = cinera.style.justifyContent; + cineraProps.ScrollX = window.scrollX; + cineraProps.ScrollY = window.scrollY; cinera.style.backgroundColor = "#000"; cinera.style.zIndex = 64; @@ -683,6 +929,9 @@ function toggleTheatreMode() { cinera.style.height = "100%"; cinera.style.maxHeight = "100%"; cinera.style.position = "fixed"; + cinera.style.display = "flex"; + cinera.style.flexDirection = "column"; + cinera.style.justifyContent = "center"; viewItems[0].setAttribute("data-id", "regular"); viewItems[0].setAttribute("title", "Regular mode"); @@ -703,6 +952,15 @@ function toggleTheatreMode() { cinera.style.height = cineraProps.H; cinera.style.maxHeight = cineraProps.mH; cinera.style.position = cineraProps.P; + cinera.style.display = cineraProps.Display; + cinera.style.flexDirection = cineraProps.FlexDirection; + cinera.style.justifyContent = cineraProps.JustifyContent; + window.scroll( + { + top: cineraProps.ScrollY, + left: cineraProps.ScrollX + } + ); viewItems[0].setAttribute("data-id", "theatre"); viewItems[0].setAttribute("title", "Theatre mode"); @@ -859,10 +1117,12 @@ function handleKey(key) { if(focusedElement.previousElementSibling) { focusedElement.classList.remove("focused"); + unfocusSprite(focusedElement); lastFocusedQuote = focusedElement.previousElementSibling; focusedElement = lastFocusedQuote; focusedElement.classList.add("focused"); + focusSprite(focusedElement); } } else if(focusedElement.parentNode.classList.contains("references_container")) @@ -870,15 +1130,19 @@ function handleKey(key) { if(focusedElement.previousElementSibling) { focusedElement.classList.remove("focused"); + unfocusSprite(focusedElement); focusedIdentifier.classList.remove("focused"); + unfocusSprite(focusedIdentifier); lastFocusedReference = focusedElement.previousElementSibling; focusedElement = lastFocusedReference; focusedElement.classList.add("focused"); + focusSprite(focusedElement); lastFocusedIdentifier = focusedElement.querySelector(".ref_indices").firstElementChild; focusedIdentifier = lastFocusedIdentifier; focusedIdentifier.classList.add("focused"); + focusSprite(focusedIdentifier); } } else if(focusedElement.parentNode.parentNode.classList.contains("filters")) @@ -887,10 +1151,12 @@ function handleKey(key) { focusedElement.previousElementSibling.classList.contains("filter_content")) { focusedElement.classList.remove("focused"); + unfocusSprite(focusedElement); lastFocusedCategory = focusedElement.previousElementSibling; focusedElement = lastFocusedCategory; focusedElement.classList.add("focused"); + focusSprite(focusedElement); } } else if(focusedElement.parentNode.classList.contains("credit")) @@ -898,6 +1164,7 @@ function handleKey(key) { if(focusedElement.parentNode.previousElementSibling) { focusedElement.classList.remove("focused"); + unfocusSprite(focusedElement); if(focusedElement.parentNode.previousElementSibling.querySelector(".support") && focusedElement.classList.contains("support")) { @@ -905,13 +1172,14 @@ function handleKey(key) { lastFocusedCreditItem = focusedElement.parentNode.previousElementSibling.querySelector(".support"); focusedElement = lastFocusedCreditItem; focusedElement.classList.add("focused"); - setSpriteLightness(focusedElement.firstChild); + focusSprite(focusedElement); } else { lastFocusedCreditItem = focusedElement.parentNode.previousElementSibling.querySelector(".person"); focusedElement = lastFocusedCreditItem; focusedElement.classList.add("focused"); + focusSprite(focusedElement); } } } @@ -926,10 +1194,12 @@ function handleKey(key) { if(focusedElement.nextElementSibling) { focusedElement.classList.remove("focused"); + unfocusSprite(focusedElement); lastFocusedQuote = focusedElement.nextElementSibling; focusedElement = lastFocusedQuote; focusedElement.classList.add("focused"); + focusSprite(focusedElement); } } else if(focusedElement.parentNode.classList.contains("references_container")) @@ -937,15 +1207,19 @@ function handleKey(key) { if(focusedElement.nextElementSibling) { focusedElement.classList.remove("focused"); + unfocusSprite(focusedElement); focusedIdentifier.classList.remove("focused"); + unfocusSprite(focusedIdentifier); lastFocusedReference = focusedElement.nextElementSibling; focusedElement = lastFocusedReference; focusedElement.classList.add("focused"); + focusSprite(focusedElement); lastFocusedIdentifier = focusedElement.querySelector(".ref_indices").firstElementChild; focusedIdentifier = lastFocusedIdentifier; focusedIdentifier.classList.add("focused"); + focusSprite(focusedIdentifier); } } else if(focusedElement.parentNode.parentNode.classList.contains("filters")) @@ -954,10 +1228,12 @@ function handleKey(key) { focusedElement.nextElementSibling.classList.contains("filter_content")) { focusedElement.classList.remove("focused"); + unfocusSprite(focusedElement); lastFocusedCategory = focusedElement.nextElementSibling; focusedElement = lastFocusedCategory; focusedElement.classList.add("focused"); + focusSprite(focusedElement); } } else if(focusedElement.parentNode.classList.contains("credit")) @@ -965,6 +1241,7 @@ function handleKey(key) { if(focusedElement.parentNode.nextElementSibling) { focusedElement.classList.remove("focused"); + unfocusSprite(focusedElement); if(focusedElement.parentNode.nextElementSibling.querySelector(".support") && focusedElement.classList.contains("support")) { @@ -972,13 +1249,14 @@ function handleKey(key) { lastFocusedCreditItem = focusedElement.parentNode.nextElementSibling.querySelector(".support"); focusedElement = lastFocusedCreditItem; focusedElement.classList.add("focused"); - setSpriteLightness(focusedElement.firstChild); + focusSprite(focusedElement); } else { lastFocusedCreditItem = focusedElement.parentNode.nextElementSibling.querySelector(".person"); focusedElement = lastFocusedCreditItem; focusedElement.classList.add("focused"); + focusSprite(focusedElement); } } } @@ -993,16 +1271,20 @@ function handleKey(key) { if(focusedIdentifier.previousElementSibling) { focusedIdentifier.classList.remove("focused"); + unfocusSprite(focusedIdentifier); lastFocusedIdentifier = focusedIdentifier.previousElementSibling; focusedIdentifier = lastFocusedIdentifier; focusedIdentifier.classList.add("focused"); + focusSprite(focusedIdentifier); } else if(focusedIdentifier.parentNode.previousElementSibling.classList.contains("ref_indices")) { focusedIdentifier.classList.remove("focused"); + unfocusSprite(focusedIdentifier); lastFocusedIdentifier = focusedIdentifier.parentNode.previousElementSibling.lastElementChild; focusedIdentifier = lastFocusedIdentifier; focusedIdentifier.classList.add("focused"); + focusSprite(focusedIdentifier); } } else if(focusedElement.classList.contains("filter_content")) @@ -1011,6 +1293,7 @@ function handleKey(key) { focusedElement.parentNode.previousElementSibling) { focusedElement.classList.remove("focused"); + unfocusSprite(focusedElement); lastFocusedMedium = focusedElement; if(!lastFocusedTopic) @@ -1020,6 +1303,7 @@ function handleKey(key) { lastFocusedCategory = lastFocusedTopic; focusedElement = lastFocusedCategory; focusedElement.classList.add("focused"); + focusSprite(focusedElement); } } else if(focusedElement.parentNode.classList.contains("credit")) @@ -1027,14 +1311,14 @@ function handleKey(key) { if(focusedElement.classList.contains("support")) { focusedElement.classList.remove("focused"); + console.log(focusedElement); + unfocusSprite(focusedElement); lastFocusedCreditItem = focusedElement.previousElementSibling; - if(focusedElement.firstChild.classList.contains("cineraSprite")) - { - setSpriteLightness(focusedElement.firstChild); - } + setSpriteLightness(focusedElement.firstChild); focusedElement = lastFocusedCreditItem; focusedElement.classList.add("focused"); + focusSprite(focusedElement); } } } @@ -1048,17 +1332,21 @@ function handleKey(key) { if(focusedIdentifier.nextElementSibling) { focusedIdentifier.classList.remove("focused"); + unfocusSprite(focusedIdentifier); lastFocusedIdentifier = focusedIdentifier.nextElementSibling; focusedIdentifier = lastFocusedIdentifier; focusedIdentifier.classList.add("focused"); + focusSprite(focusedIdentifier); } else if(focusedIdentifier.parentNode.nextElementSibling) { focusedIdentifier.classList.remove("focused"); + unfocusSprite(focusedIdentifier); lastFocusedIdentifier = focusedIdentifier.parentNode.nextElementSibling.firstElementChild; focusedIdentifier = lastFocusedIdentifier; focusedIdentifier.classList.add("focused"); + focusSprite(focusedIdentifier); } } else if(focusedElement.classList.contains("filter_content")) @@ -1067,6 +1355,7 @@ function handleKey(key) { focusedElement.parentNode.nextElementSibling) { focusedElement.classList.remove("focused"); + unfocusSprite(focusedElement); lastFocusedTopic = focusedElement; if(!lastFocusedMedium) @@ -1076,6 +1365,7 @@ function handleKey(key) { lastFocusedCategory = lastFocusedMedium; focusedElement = lastFocusedCategory; focusedElement.classList.add("focused"); + focusSprite(focusedElement); } } else if(focusedElement.parentNode.classList.contains("credit")) @@ -1084,14 +1374,12 @@ function handleKey(key) { focusedElement.nextElementSibling) { focusedElement.classList.remove("focused"); + unfocusSprite(focusedElement); lastFocusedCreditItem = focusedElement.nextElementSibling; focusedElement = lastFocusedCreditItem; focusedElement.classList.add("focused"); - if(focusedElement.firstChild.classList.contains("cineraSprite")) - { - setSpriteLightness(focusedElement.firstChild); - } + focusSprite(focusedElement); } } } @@ -1105,6 +1393,7 @@ function handleKey(key) { focusedElement.nextElementSibling.classList.contains("filter_content")) { focusedElement.classList.remove("focused"); + unfocusSprite(focusedElement); if(focusedElement.parentNode.classList.contains("filter_topics")) { lastFocusedTopic = focusedElement.nextElementSibling; @@ -1118,6 +1407,7 @@ function handleKey(key) { lastFocusedElement = lastFocusedCategory; focusedElement = lastFocusedElement; focusedElement.classList.add("focused"); + focusSprite(focusedElement); } } } break; @@ -1130,6 +1420,7 @@ function handleKey(key) { focusedElement.previousElementSibling.classList.contains("filter_content")) { focusedElement.classList.remove("focused"); + unfocusSprite(focusedElement); if(focusedElement.parentNode.classList.contains("filter_topics")) { lastFocusedTopic = focusedElement.previousElementSibling; @@ -1143,6 +1434,7 @@ function handleKey(key) { lastFocusedElement = lastFocusedCategory; focusedElement = lastFocusedElement; focusedElement.classList.add("focused"); + focusSprite(focusedElement); } } } break; @@ -1163,7 +1455,10 @@ function handleKey(key) { } break; case "?": { - helpDocumentation.classList.toggle("visible"); + if(helpDocumentation) + { + helpDocumentation.classList.toggle("visible"); + } } break; case 'N': @@ -1319,7 +1614,7 @@ function filterItemToggle(filterItem) { filterItem.querySelector(".icon").style.backgroundColor = getComputedStyle(filterItem.querySelector(".icon")).getPropertyValue("border-color"); } setDotLightness(filterItem.querySelector(".icon")); - var testMarkers = document.querySelectorAll(".marker.off_" + selectedCategory); + var testMarkers = cinera.querySelectorAll(".marker.off_" + selectedCategory); for(var j = 0; j < testMarkers.length; ++j) { if(filterState[selectedCategory].type == "topic") @@ -1461,35 +1756,36 @@ function onRefChanged(ref, element, player) { { player.jumpToNextMarker(); } - return; } - - for (var MenuIndex = 0; MenuIndex < sourceMenus.length; ++MenuIndex) + else { - var SetMenu = 0; - if (ref !== undefined && ref !== null) { - var refElements = sourceMenus[MenuIndex].querySelectorAll(".refs .ref"); - var refs = ref.split(","); + for (var MenuIndex = 0; MenuIndex < sourceMenus.length; ++MenuIndex) + { + var SetMenu = 0; + if (ref !== undefined && ref !== null) { + var refElements = sourceMenus[MenuIndex].querySelectorAll(".refs .ref"); + var refs = ref.split(","); - for (var i = 0; i < refElements.length; ++i) { - if (refs.includes(refElements[i].getAttribute("data-id"))) { - refElements[i].classList.add("current"); - SetMenu = 1; - } else { - refElements[i].classList.remove("current"); + for (var i = 0; i < refElements.length; ++i) { + if (refs.includes(refElements[i].getAttribute("data-id"))) { + refElements[i].classList.add("current"); + SetMenu = 1; + } else { + refElements[i].classList.remove("current"); + } } - } - if(SetMenu) { - sourceMenus[MenuIndex].classList.add("current"); + if(SetMenu) { + sourceMenus[MenuIndex].classList.add("current"); + } else { + sourceMenus[MenuIndex].classList.remove("current"); + } + } else { sourceMenus[MenuIndex].classList.remove("current"); - } - - } else { - sourceMenus[MenuIndex].classList.remove("current"); - var refs = sourceMenus[MenuIndex].querySelectorAll(".refs .ref"); - for (var i = 0; i < refs.length; ++i) { - refs[i].classList.remove("current"); + var refs = sourceMenus[MenuIndex].querySelectorAll(".refs .ref"); + for (var i = 0; i < refs.length; ++i) { + refs[i].classList.remove("current"); + } } } } @@ -1574,10 +1870,11 @@ function mouseSkipToTimecode(player, time, ev) return false; } -function handleMouseOverMenu(menu, eventType) +function handleMenuTogglerInteraction(menu, eventType) { if(!(menu.classList.contains("visible")) && eventType == "mouseenter" || - menu.classList.contains("visible") && eventType == "mouseleave") + menu.classList.contains("visible") && eventType == "mouseleave" || + (eventType == "click" && !menu.classList.contains("cineraHelp"))) { if(menu.classList.contains("quotes")) { @@ -1600,10 +1897,6 @@ function handleMouseOverMenu(menu, eventType) toggleMenuVisibility(creditsMenu); } } - if(eventType == "click" && menu.classList.contains("help")) - { - helpDocumentation.classList.toggle("visible"); - } } function RGBtoHSL(colour) @@ -1648,7 +1941,6 @@ function getBackgroundBrightness(element) { var result = Math.sqrt(rgb[0] * rgb[0] * .241 + rgb[1] * rgb[1] * .691 + rgb[2] * rgb[2] * .068); - console.log(result); return result; } diff --git a/cinera/cinera_pre.js b/cinera/cinera_pre.js index fecf7b5..14a0c83 100644 --- a/cinera/cinera_pre.js +++ b/cinera/cinera_pre.js @@ -1,3 +1,24 @@ +var orientations = { + PORTRAIT: 0, + LANDSCAPE_LEFT: 90, + LANDSCAPE_RIGHT: -90, +}; + +var DebugConsoleMessageCount = 0; +function Say(Message) +{ + var DebugConsole = document.getElementById("debug-console"); + if(DebugConsole) + { + DebugConsole.textContent += DebugConsoleMessageCount++ + ": " + Message + "\n"; + DebugConsole.scrollTo( + { + top: DebugConsole.scrollHeight, + behavior: "smooth" + }); + } +} + function getBackgroundBrightness(element) { var colour = getComputedStyle(element).getPropertyValue("background-color"); var depth = 0; @@ -110,3 +131,233 @@ function unfocusSprite(Element) } } } + +function IsMobile() { + // NOTE(matt): From https://medium.com/simplejs/detect-the-users-device-type-with-a-simple-javascript-check-4fc656b735e1 + var Identifier = navigator.userAgent||navigator.vendor||window.opera; + var Result = (/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i.test(Identifier)||/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(Identifier.substr(0,4))); + return Result; +}; + +function GetRule(SelectorText) +{ + // NOTE(matt): Modifying CSS style + // from https://stackoverflow.com/a/566445 + // https://usefulangle.com/post/39/adding-css-to-stylesheet-with-javascript + var Result = undefined; + + var StyleSheets = document.styleSheets; + var cssRuleCode = document.all ? 'rules' : 'cssRules'; // account for IE and FF + for(var StyleSheetIndex = StyleSheets.length - 1; StyleSheetIndex >= 0; --StyleSheetIndex) + { + var ThisSheet = StyleSheets[StyleSheetIndex]; + var Rules = ThisSheet[cssRuleCode]; + for(var RuleIndex = Rules.length - 1; RuleIndex >= 0; --RuleIndex) + { + var ThisRule = Rules[RuleIndex]; + if(SelectorText === ThisRule.selectorText) + { + Result = ThisRule; + break; + } + } + if(Result !== undefined) { break; } + } + + return Result; +} + +function +GetRulesOfStyleSheetIndex(Index) +{ + var Result = undefined; + var StyleSheets = document.styleSheets; + var cssRuleCode = document.all ? 'rules' : 'cssRules'; // account for IE and FF + var StyleSheet = StyleSheets[Index]; + if(StyleSheet) + { + Result = StyleSheet[cssRuleCode]; + } + return Result; +} + +function +GetOrSetRule(SelectorText) +{ + var Result = GetRule(SelectorText); + if(Result === undefined) + { + var StyleSheet = document.styleSheets[0]; + var RuleIndex = StyleSheet.insertRule(SelectorText + "{}", StyleSheet.length - 1); + var Rules = GetRulesOfStyleSheetIndex(0); + Result = Rules[RuleIndex]; + } + return Result; +} + +/* Auto-scrolling */ +var LastScrollYPos = 0; +var ScrollTicking = false; +var ScrollerFunction; +var ScrollCondition; + +function ScrollTo(Element, ScrollPos) { + var BoundingRect = Element.getBoundingClientRect(); + var Height = BoundingRect.height; + var PercentageOfInView = 89; + var GatherableHeight = Height * (1 - PercentageOfInView / 100); + var ScrollY = BoundingRect.top; + var YOffsetFromPage = getElementYOffsetFromPage(Element); + var DesiredScroll = null; + + if(ScrollY < 0) + { + ScrollY = ~ScrollY; + if(ScrollY <= GatherableHeight) + { + DesiredScroll = YOffsetFromPage; + } + } + else + { + var LowerProtrusion = ScrollY - (window.innerHeight - Height); + if(LowerProtrusion > 0 && LowerProtrusion <= GatherableHeight) + { + DesiredScroll = ScrollPos + LowerProtrusion; + } + } + + if(DesiredScroll !== null) + { + window.scrollTo({ + top: DesiredScroll, + behavior: "smooth" + }); + } +} + +function +InitScrollEventListener(Element) +{ + window.addEventListener('scroll', function() { + if(ScrollCondition == undefined || ScrollCondition == true) + { + LastScrollYPos = window.scrollY; + + if (!ScrollTicking) { + window.requestAnimationFrame(function() { + clearTimeout(ScrollerFunction); + ScrollerFunction = setTimeout(ScrollTo, 2000, Element, LastScrollYPos); + ScrollTicking = false; + }); + + ScrollTicking = true; + } + } + }); +} +/* /Auto-scrolling */ + +function getElementXOffsetFromPage(el) { + var left = 0; + do { + left += el.offsetLeft; + } while (el = el.offsetParent); + return left; +} + +function getElementYOffsetFromPage(el) { + var top = 0; + do { + top += el.offsetTop; + } while (el = el.offsetParent); + return top; +} + +function +MaxWidthOfElement(Element) +{ + var Result = 0; + var OriginalWidth = Element.style.width; + Element.style.width = "100%"; + var Max = parseInt(window.getComputedStyle(Element).width); + Element.style.width = "unset"; + var Default = parseInt(window.getComputedStyle(Element).width); + Element.style.width = OriginalWidth; + + if(Max > window.innerWidth || Max == Default) + { + Result = window.innerWidth; + } + else + { + Result = Max; + } + return Result; +} + +function +MaxHeightOfElement(Element) +{ + var Result = 0; + var OriginalHeight = Element.style.height; + Element.style.height = "100%"; + var Max = parseInt(window.getComputedStyle(Element).height); + Element.style.height = "unset"; + var Default = parseInt(window.getComputedStyle(Element).height); + Element.style.height = OriginalHeight; + + if(Max > window.innerHeight || Max == Default) + { + Result = window.innerHeight; + } + else + { + Result = Max; + } + return Result; +} + +function +Clamp(EndA, N, EndB) +{ + var Min = EndA < EndB ? EndA : EndB; + var Max = EndA > EndB ? EndA : EndB; + return N < Min ? Min : N > Max ? Max : N; +} + +function +Clamp01(N) +{ + return N < 0 ? 0 : N > 1 ? 1 : N; +} + +function +Lerp(A, t, B) +{ + return (1-t)*A + t*B; +} + +function +IsOverflowed(Element) +{ + return Element.scrollHeight > Element.clientHeight || Element.scrollWidth > Element.clientWidth; +} + +function +BindHelp(Button, DocumentationContainer) +{ + window.addEventListener("blur", function(){ + Button.firstElementChild.innerText = "¿"; + Button.firstElementChild.title = "Keypresses will not pass through to Cinera because focus is currently elsewhere.\n\nTo regain focus, please press Tab / Shift-Tab (multiple times) or click somewhere related to Cinera other than the video, e.g. this button"; + }); + + window.addEventListener("focus", function(){ + Button.firstElementChild.innerText = "?"; + Button.firstElementChild.title = "" + }); + + Button.addEventListener("click", function() { + DocumentationContainer.classList.toggle("visible"); + }) +} diff --git a/cinera/cinera_search.js b/cinera/cinera_search.js deleted file mode 100644 index 9b679c0..0000000 --- a/cinera/cinera_search.js +++ /dev/null @@ -1,642 +0,0 @@ -document.body.style.overflowY = "scroll"; - -if (location.hash && location.hash.length > 0) { - var initialQuery = location.hash; - if (initialQuery[0] == "#") { - initialQuery = initialQuery.slice(1); - } - document.getElementById("query").value = decodeURIComponent(initialQuery); -} - -var indexControl = document.getElementById("cineraIndexControl"); -var indexSort = indexControl.querySelector("#cineraIndexSort"); -var indexSortChronological = true; - -var filterMenu = indexControl.querySelector(".cineraIndexFilter"); -if(filterMenu) -{ - var filterContainer = filterMenu.querySelector(".filter_container"); - //menuState.push(linkMenu); - - filterMenu.addEventListener("mouseenter", function(ev) { - filterContainer.style.display = "block"; - }); - - filterMenu.addEventListener("mouseleave", function(ev) { - filterContainer.style.display = "none"; - }); -} - -function hideEntriesOfProject(ProjectElement) -{ - if(!ProjectElement.classList.contains("off")) - { - ProjectElement.classList.add("off"); - } - var baseURL = ProjectElement.attributes.getNamedItem("data-baseURL").value; - var searchLocation = ProjectElement.attributes.getNamedItem("data-searchLocation").value; - var playerLocation = ProjectElement.attributes.getNamedItem("data-playerLocation").value; - for(var i = 0; i < projects.length; ++i) - { - var ThisProject = projects[i]; - if(baseURL === ThisProject.baseURL && searchLocation === ThisProject.searchLocation && playerLocation === ThisProject.playerLocation) - { - ThisProject.filteredOut = true; - if(ThisProject.entriesContainer != null) - { - ThisProject.entriesContainer.style.display = "none"; - disableSprite(ThisProject.entriesContainer.parentElement); - } - } - } -} - -function showEntriesOfProject(ProjectElement) -{ - if(ProjectElement.classList.contains("off")) - { - ProjectElement.classList.remove("off"); - } - var baseURL = ProjectElement.attributes.getNamedItem("data-baseURL").value; - var searchLocation = ProjectElement.attributes.getNamedItem("data-searchLocation").value; - var playerLocation = ProjectElement.attributes.getNamedItem("data-playerLocation").value; - for(var i = 0; i < projects.length; ++i) - { - var ThisProject = projects[i]; - if(baseURL === ThisProject.baseURL && searchLocation === ThisProject.searchLocation && playerLocation === ThisProject.playerLocation) - { - ThisProject.filteredOut = false; - if(ThisProject.entriesContainer != null) - { - ThisProject.entriesContainer.style.display = "flex"; - enableSprite(ThisProject.entriesContainer.parentElement); - } - } - } -} - -function hideProjectSearchResults(baseURL, searchLocation, playerLocation) -{ - var cineraResults = document.getElementById("cineraResults"); - if(cineraResults) - { - var cineraResultsProjects = cineraResults.querySelectorAll(".projectContainer"); - for(var i = 0; i < cineraResultsProjects.length; ++i) - { - var resultBaseURL = cineraResultsProjects[i].attributes.getNamedItem("data-baseURL").value; - var resultSearchLocation = cineraResultsProjects[i].attributes.getNamedItem("data-searchLocation").value; - var resultPlayerLocation = cineraResultsProjects[i].attributes.getNamedItem("data-playerLocation").value; - if(baseURL === resultBaseURL && searchLocation === resultSearchLocation && playerLocation === resultPlayerLocation) - { - cineraResultsProjects[i].style.display = "none"; - return; - } - } - } -} - -function showProjectSearchResults(baseURL, searchLocation, playerLocation) -{ - var cineraResults = document.getElementById("cineraResults"); - if(cineraResults) - { - var cineraResultsProjects = cineraResults.querySelectorAll(".projectContainer"); - for(var i = 0; i < cineraResultsProjects.length; ++i) - { - var resultBaseURL = cineraResultsProjects[i].attributes.getNamedItem("data-baseURL").value; - var resultSearchLocation = cineraResultsProjects[i].attributes.getNamedItem("data-searchLocation").value; - var resultPlayerLocation = cineraResultsProjects[i].attributes.getNamedItem("data-playerLocation").value; - if(baseURL === resultBaseURL && searchLocation === resultSearchLocation && playerLocation === resultPlayerLocation) - { - cineraResultsProjects[i].style.display = "flex"; - return; - } - } - } -} - -function toggleEntriesOfProjectAndChildren(ProjectFilterElement) -{ - var baseURL = ProjectFilterElement.attributes.getNamedItem("data-baseURL").value; - var searchLocation = ProjectFilterElement.attributes.getNamedItem("data-searchLocation").value; - var playerLocation = ProjectFilterElement.attributes.getNamedItem("data-playerLocation").value; - var shouldShow = ProjectFilterElement.classList.contains("off"); - if(shouldShow) - { - ProjectFilterElement.classList.remove("off"); - enableSprite(ProjectFilterElement); - } - else - { - ProjectFilterElement.classList.add("off"); - disableSprite(ProjectFilterElement); - } - - for(var i = 0; i < projects.length; ++i) - { - var ThisProject = projects[i]; - - if(baseURL === ThisProject.baseURL && searchLocation === ThisProject.searchLocation && playerLocation === ThisProject.playerLocation) - { - if(shouldShow) - { - ThisProject.filteredOut = false; - enableSprite(ThisProject.projectTitleElement.parentElement); - if(ThisProject.entriesContainer != null) - { - ThisProject.entriesContainer.style.display = "flex"; - } - showProjectSearchResults(ThisProject.baseURL, ThisProject.searchLocation, ThisProject.playerLocation); - } - else - { - ThisProject.filteredOut = true; - disableSprite(ThisProject.projectTitleElement.parentElement); - if(ThisProject.entriesContainer != null) - { - ThisProject.entriesContainer.style.display = "none"; - } - hideProjectSearchResults(ThisProject.baseURL, ThisProject.searchLocation, ThisProject.playerLocation); - } - } - } - - var indexChildFilterProjects = ProjectFilterElement.querySelectorAll(".cineraFilterProject"); - - for(var j = 0; j < indexChildFilterProjects.length; ++j) - { - var ThisElement = indexChildFilterProjects[j]; - var baseURL = ThisElement.attributes.getNamedItem("data-baseURL").value; - var searchLocation = ThisElement.attributes.getNamedItem("data-searchLocation").value; - var playerLocation = ThisElement.attributes.getNamedItem("data-playerLocation").value; - if(shouldShow) - { - showEntriesOfProject(ThisElement); - showProjectSearchResults(baseURL, searchLocation, playerLocation); - } - else - { - hideEntriesOfProject(ThisElement); - hideProjectSearchResults(baseURL, searchLocation, playerLocation); - } - } -} - -var indexFilter = indexControl.querySelector(".cineraIndexFilter"); -if(indexFilter) -{ - var indexFilterProjects = indexFilter.querySelectorAll(".cineraFilterProject"); - for(var i = 0; i < indexFilterProjects.length; ++i) - { - indexFilterProjects[i].addEventListener("mouseover", function(ev) { - ev.stopPropagation(); - this.classList.add("focused"); - focusSprite(this); - }); - indexFilterProjects[i].addEventListener("mouseout", function(ev) { - ev.stopPropagation(); - this.classList.remove("focused"); - unfocusSprite(this); - }); - indexFilterProjects[i].addEventListener("click", function(ev) { - ev.stopPropagation(); - toggleEntriesOfProjectAndChildren(this); - }); - } -} - -var resultsSummary = document.getElementById("cineraResultsSummary"); -var resultsContainer = document.getElementById("cineraResults"); - -var indexContainer = document.getElementById("cineraIndex"); - -var projectsContainer = indexContainer.querySelectorAll(".cineraIndexProject"); - -var projectContainerPrototype = document.createElement("DIV"); -projectContainerPrototype.classList.add("projectContainer"); - -var dayContainerPrototype = document.createElement("DIV"); -dayContainerPrototype.classList.add("dayContainer"); - -var dayNamePrototype = document.createElement("SPAN"); -dayNamePrototype.classList.add("dayName"); -dayContainerPrototype.appendChild(dayNamePrototype); - -var markerListPrototype = document.createElement("DIV"); -markerListPrototype.classList.add("markerList"); -dayContainerPrototype.appendChild(markerListPrototype); - -var markerPrototype = document.createElement("A"); -markerPrototype.classList.add("marker"); -if(resultsContainer.getAttribute("data-single") == 0) -{ - markerPrototype.setAttribute("target", "_blank"); -} - -function prepareToParseIndexFile(project) -{ - project.xhr.addEventListener("load", function() { - var contents = project.xhr.response; - var lines = contents.split("\n"); - var mode = "none"; - var episode = null; - for (var i = 0; i < lines.length; ++i) { - var line = lines[i]; - if (line.trim().length == 0) { continue; } - if (line == "---") { - if (episode != null && episode.name != null && episode.title != null) { - episode.filename = episode.name; - episode.day = getEpisodeName(episode.filename + ".html.md"); - episode.dayContainerPrototype = project.dayContainerPrototype; - episode.markerPrototype = markerPrototype; - episode.playerURLPrefix = project.playerURLPrefix; - project.episodes.push(episode); - } - episode = {}; - mode = "none"; - } else if (line.startsWith("name:")) { - episode.name = line.slice(6); - } else if (line.startsWith("title:")) { - episode.title = line.slice(7).trim().slice(1, -1); - } else if (line.startsWith("markers")) { - mode = "markers"; - episode.markers = []; - } else if (mode == "markers") { - var match = line.match(/"(\d+)": "(.+)"/); - if (match == null) { - console.log(name, line); - } else { - var totalTime = parseInt(line.slice(1)); - var marker = { - totalTime: totalTime, - prettyTime: markerTime(totalTime), - text: match[2].replace(/\\"/g, "\"") - } - episode.markers.push(marker); - } - } - } - document.querySelector(".spinner").classList.remove("show"); - project.parsed = true; - runSearch(true); - }); - project.xhr.addEventListener("error", function() { - console.error("Failed to load content"); - }); -} - -var projects = []; -function prepareProjects() -{ - for(var i = 0; i < projectsContainer.length; ++i) - { - var ID = projectsContainer[i].attributes.getNamedItem("data-project").value; - var baseURL = projectsContainer[i].attributes.getNamedItem("data-baseURL").value; - var searchLocation = projectsContainer[i].attributes.getNamedItem("data-searchLocation").value; - var playerLocation = projectsContainer[i].attributes.getNamedItem("data-playerLocation").value; - var theme = projectsContainer[i].classList.item(1); - - projects[i] = - { - baseURL: baseURL, - searchLocation: searchLocation, - playerLocation: playerLocation, - playerURLPrefix: (baseURL ? baseURL + "/" : "") + (playerLocation ? playerLocation + "/" : ""), - indexLocation: (baseURL ? baseURL + "/" : "") + (searchLocation ? searchLocation + "/" : "") + ID + ".index", - projectTitleElement: projectsContainer[i].querySelector(":scope > .cineraProjectTitle"), - entriesContainer: projectsContainer[i].querySelector(":scope > .cineraIndexEntries"), - dayContainerPrototype: dayContainerPrototype.cloneNode(true), - filteredOut: false, - parsed: false, - searched: false, - resultsToRender: [], - resultsIndex: 0, - theme: theme, - episodes: [], - xhr: new XMLHttpRequest(), - } - - projects[i].dayContainerPrototype.classList.add(theme); - projects[i].dayContainerPrototype.children[1].classList.add(theme); - - document.querySelector(".spinner").classList.add("show"); - projects[i].xhr.open("GET", projects[i].indexLocation); - projects[i].xhr.setRequestHeader("Content-Type", "text/plain"); - projects[i].xhr.send(); - prepareToParseIndexFile(projects[i]); - } -} -prepareProjects(); - -indexSort.addEventListener("click", function(ev) { - if(indexSortChronological) - { - this.firstChild.nodeValue = "Sort: New to Old ⏷" - for(var i = 0; i < projects.length; ++i) - { - if(projects[i].entriesContainer) - { - projects[i].entriesContainer.style.flexFlow = "column-reverse"; - } - } - } - else - { - this.firstChild.nodeValue = "Sort: Old to New ⏶" - for(var i = 0; i < projects.length; ++i) - { - if(projects[i].entriesContainer) - { - projects[i].entriesContainer.style.flexFlow = "column"; - } - } - } - indexSortChronological = !indexSortChronological; - runSearch(true); -}); - -var lastQuery = null; -var markerList = null; -var projectContainer = null; -var resultsMarkerIndex = -1; -var rendering = false; - -var highlightPrototype = document.createElement("B"); - -function getEpisodeName(filename) { - var day = filename; - var dayParts = day.match(/([a-zA-Z_-]+)([0-9]+)?([a-zA-Z]+)?/); - day = dayParts[1].slice(0, 1).toUpperCase() + dayParts[1].slice(1) + (dayParts[2] ? " " + dayParts[2] : "") + (dayParts[3] ? " " + dayParts[3].toUpperCase() : ""); - return day; -} - -function markerTime(totalTime) { - var markTime = "("; - var hours = Math.floor(totalTime / 60 / 60); - var minutes = Math.floor(totalTime / 60) % 60; - var seconds = totalTime % 60; - if (hours > 0) { - markTime += padTimeComponent(hours) + ":"; - } - - markTime += padTimeComponent(minutes) + ":" + padTimeComponent(seconds) + ")"; - - return markTime; -} - -function padTimeComponent(component) { - return (component < 10 ? "0" + component : component); -} - -function resetProjectsForSearch() -{ - for(var i = 0; i < projects.length; ++i) - { - var project = projects[i]; - project.searched = false; - project.resultsToRender = []; - } -} - -var renderHandle; - -function runSearch(refresh) { - var queryStr = document.getElementById("query").value; - if (refresh || lastQuery != queryStr) { - var oldResultsContainer = resultsContainer; - resultsContainer = oldResultsContainer.cloneNode(false); - oldResultsContainer.parentNode.insertBefore(resultsContainer, oldResultsContainer); - oldResultsContainer.remove(); - for(var i = 0; i < projects.length; ++i) - { - projects[i].resultsIndex = 0; - } - resultsMarkerIndex = -1; - } - lastQuery = queryStr; - - resetProjectsForSearch(); - - var numEpisodes = 0; - var numMarkers = 0; - var totalSeconds = 0; - - // NOTE(matt): Function defined within runSearch() so that we can modify numEpisodes, numMarkers and totalSeconds - function runSearchInterior(resultsToRender, query, episode) - { - var matches = []; - for (var k = 0; k < episode.markers.length; ++k) { - query.lastIndex = 0; - var result = query.exec(episode.markers[k].text); - if (result && result[0].length > 0) { - numMarkers++; - matches.push(episode.markers[k]); - if (k < episode.markers.length-1) { - totalSeconds += episode.markers[k+1].totalTime - episode.markers[k].totalTime; - } - } - } - if (matches.length > 0) { - numEpisodes++; - resultsToRender.push({ - query: query, - episode: episode, - matches: matches - }); - } - } - - if (queryStr && queryStr.length > 0) { - indexContainer.style.display = "none"; - resultsSummary.style.display = "block"; - var shouldRender = false; - var query = new RegExp(queryStr.replace("(", "\\(").replace(")", "\\)").replace(/\|+/, "\|").replace(/\|$/, "").replace(/(^|[^\\])\\$/, "$1"), "gi"); - - // Visible - for(var i = 0; i < projects.length; ++i) - { - var project = projects[i]; - if(project.parsed && !project.filteredOut && project.episodes.length > 0) { - if(indexSortChronological) - { - for(var j = 0; j < project.episodes.length; ++j) { - var episode = project.episodes[j]; - runSearchInterior(project.resultsToRender, query, episode); - } - } - else - { - for(var j = project.episodes.length; j > 0; --j) { - var episode = project.episodes[j - 1]; - runSearchInterior(project.resultsToRender, query, episode); - } - } - - shouldRender = true; - project.searched = true; - - } - } - - // Invisible - for(var i = 0; i < projects.length; ++i) - { - var project = projects[i]; - if(project.parsed && project.filteredOut && !project.searched && project.episodes.length > 0) { - if(indexSortChronological) - { - for(var j = 0; j < project.episodes.length; ++j) { - var episode = project.episodes[j]; - runSearchInterior(project.resultsToRender, query, episode); - } - } - else - { - for(var j = project.episodes.length; j > 0; --j) { - var episode = project.episodes[j - 1]; - runSearchInterior(project.resultsToRender, query, episode); - } - } - - shouldRender = true; - project.searched = true; - - } - } - - if(shouldRender) - { - if (rendering) { - clearTimeout(renderHandle); - } - renderResults(); - } - } - else - { - indexContainer.style.display = "block"; - resultsSummary.style.display = "none"; - } - - var totalTime = Math.floor(totalSeconds/60/60) + "h " + Math.floor(totalSeconds/60)%60 + "m " + totalSeconds%60 + "s "; - - resultsSummary.textContent = "Found: " + numEpisodes + " episodes, " + numMarkers + " markers, " + totalTime + "total."; -} - -function renderResults() { - var maxItems = 42; - var numItems = 0; - for(var i = 0; i < projects.length; ++i) - { - var project = projects[i]; - if (project.resultsIndex < project.resultsToRender.length) { - rendering = true; - while (numItems < maxItems && project.resultsIndex < project.resultsToRender.length) { - var query = project.resultsToRender[project.resultsIndex].query; - var episode = project.resultsToRender[project.resultsIndex].episode; - var matches = project.resultsToRender[project.resultsIndex].matches; - if (resultsMarkerIndex == -1) { - if(project.resultsIndex == 0 || project.resultsToRender[project.resultsIndex - 1].episode.playerURLPrefix != episode.playerURLPrefix) - { - projectContainer = projectContainerPrototype.cloneNode(true); - for(var i = 0; i < projects.length; ++i) - { - if(projects[i].playerURLPrefix === episode.playerURLPrefix) - { - projectContainer.setAttribute("data-baseURL", projects[i].baseURL); - projectContainer.setAttribute("data-searchLocation", projects[i].searchLocation); - projectContainer.setAttribute("data-playerLocation", projects[i].playerLocation); - if(projects[i].filteredOut) - { - projectContainer.style.display = "none"; - } - } - } - resultsContainer.appendChild(projectContainer); - } - else - { - projectContainer = resultsContainer.lastElementChild; - } - - - var dayContainer = episode.dayContainerPrototype.cloneNode(true); - var dayName = dayContainer.children[0]; - markerList = dayContainer.children[1]; - dayName.textContent = episode.day + ": " + episode.title; - projectContainer.appendChild(dayContainer); - resultsMarkerIndex = 0; - numItems++; - } - - while (numItems < maxItems && resultsMarkerIndex < matches.length) { - var match = matches[resultsMarkerIndex]; - var marker = episode.markerPrototype.cloneNode(true); - marker.setAttribute("href", episode.playerURLPrefix + episode.filename.replace(/"/g, "") + "/#" + match.totalTime); - query.lastIndex = 0; - var cursor = 0; - var text = match.text; - var result = null; - marker.appendChild(document.createTextNode(match.prettyTime + " ")); - while (result = query.exec(text)) { - if (result.index > cursor) { - marker.appendChild(document.createTextNode(text.slice(cursor, result.index))); - } - var highlightEl = highlightPrototype.cloneNode(); - highlightEl.textContent = result[0]; - marker.appendChild(highlightEl); - cursor = result.index + result[0].length; - } - - if (cursor < text.length) { - marker.appendChild(document.createTextNode(text.slice(cursor, text.length))); - } - markerList.appendChild(marker); - numItems++; - resultsMarkerIndex++; - } - - if (resultsMarkerIndex == matches.length) { - resultsMarkerIndex = -1; - project.resultsIndex++; - } - } - renderHandle = setTimeout(renderResults, 0); - } else { - rendering = false; - } - } -} - -function IsVisible(el) { - var xPos = 0; - var yPos = 0; - var Height = parseInt(getComputedStyle(el).height); - - while (el) { - if (el.tagName == "BODY") { - var xScroll = el.scrollLeft || document.documentElement.scrollLeft; - var yScroll = el.scrollTop || document.documentElement.scrollTop; - - xPos += (el.offsetLeft - xScroll + el.clientLeft) - yPos += (el.offsetTop - yScroll + el.clientTop) - } else { - xPos += (el.offsetLeft - el.scrollLeft + el.clientLeft); - yPos += (el.offsetTop - el.scrollTop + el.clientTop); - } - - el = el.offsetParent; - } - return ((xPos > 0 && xPos < window.innerWidth) && (yPos > 0 && yPos + Height < window.innerHeight)); -} - -var queryEl = document.getElementById("query"); -if(document.hasFocus() && IsVisible(queryEl)) { queryEl.focus(); } -queryEl.addEventListener("input", function(ev) { - history.replaceState(null, null, "#" + encodeURIComponent(queryEl.value)); - runSearch(); -}); - -runSearch(); - -// Testing diff --git a/cinera/cinera_search_post.js b/cinera/cinera_search_post.js new file mode 100644 index 0000000..e415e3b --- /dev/null +++ b/cinera/cinera_search_post.js @@ -0,0 +1,62 @@ +document.body.style.overflowY = "scroll"; + +// Element Selection +// +Nav.Nexus = document.getElementById("cineraIndex"); +Nav.Controls.Header = document.getElementById("cineraIndexControl"); +Nav.Controls.Sort = Nav.Controls.Header.querySelector(".cineraMenuItem.sort"); +Nav.Controls.View = Nav.Controls.Header.querySelector(".cineraMenuItem.view"); +Nav.Controls.Anim = Nav.Controls.Header.querySelector(".cineraMenuItem.anim"); +Nav.Controls.Save = Nav.Controls.Header.querySelector(".cineraMenuItem.save"); +Nav.Controls.Help = Nav.Nexus.querySelector(".cineraHelp"); +Nav.Controls.HelpDocumentation = Nav.Controls.Help.querySelector(".help_container"); +Nav.GridContainer = Nav.Nexus.querySelector(".cineraIndexGridContainer"); +Nav.Controls.GridTraversal.Header = Nav.GridContainer.querySelector(".cineraTraversal"); +Nav.Controls.GridTraversal.Ascend = Nav.Controls.GridTraversal.Header.querySelector(".cineraButton.ascension"); +Nav.Controls.GridTraversal.Prev = Nav.Controls.GridTraversal.Header.querySelector(".cineraButton.prev"); +Nav.Controls.GridTraversal.Next = Nav.Controls.GridTraversal.Header.querySelector(".cineraButton.next"); + +Search.QueryElement = document.getElementById("query"); +Search.ResultsSummary = document.getElementById("cineraResultsSummary"); +Search.ResultsContainer = document.getElementById("cineraResults"); +Search.IndexContainer = document.getElementById("cineraIndexList"); +Search.ProjectsContainer = Search.IndexContainer.querySelectorAll(".cineraIndexProject"); +// +/// + +// NOTE(matt): Initialisation +// +if(CineraProps.IsMobile) +{ + Nav.Nexus.classList.add("mobile"); +} +InitTraversalStack(); +InitNexus(); +InitHelpKeys(Nav.Controls.HelpDocumentation); +Nav.GridSize = ComputeOptimalGridSize(); +SetHelpKeyAvailability(Nav.GridSize); +InitButtons(); // NOTE(matt): Also does "keydown" listeners, needed before UpdateButtons() +UpdateButtons(); + +InitQuery(Search.QueryElement); +InitPrototypes(Search.ResultsContainer); +prepareProjects(); + +SyncNavState(); +// +//// + +// NOTE(matt): Listeners +// +BindControls(); +InitResizeEventListener(); +InitOrientationChangeListener(); +InitScrollEventListener(Nav.Nexus); +// +//// + +// NOTE(matt): On-load Execution +// +runSearch(); +// +//// diff --git a/cinera/cinera_search_pre.js b/cinera/cinera_search_pre.js new file mode 100644 index 0000000..10d1c56 --- /dev/null +++ b/cinera/cinera_search_pre.js @@ -0,0 +1,3654 @@ +// Processing (Searching / Filtering) +// +var CineraProps = { + IsMobile: IsMobile(), + Orientation: window.orientation, + +}; + +var Search = { + ResultsSummary: null, + ResultsContainer: null, + ProjectsContainer: null, + QueryElement: null, + + Projects: [], + + LastQuery: null, + MarkerList: null, + ProjectContainer: null, + ResultsMarkerIndex: -1, + RenderHandle: undefined, + Rendering: false, + + Prototypes: { + ProjectContainer: null, + DayContainer: null, + Marker: null, + Highlight: null, + } +}; + +function hideEntriesOfProject(ProjectElement) +{ + if(!ProjectElement.classList.contains("off")) + { + ProjectElement.classList.add("off"); + } + var baseURL = ProjectElement.attributes.getNamedItem("data-baseURL").value; + var searchLocation = ProjectElement.attributes.getNamedItem("data-searchLocation").value; + var playerLocation = ProjectElement.attributes.getNamedItem("data-playerLocation").value; + for(var i = 0; i < Search.Projects.length; ++i) + { + var ThisProject = Search.Projects[i]; + if(baseURL === ThisProject.baseURL && searchLocation === ThisProject.searchLocation && playerLocation === ThisProject.playerLocation) + { + ThisProject.filteredOut = true; + if(ThisProject.entriesContainer != null) + { + ThisProject.entriesContainer.style.display = "none"; + disableSprite(ThisProject.entriesContainer.parentElement); + } + } + } +} + +function showEntriesOfProject(ProjectElement) +{ + if(ProjectElement.classList.contains("off")) + { + ProjectElement.classList.remove("off"); + } + var baseURL = ProjectElement.attributes.getNamedItem("data-baseURL").value; + var searchLocation = ProjectElement.attributes.getNamedItem("data-searchLocation").value; + var playerLocation = ProjectElement.attributes.getNamedItem("data-playerLocation").value; + for(var i = 0; i < Search.Projects.length; ++i) + { + var ThisProject = Search.Projects[i]; + if(baseURL === ThisProject.baseURL && searchLocation === ThisProject.searchLocation && playerLocation === ThisProject.playerLocation) + { + ThisProject.filteredOut = false; + if(ThisProject.entriesContainer != null) + { + ThisProject.entriesContainer.style.display = "flex"; + enableSprite(ThisProject.entriesContainer.parentElement); + } + } + } +} + +function hideProjectSearchResults(baseURL, searchLocation, playerLocation) +{ + var cineraResults = document.getElementById("cineraResults"); + if(cineraResults) + { + var cineraResultsProjects = cineraResults.querySelectorAll(".projectContainer"); + for(var i = 0; i < cineraResultsProjects.length; ++i) + { + var resultBaseURL = cineraResultsProjects[i].attributes.getNamedItem("data-baseURL").value; + var resultSearchLocation = cineraResultsProjects[i].attributes.getNamedItem("data-searchLocation").value; + var resultPlayerLocation = cineraResultsProjects[i].attributes.getNamedItem("data-playerLocation").value; + if(baseURL === resultBaseURL && searchLocation === resultSearchLocation && playerLocation === resultPlayerLocation) + { + cineraResultsProjects[i].style.display = "none"; + return; + } + } + } +} + +function showProjectSearchResults(baseURL, searchLocation, playerLocation) +{ + var cineraResults = document.getElementById("cineraResults"); + if(cineraResults) + { + var cineraResultsProjects = cineraResults.querySelectorAll(".projectContainer"); + for(var i = 0; i < cineraResultsProjects.length; ++i) + { + var resultBaseURL = cineraResultsProjects[i].attributes.getNamedItem("data-baseURL").value; + var resultSearchLocation = cineraResultsProjects[i].attributes.getNamedItem("data-searchLocation").value; + var resultPlayerLocation = cineraResultsProjects[i].attributes.getNamedItem("data-playerLocation").value; + if(baseURL === resultBaseURL && searchLocation === resultSearchLocation && playerLocation === resultPlayerLocation) + { + cineraResultsProjects[i].style.display = "flex"; + return; + } + } + } +} + +function toggleEntriesOfProjectAndChildren(ProjectFilterElement) +{ + var baseURL = ProjectFilterElement.attributes.getNamedItem("data-baseURL").value; + var searchLocation = ProjectFilterElement.attributes.getNamedItem("data-searchLocation").value; + var playerLocation = ProjectFilterElement.attributes.getNamedItem("data-playerLocation").value; + var shouldShow = ProjectFilterElement.classList.contains("off"); + if(shouldShow) + { + ProjectFilterElement.classList.remove("off"); + enableSprite(ProjectFilterElement); + } + else + { + ProjectFilterElement.classList.add("off"); + disableSprite(ProjectFilterElement); + } + + for(var i = 0; i < Search.Projects.length; ++i) + { + var ThisProject = Search.Projects[i]; + + if(baseURL === ThisProject.baseURL && searchLocation === ThisProject.searchLocation && playerLocation === ThisProject.playerLocation) + { + if(shouldShow) + { + ThisProject.filteredOut = false; + enableSprite(ThisProject.projectTitleElement.parentElement); + if(ThisProject.entriesContainer != null) + { + ThisProject.entriesContainer.style.display = "flex"; + } + showProjectSearchResults(ThisProject.baseURL, ThisProject.searchLocation, ThisProject.playerLocation); + } + else + { + ThisProject.filteredOut = true; + disableSprite(ThisProject.projectTitleElement.parentElement); + if(ThisProject.entriesContainer != null) + { + ThisProject.entriesContainer.style.display = "none"; + } + hideProjectSearchResults(ThisProject.baseURL, ThisProject.searchLocation, ThisProject.playerLocation); + } + } + } + + var indexChildFilterProjects = ProjectFilterElement.querySelectorAll(".cineraFilterProject"); + + for(var j = 0; j < indexChildFilterProjects.length; ++j) + { + var ThisElement = indexChildFilterProjects[j]; + var baseURL = ThisElement.attributes.getNamedItem("data-baseURL").value; + var searchLocation = ThisElement.attributes.getNamedItem("data-searchLocation").value; + var playerLocation = ThisElement.attributes.getNamedItem("data-playerLocation").value; + if(shouldShow) + { + showEntriesOfProject(ThisElement); + showProjectSearchResults(baseURL, searchLocation, playerLocation); + } + else + { + hideEntriesOfProject(ThisElement); + hideProjectSearchResults(baseURL, searchLocation, playerLocation); + } + } +} + +function prepareToParseIndexFile(project) +{ + project.xhr.addEventListener("load", function() { + var contents = project.xhr.response; + var lines = contents.split("\n"); + var mode = "none"; + var episode = null; + for (var i = 0; i < lines.length; ++i) { + var line = lines[i]; + if (line.trim().length == 0) { continue; } + if (line == "---") { + if (episode != null && episode.filename != null && episode.number != null && episode.title != null) { + episode.day = getEpisodeName(project.unit, episode.number); + episode.dayContainerPrototype = project.dayContainerPrototype; + episode.markerPrototype = Search.Prototypes.Marker; + episode.playerURLPrefix = project.playerURLPrefix; + project.episodes.push(episode); + } + episode = {}; + mode = "none"; + } else if (line.startsWith("location:")) { + episode.filename = line.slice(10); + } else if (line.startsWith("number:")) { + episode.number = line.slice(8).trim().slice(1, -1); + } else if (line.startsWith("title:")) { + episode.title = line.slice(7).trim().slice(1, -1); + } else if (line.startsWith("markers")) { + mode = "markers"; + episode.markers = []; + } else if (mode == "markers") { + var match = line.match(/"(\d+)": "(.+)"/); + if (match == null) { + console.log(name, line); + } else { + var totalTime = parseInt(line.slice(1)); + var marker = { + totalTime: totalTime, + prettyTime: markerTime(totalTime), + text: match[2].replace(/\\"/g, "\"") + } + episode.markers.push(marker); + } + } + } + document.querySelector(".spinner").classList.remove("show"); + project.parsed = true; + runSearch(true); + }); + project.xhr.addEventListener("error", function() { + console.error("Failed to load content"); + }); +} + +function prepareProjects() +{ + for(var i = 0; i < Search.ProjectsContainer.length; ++i) + { + var ID = Search.ProjectsContainer[i].attributes.getNamedItem("data-project").value; + var baseURL = Search.ProjectsContainer[i].attributes.getNamedItem("data-baseURL").value; + var searchLocation = Search.ProjectsContainer[i].attributes.getNamedItem("data-searchLocation").value; + var playerLocation = Search.ProjectsContainer[i].attributes.getNamedItem("data-playerLocation").value; + var unit = Search.ProjectsContainer[i].attributes.getNamedItem("data-unit").value; + var theme = Search.ProjectsContainer[i].classList.item(1); + + Search.Projects[i] = + { + baseURL: baseURL, + searchLocation: searchLocation, + playerLocation: playerLocation, + unit: unit, + playerURLPrefix: (baseURL ? baseURL + "/" : "") + (playerLocation ? playerLocation + "/" : ""), + indexLocation: (baseURL ? baseURL + "/" : "") + (searchLocation ? searchLocation + "/" : "") + ID + ".index", + projectTitleElement: Search.ProjectsContainer[i].querySelector(":scope > .cineraProjectTitle"), + entriesContainer: Search.ProjectsContainer[i].querySelector(":scope > .cineraIndexEntries"), + dayContainerPrototype: Search.Prototypes.DayContainer.cloneNode(true), + filteredOut: false, + parsed: false, + searched: false, + resultsToRender: [], + resultsIndex: 0, + theme: theme, + episodes: [], + xhr: new XMLHttpRequest(), + } + + Search.Projects[i].dayContainerPrototype.classList.add(theme); + Search.Projects[i].dayContainerPrototype.children[1].classList.add(theme); + + document.querySelector(".spinner").classList.add("show"); + Search.Projects[i].xhr.open("GET", Search.Projects[i].indexLocation); + Search.Projects[i].xhr.setRequestHeader("Content-Type", "text/plain"); + Search.Projects[i].xhr.send(); + prepareToParseIndexFile(Search.Projects[i]); + } +} + +function getEpisodeName(unit, number) { + var day = null; + if(unit) + { + day = unit + " " + number; + } + return day; +} + +function markerTime(totalTime) { + var markTime = "("; + var hours = Math.floor(totalTime / 60 / 60); + var minutes = Math.floor(totalTime / 60) % 60; + var seconds = totalTime % 60; + if (hours > 0) { + markTime += padTimeComponent(hours) + ":"; + } + + markTime += padTimeComponent(minutes) + ":" + padTimeComponent(seconds) + ")"; + + return markTime; +} + +function padTimeComponent(component) { + return (component < 10 ? "0" + component : component); +} + +function resetProjectsForSearch() +{ + for(var i = 0; i < Search.Projects.length; ++i) + { + var project = Search.Projects[i]; + project.searched = false; + project.resultsToRender = []; + } +} + +function +IsQuery() +{ + return Search.LastQuery && Search.LastQuery.length > 0; +} + +function runSearch(refresh) { + var queryStr = document.getElementById("query").value; + if (refresh || Search.LastQuery != queryStr) { + var oldResultsContainer = Search.ResultsContainer; + Search.ResultsContainer = oldResultsContainer.cloneNode(false); + oldResultsContainer.parentNode.insertBefore(Search.ResultsContainer, oldResultsContainer); + oldResultsContainer.remove(); + for(var i = 0; i < Search.Projects.length; ++i) + { + Search.Projects[i].resultsIndex = 0; + } + Search.ResultsMarkerIndex = -1; + } + Search.LastQuery = queryStr; + + resetProjectsForSearch(); + + var numEpisodes = 0; + var numMarkers = 0; + var totalSeconds = 0; + + // NOTE(matt): Function defined within runSearch() so that we can modify numEpisodes, numMarkers and totalSeconds + function runSearchInterior(resultsToRender, query, episode) + { + var matches = []; + for (var k = 0; k < episode.markers.length; ++k) { + query.lastIndex = 0; + var result = query.exec(episode.markers[k].text); + if (result && result[0].length > 0) { + numMarkers++; + matches.push(episode.markers[k]); + if (k < episode.markers.length-1) { + totalSeconds += episode.markers[k+1].totalTime - episode.markers[k].totalTime; + } + } + } + if (matches.length > 0) { + numEpisodes++; + resultsToRender.push({ + query: query, + episode: episode, + matches: matches + }); + } + } + + if (IsQuery()) { + switch(Nav.ViewType) + { + case view_type.LIST: + { + Nav.List.classList.add("hidden"); + } break; + case view_type.GRID: + { + Nav.GridContainer.classList.add("hidden"); + } break; + } + Search.ResultsSummary.style.display = "block"; + var shouldRender = false; + var query = new RegExp(Search.LastQuery.replace("(", "\\(").replace(")", "\\)").replace(/\|+/, "\|").replace(/\|$/, "").replace(/(^|[^\\])\\$/, "$1"), "gi"); + + // Visible + for(var i = 0; i < Search.Projects.length; ++i) + { + var project = Search.Projects[i]; + if(project.parsed && !project.filteredOut && project.episodes.length > 0) { + if(Nav.SortChronological) + { + for(var j = 0; j < project.episodes.length; ++j) { + var episode = project.episodes[j]; + runSearchInterior(project.resultsToRender, query, episode); + } + } + else + { + for(var j = project.episodes.length; j > 0; --j) { + var episode = project.episodes[j - 1]; + runSearchInterior(project.resultsToRender, query, episode); + } + } + + shouldRender = true; + project.searched = true; + + } + } + + // Invisible + for(var i = 0; i < Search.Projects.length; ++i) + { + var project = Search.Projects[i]; + if(project.parsed && project.filteredOut && !project.searched && project.episodes.length > 0) { + if(Nav.SortChronological) + { + for(var j = 0; j < project.episodes.length; ++j) { + var episode = project.episodes[j]; + runSearchInterior(project.resultsToRender, query, episode); + } + } + else + { + for(var j = project.episodes.length; j > 0; --j) { + var episode = project.episodes[j - 1]; + runSearchInterior(project.resultsToRender, query, episode); + } + } + + shouldRender = true; + project.searched = true; + + } + } + + if(shouldRender) + { + if (Search.Rendering) { + clearTimeout(Search.RenderHandle); + } + renderResults(); + } + } + else + { + switch(Nav.ViewType) + { + case view_type.LIST: + { + Nav.List.classList.remove("hidden"); + } break; + case view_type.GRID: + { + Nav.GridContainer.classList.remove("hidden"); + } break; + } + Search.ResultsSummary.style.display = "none"; + } + + var totalTime = Math.floor(totalSeconds/60/60) + "h " + Math.floor(totalSeconds/60)%60 + "m " + totalSeconds%60 + "s "; + + Search.ResultsSummary.textContent = "Found: " + numEpisodes + " episodes, " + numMarkers + " markers, " + totalTime + "total."; +} + +function renderResults() { + var maxItems = 42; + var numItems = 0; + for(var i = 0; i < Search.Projects.length; ++i) + { + var project = Search.Projects[i]; + if (project.resultsIndex < project.resultsToRender.length) { + Search.Rendering = true; + while (numItems < maxItems && project.resultsIndex < project.resultsToRender.length) { + var query = project.resultsToRender[project.resultsIndex].query; + var episode = project.resultsToRender[project.resultsIndex].episode; + var matches = project.resultsToRender[project.resultsIndex].matches; + if (Search.ResultsMarkerIndex == -1) { + if(project.resultsIndex == 0 || project.resultsToRender[project.resultsIndex - 1].episode.playerURLPrefix != episode.playerURLPrefix) + { + Search.ProjectContainer = Search.Prototypes.ProjectContainer.cloneNode(true); + for(var i = 0; i < Search.Projects.length; ++i) + { + if(Search.Projects[i].playerURLPrefix === episode.playerURLPrefix) + { + Search.ProjectContainer.setAttribute("data-baseURL", Search.Projects[i].baseURL); + Search.ProjectContainer.setAttribute("data-searchLocation", Search.Projects[i].searchLocation); + Search.ProjectContainer.setAttribute("data-playerLocation", Search.Projects[i].playerLocation); + if(Search.Projects[i].filteredOut) + { + Search.ProjectContainer.style.display = "none"; + } + } + } + Search.ResultsContainer.appendChild(Search.ProjectContainer); + } + else + { + Search.ProjectContainer = Search.ResultsContainer.lastElementChild; + } + + + var dayContainer = episode.dayContainerPrototype.cloneNode(true); + var dayName = dayContainer.children[0]; + Search.MarkerList = dayContainer.children[1]; + + // TODO(matt): Maybe prepend the entire lineage? + dayName.textContent = (project.projectTitleElement.textContent ? project.projectTitleElement.textContent + " / " : "") + (episode.day ? episode.day + ": " : "") + episode.title; + + Search.ProjectContainer.appendChild(dayContainer); + Search.ResultsMarkerIndex = 0; + numItems++; + } + + while (numItems < maxItems && Search.ResultsMarkerIndex < matches.length) { + var match = matches[Search.ResultsMarkerIndex]; + var marker = episode.markerPrototype.cloneNode(true); + marker.setAttribute("href", episode.playerURLPrefix + episode.filename.replace(/"/g, "") + "/#" + match.totalTime); + query.lastIndex = 0; + var cursor = 0; + var text = match.text; + var result = null; + marker.appendChild(document.createTextNode(match.prettyTime + " ")); + while (result = query.exec(text)) { + if (result.index > cursor) { + marker.appendChild(document.createTextNode(text.slice(cursor, result.index))); + } + var highlightEl = Search.Prototypes.Highlight.cloneNode(); + highlightEl.textContent = result[0]; + marker.appendChild(highlightEl); + cursor = result.index + result[0].length; + } + + if (cursor < text.length) { + marker.appendChild(document.createTextNode(text.slice(cursor, text.length))); + } + Search.MarkerList.appendChild(marker); + numItems++; + Search.ResultsMarkerIndex++; + } + + if (Search.ResultsMarkerIndex == matches.length) { + Search.ResultsMarkerIndex = -1; + project.resultsIndex++; + } + } + Search.RenderHandle = setTimeout(renderResults, 0); + } else { + Search.Rendering = false; + } + } +} + +function IsVisible(el) { + var xPos = 0; + var yPos = 0; + var Height = parseInt(getComputedStyle(el).height); + + while (el) { + if (el.tagName == "BODY") { + var xScroll = el.scrollLeft || document.documentElement.scrollLeft; + var yScroll = el.scrollTop || document.documentElement.scrollTop; + + xPos += (el.offsetLeft - xScroll + el.clientLeft) + yPos += (el.offsetTop - yScroll + el.clientTop) + } else { + xPos += (el.offsetLeft - el.scrollLeft + el.clientLeft); + yPos += (el.offsetTop - el.scrollTop + el.clientTop); + } + + el = el.offsetParent; + } + return ((xPos > 0 && xPos < window.innerWidth) && (yPos > 0 && yPos + Height < window.innerHeight)); +} + +function +InitQuery(QueryElement) +{ + if(location.hash && location.hash.length > 0) + { + var initialQuery = location.hash; + if(initialQuery[0] == "#") + { + initialQuery = initialQuery.slice(1); + } + QueryElement.value = decodeURIComponent(initialQuery); + } + + if(document.hasFocus() && IsVisible(QueryElement)) { QueryElement.focus(); } +} + +function +InitPrototypes(ResultsContainer) +{ + Search.Prototypes.ProjectContainer = document.createElement("DIV"); + Search.Prototypes.ProjectContainer.classList.add("projectContainer"); + + Search.Prototypes.DayContainer = document.createElement("DIV"); + Search.Prototypes.DayContainer.classList.add("dayContainer"); + + var DayName = document.createElement("SPAN"); + DayName.classList.add("dayName"); + Search.Prototypes.DayContainer.appendChild(DayName); + + var MarkerList = document.createElement("DIV"); + MarkerList.classList.add("markerList"); + Search.Prototypes.DayContainer.appendChild(MarkerList); + + Search.Prototypes.Marker = document.createElement("A"); + Search.Prototypes.Marker.classList.add("marker"); + if(ResultsContainer.getAttribute("data-single") == 0) + { + Search.Prototypes.Marker.setAttribute("target", "_blank"); + } + + Search.Prototypes.Highlight = document.createElement("B"); +} +// +// Processing (Searching / Filtering) + +// Presenting / Navigating (Laying out and traversing the grid, and sorting) +// +var state_bit = +{ + DISABLE_ANIMATIONS: 1 << 0, + SORT_REVERSED: 1 << 1, + VIEW_LIST: 1 << 2, + VIEW_GRID: 1 << 3, + + NO_SAVE: 1 << 31, +}; + +var siblings = +{ + PREV: 0, + NEXT: 1, +}; + +var item_type = +{ + PROJECT: 0, + ENTRY: 1, +}; + +var view_type = +{ + LIST: 0, + GRID: 1, +}; + +var transition_type = +{ + SIBLING_SHIFT_PREV: 0, + SIBLING_SHIFT_NEXT: 1, + PROJECT_ENTRY: 2, + PROJECT_EXIT: 3, + SUBDIVISION_DESCENT: 4, + SUBDIVISION_ASCENT: 5, +}; + +var interaction_type = +{ + PUSH_BUTTON: 0, + SIBLING_SHIFT_PREV: 1, + SIBLING_SHIFT_NEXT: 2, + ASCEND: 3, + SORT: 4, +}; + +var Nav = { + Nexus: null, + GridContainer: null, + ButtonsContainer: null, + GridSize: null, + GridWidth: null, + GridColumnGap: null, + GridRowGap: null, + + SortChronological: true, + ViewType: view_type.GRID, + List: null, + Grid: null, + + // NOTE(matt): Controls + Controls: { + Header: null, + Sort: null, + View: null, + Anim: null, + Save: null, + + Help: null, + HelpDocumentation: null, + HelpKeys: [], + + GridTraversal: { + Header: null, + Ascend: null, + Prev: null, + PrevAscends: false, + Next: null, + NextAscends: false, + }, + }, + + Buttons: [], + + Transition: { + Enabled: true, + ButtonsTransitionContainer: null, + ButtonsContainerCloneElement: null, + RelevantButtonElement: null, + + StageDurations: [], + + Transforms: { + ButtonsTransitionContainer: { + Initial: { + Pos: { X: 0, Y: 0, }, + Scale: { X: 1, Y: 1, }, + Rotation: { X: 0, Y: 0, Z: 0, }, + Opacity: 1, + ScrollX: 0, + ZIndex: 0, + }, + Current: { + Pos: { X: 0, Y: 0, }, + Scale: { X: 1, Y: 1, }, + Rotation: { X: 0, Y: 0, Z: 0, }, + Opacity: 1, + ScrollX: 0, + ZIndex: 0, + }, + TargetStages: [], + }, + ButtonsContainer: { + Initial: { + Pos: { X: 0, Y: 0, }, + Scale: { X: 1, Y: 1, }, + Rotation: { X: 0, Y: 0, Z: 0, }, + Opacity: 1, + ScrollX: 0, + ZIndex: 0, + }, + Current: { + Pos: { X: 0, Y: 0, }, + Scale: { X: 1, Y: 1, }, + Rotation: { X: 0, Y: 0, Z: 0, }, + Opacity: 1, + ScrollX: 0, + ZIndex: 0, + }, + TargetStages: [], + }, + ButtonsContainerClone: { + Initial: { + Pos: { X: 0, Y: 0, }, + Scale: { X: 1, Y: 1, }, + Rotation: { X: 0, Y: 0, Z: 0, }, + Opacity: 1, + ScrollX: 0, + ZIndex: 0, + }, + Current: { + Pos: { X: 0, Y: 0, }, + Scale: { X: 1, Y: 1, }, + Rotation: { X: 0, Y: 0, Z: 0, }, + Opacity: 1, + ScrollX: 0, + ZIndex: 0, + }, + TargetStages: [], + }, + RelevantButton: { + Initial: { + Pos: { X: 0, Y: 0, }, + Scale: { X: 1, Y: 1, }, + Rotation: { X: 0, Y: 0, Z: 0, }, + Opacity: 1, + ScrollX: 0, + ZIndex: 0, + }, + Current: { + Pos: { X: 0, Y: 0, }, + Scale: { X: 1, Y: 1, }, + Rotation: { X: 0, Y: 0, Z: 0, }, + Opacity: 1, + ScrollX: 0, + ZIndex: 0, + }, + TargetStages: [], + }, + }, + + StartTime: undefined, + RequestedFrame: undefined, + }, + + InteractionQueue: [], + TraversalStack: [], + State: null, +}; + +function +StateBitIsSet(Bit) +{ + return Nav.State & Bit; +} + +function +StateBitIsClear(Bit) +{ + return !(Nav.State & Bit); +} + +function +MaintainingState() +{ + return StateBitIsClear(state_bit.NO_SAVE); +} + +function +SaveState() +{ + localStorage.setItem("CineraState", Nav.State); +} + +function +SetStateBit(Bit) +{ + if(MaintainingState()) { Nav.State |= Bit; SaveState(); } +} + +function +ClearStateBit(Bit) +{ + if(MaintainingState()) { Nav.State &= ~Bit; SaveState(); } +} + +function +SetHelpKeyAvailability(GridSize) +{ + for(var i = 0; i < Nav.Controls.HelpKeys.length; ++i) + { + Nav.Controls.HelpKeys[i].classList.remove("unavailable"); + } + + if(GridSize < 4) + { + Nav.Controls.HelpKeys[3].classList.add("unavailable"); + Nav.Controls.HelpKeys[7].classList.add("unavailable"); + Nav.Controls.HelpKeys[11].classList.add("unavailable"); + Nav.Controls.HelpKeys[12].classList.add("unavailable"); + Nav.Controls.HelpKeys[13].classList.add("unavailable"); + Nav.Controls.HelpKeys[14].classList.add("unavailable"); + Nav.Controls.HelpKeys[15].classList.add("unavailable"); + + if(GridSize < 3) + { + Nav.Controls.HelpKeys[2].classList.add("unavailable"); + Nav.Controls.HelpKeys[6].classList.add("unavailable"); + Nav.Controls.HelpKeys[8].classList.add("unavailable"); + Nav.Controls.HelpKeys[9].classList.add("unavailable"); + Nav.Controls.HelpKeys[10].classList.add("unavailable"); + } + } +} + +function +InitHelpKeys(HelpDocumentation) +{ + var Paragraph = HelpDocumentation.querySelector(".help_paragraph"); + Nav.Controls.HelpKeys = Paragraph.querySelectorAll(".help_key"); +} + +function +SyncNavState() +{ + Nav.State = localStorage.getItem("CineraState"); + if(Nav.State) + { + if(MaintainingState()) + { + if(StateBitIsSet(state_bit.DISABLE_ANIMATIONS)) { ToggleAnimations(); } + if(StateBitIsSet(state_bit.SORT_REVERSED)) { Sort(true); } + + // Nav.ViewType is initialised to view_type.GRID, so we needn't do anything if the Nav.State is also on Grid + if(StateBitIsSet(state_bit.VIEW_LIST)) { ToggleView(); } + } + else + { + Nav.Controls.Save.textContent = "Save Settings: ✘"; + } + } + else + { + Nav.State = 0; + SetStateBit(state_bit.VIEW_GRID); // NOTE(matt): Nav.ViewType was initialised to view_type.GRID + } +} + +function +InitTraversalStack() +{ + Nav.List = document.getElementById("cineraIndexList"); + + var Projects = Nav.List.querySelectorAll(":scope > .cineraIndexProject"); + + var Level = { + Projects: null, + Entries: null, + HeadIndex: null, + TailIndex: null, + } + + if(Projects.length === 1) + { + // NOTE(matt): Automatically descend into the lone project + Level.Projects = Projects[0].querySelectorAll(":scope > .cineraIndexProject"); + Level.Entries = Projects[0].querySelectorAll(":scope > .cineraIndexEntries > div"); + } + else + { + Level.Projects = Projects; + // NOTE(matt): The top-level "root" cannot itself contain any entries + } + + Nav.TraversalStack.push(Level); + console.log(Level); +} + +function +ComputeFullButtonItemCount(ParentItemCount, AvailableButtonCount) +{ + return ParentItemCount > 0 ? Math.ceil(ParentItemCount / AvailableButtonCount) : 0; +} + +function +EmptyElement(Element) +{ + while(Element.firstChild) { Element.removeChild(Element.firstChild); } +} + +function +EmptyAndResetButton(Button) +{ + EmptyElement(Button.Element); + Button.Element.style.fontSize = null; + Button.Element.style.fontWeight = null; + for(var i = 0; i < Button.Element.children.length; ++i) + { + Button.Element.children[i].style.fontSize = null; + Button.Element.children[i].style.fontWeight = null; + } + + for(var i = 0; i < Button.Element.classList.length;) + { + var Class = Button.Element.classList[i]; + if(Class != "cineraButton" && Class != "subdivision") + { + Button.Element.classList.remove(Class); + } + else + { + ++i; + } + } + + Button.Projects = null; + Button.Entries = null; + Button.HeadIndex = null; + Button.TailIndex = null; +} + +function +HasPrevSibling(Level) +{ + return Level.HeadIndex && Level.HeadIndex > 0; +} + +function +HasNextSibling(Level) +{ + return Level.TailIndex && Level.TailIndex < (Level.Entries ? Level.Entries.length : Level.Projects.length) - 1; +} + +function +Diff(A, B) +{ + return Math.abs(A - B); +} + +function +SetButtonInfo(NewButton, Prev, Level, Distribution) +{ + var Result = { + HeadIndex: null, + TailIndex: null, + ItemCount: null, + Theme: null, + } + var ItemsToPlace = null; + var FullButtonItemCount = null; + + if(Distribution.ProjectsToPlace) + { + ItemsToPlace = Distribution.ProjectsToPlace; + FullButtonItemCount = Distribution.FullButtonProjectCount; + } + else if(Distribution.EntriesToPlace) + { + ItemsToPlace = Distribution.EntriesToPlace; + FullButtonItemCount = Distribution.FullButtonEntryCount; + Result.Theme = Level.Entries[0].parentElement.parentElement.classList[1]; + } + + Result.ItemCount = ItemsToPlace > FullButtonItemCount ? FullButtonItemCount : ItemsToPlace; + if(Result.ItemCount > 0) + { + Result.HeadIndex = Prev ? Prev.TailIndex + 1 : Level.HeadIndex ? Level.HeadIndex : 0; + Result.TailIndex = Result.HeadIndex + Result.ItemCount - 1; + if(Result.ItemCount > 1) + { + if(Result.Theme == null) + { + Result.Theme = Level.Projects[Result.HeadIndex].classList[1]; + } + NewButton.HeadIndex = Result.HeadIndex; + NewButton.TailIndex = Result.TailIndex; + } + else if(Level.Projects && Level.Projects.length > 0) + { + if(Distribution.ProjectsToPlace) + { + NewButton.Projects = Level.Projects[Result.HeadIndex].querySelectorAll(":scope > .cineraIndexProject"); + Result.Theme = Level.Projects[Result.HeadIndex].classList[1]; + } + else + { + NewButton.Entries = Level.Projects[Result.HeadIndex].querySelectorAll(":scope > .cineraIndexEntries > div"); + } + } + } + return Result; +} + +function +ComputeItemDistribution(Level) +{ + var Result = { + ProjectsToPlace: Level.Projects ? Level.HeadIndex !== null && Level.TailIndex !== null ? Diff(Level.HeadIndex, Level.TailIndex) + 1 : Level.Projects.length : 0, + ButtonsForProjects: 0, + FullButtonProjectCount: 0, + + EntriesToPlace: Level.Entries ? Level.HeadIndex !== null && Level.TailIndex !== null ? Diff(Level.HeadIndex, Level.TailIndex) + 1 : Level.Entries.length : 0, + ButtonsForEntries: 0, + FullButtonEntryCount: 0, + }; + + // NOTE(matt): Assuming a square grid, reserving the top row for projects + Result.ButtonsForProjects = Math.min(Math.sqrt(Nav.Buttons.length), Result.ProjectsToPlace); + Result.ButtonsForEntries = Nav.Buttons.length - Result.ButtonsForProjects; + if(Result.EntriesToPlace < Result.ButtonsForEntries) + { + Result.ButtonsForProjects += (Result.ButtonsForEntries - Result.EntriesToPlace); + Result.ButtonsForEntries = Nav.Buttons.length - Result.ButtonsForProjects; + } + + Result.FullButtonProjectCount = ComputeFullButtonItemCount(Result.ProjectsToPlace, Result.ButtonsForProjects); + Result.FullButtonEntryCount = ComputeFullButtonItemCount(Result.EntriesToPlace, Result.ButtonsForEntries); + + return Result; +} + +function +ResetTransform(Transform, IgnoreSorting) +{ + Transform.Pos.X = 0; + Transform.Pos.Y = 0; + + Transform.Scale.X = 1; + Transform.Scale.Y = 1; + + Transform.Rotation.X = 0; + Transform.Rotation.Y = 0; + Transform.Rotation.Z = (Nav.SortChronological || IgnoreSorting) ? 0 : 180; + + Transform.Opacity = 1; + + Transform.ScrollX = 0; + + Transform.ZIndex = null; +} + +function +ResetButtonsContainerClone() +{ + EmptyElement(Nav.Transition.ButtonsContainerCloneElement); + + ResetTransform(Nav.Transition.Transforms.ButtonsTransitionContainer.Current, true); + ResetTransform(Nav.Transition.Transforms.ButtonsTransitionContainer.Initial, true); + ApplyTransform(Nav.Transition.ButtonsTransitionContainerElement, Nav.Transition.Transforms.ButtonsTransitionContainer.Current); + + ResetTransform(Nav.Transition.Transforms.ButtonsContainer.Current); + ResetTransform(Nav.Transition.Transforms.ButtonsContainer.Initial); + ApplyTransform(Nav.ButtonsContainer, Nav.Transition.Transforms.ButtonsContainer.Current); + + ResetTransform(Nav.Transition.Transforms.ButtonsContainerClone.Current); + ResetTransform(Nav.Transition.Transforms.ButtonsContainerClone.Initial); + ApplyTransform(Nav.Transition.ButtonsContainerCloneElement, Nav.Transition.Transforms.ButtonsContainerClone.Current); + + Nav.Transition.ButtonsContainerCloneElement.style.width = Nav.ButtonsContainer.style.width; + Nav.Transition.ButtonsContainerCloneElement.style.height = Nav.ButtonsContainer.style.height; + Nav.Transition.ButtonsContainerCloneElement.style.gridTemplateColumns = Nav.ButtonsContainer.style.gridTemplateColumns; + Nav.Transition.ButtonsContainerCloneElement.style.gridTemplateRows = Nav.ButtonsContainer.style.gridTemplateRows; + + Nav.Transition.ButtonsContainerCloneElement.style.paddingRight = null; + Nav.Transition.ButtonsContainerCloneElement.style.paddingLeft = null; + Nav.Transition.ButtonsContainerCloneElement.style.position = "absolute"; + + if(Nav.Transition.RelevantButtonElement) + { + ResetTransform(Nav.Transition.Transforms.RelevantButton.Current, true); + ResetTransform(Nav.Transition.Transforms.RelevantButton.Initial, true); + ApplyTransform(Nav.Transition.RelevantButtonElement, Nav.Transition.Transforms.RelevantButton.Current); + Nav.Transition.RelevantButtonElement = null; + } + + Nav.ButtonsContainer.style.zIndex = 1; + Nav.ButtonsContainer.style.order = 1; + Nav.Transition.ButtonsContainerCloneElement.style.order = 0; +} + +function +CloneButtonsContainer() +{ + ResetButtonsContainerClone(); + for(var i = 0; i < Nav.ButtonsContainer.children.length; ++i) + { + var ChildClone = Nav.ButtonsContainer.children[i].cloneNode(true); + Nav.Transition.ButtonsContainerCloneElement.appendChild(ChildClone); + } + + CopyTransform(Nav.Transition.Transforms.ButtonsContainerClone.Current, Nav.Transition.Transforms.ButtonsContainer.Current); + ApplyTransform(Nav.Transition.ButtonsContainerCloneElement, Nav.Transition.Transforms.ButtonsContainerClone.Current); + Nav.Transition.ButtonsContainerCloneElement.style.zIndex = 1; +} + +function +GetIndexOfElement(ParentNodeList, Element) +{ + var Result = null; + for(var i = 0; i < ParentNodeList.length; ++i) + { + if(Element == ParentNodeList[i]) + { + Result = i; + break; + } + } + return Result; +} + +function +GetIndexOfButton(Button) +{ + var Result = null; + for(var i = 0; i < Nav.Buttons.length; ++i) + { + if(Button == Nav.Buttons[i]) + { + Result = i + break; + } + } + return Result; +} + +function +ApplyTransform(Element, Transform) +{ + var TranslateStyle = "translate(" + Transform.Pos.X + "px, " + Transform.Pos.Y + "px)"; + var ScaleStyle = "scale(" + Transform.Scale.X + "," + Transform.Scale.Y + ")"; + var RotateX = "rotate3d(1, 0, 0, " + Transform.Rotation.X + "deg)"; + var RotateY = "rotate3d(0, 1, 0, " + Transform.Rotation.Y + "deg)"; + var RotateZ = "rotate3d(0, 0, 1, " + Transform.Rotation.Z + "deg)"; + var RotateStyle = RotateX + " " + RotateY + " " + RotateZ; + + var TransformString = TranslateStyle + " " + ScaleStyle + " " + RotateStyle; + + Element.style.transform = TransformString; + Element.style.opacity = Transform.Opacity; + Element.style.zIndex = Transform.ZIndex; + + if(Transform.ScrollX !== null) + { + Element.scrollLeft = Transform.ScrollX; + } +} + +function +CopyTransform(Dest, Src) +{ + Dest.Pos.X = Src.Pos.X; + Dest.Pos.Y = Src.Pos.Y; + + Dest.Scale.X = Src.Scale.X; + Dest.Scale.Y = Src.Scale.Y; + + Dest.Rotation.X = Src.Rotation.X; + Dest.Rotation.Y = Src.Rotation.Y; + Dest.Rotation.Z = Src.Rotation.Z; + + Dest.Opacity = Src.Opacity; + + Dest.ScrollX = Src.ScrollX; + + Dest.ZIndex = Src.ZIndex; +} + +function +TransformsMatch(Current, Target) +{ + var Result = true; + if((Target.Pos.X != null && Target.Pos.X != Current.Pos.X) || + (Target.Pos.Y != null && Target.Pos.Y != Current.Pos.Y) || + (Target.Scale.X != null && Target.Scale.X != Current.Scale.X) || + (Target.Scale.Y != null && Target.Scale.Y != Current.Scale.Y) || + (Target.Rotation.X != null && Target.Rotation.X != Current.Rotation.X) || + (Target.Rotation.Y != null && Target.Rotation.Y != Current.Rotation.Y) || + (Target.Rotation.Z != null && Target.Rotation.Z != Current.Rotation.Z) || + (Target.Opacity != null && Target.Opacity != Current.Opacity) || + (Target.ScrollX != null && Target.ScrollX != Current.ScrollX) || + (Target.ZIndex != null && Target.ZIndex != Current.ZIndex)) + { + Result = false; + } + return Result; +} + +function +TransformsComplete(TransformSet) +{ + var Result = true; + + var ButtonsContainer = TransformSet.ButtonsContainer; + var ButtonsContainerClone = TransformSet.ButtonsContainerClone; + var ButtonsTransitionContainer = TransformSet.ButtonsTransitionContainer; + var RelevantButton = TransformSet.RelevantButton; + + if((ButtonsContainer.TargetStages.length && !TransformsMatch(ButtonsContainer.Current, ButtonsContainer.TargetStages[0])) || + (ButtonsContainerClone.TargetStages.length && !TransformsMatch(ButtonsContainerClone.Current, ButtonsContainerClone.TargetStages[0])) || + (ButtonsTransitionContainer.TargetStages.length && !TransformsMatch(ButtonsTransitionContainer.Current, ButtonsTransitionContainer.TargetStages[0])) || + (RelevantButton.TargetStages.length && !TransformsMatch(RelevantButton.Current, RelevantButton.TargetStages[0]))) + { + Result = false; + } + return Result; +} + +function +FinaliseTransforms(TransformsSet) +{ + Nav.Transition.StartTime = undefined; + CopyTransform(TransformsSet.ButtonsTransitionContainer.Initial, TransformsSet.ButtonsTransitionContainer.Current); + CopyTransform(TransformsSet.ButtonsContainer.Initial, TransformsSet.ButtonsContainer.Current); + CopyTransform(TransformsSet.ButtonsContainerClone.Initial, TransformsSet.ButtonsContainerClone.Current); + CopyTransform(TransformsSet.RelevantButton.Initial, TransformsSet.RelevantButton.Current); + ShiftStage(); +} + +function +LerpTransforms(TransformSet, t) +{ + var Result = false; + if(TransformSet.TargetStages.length) + { + var Initial = TransformSet.Initial; + var Current = TransformSet.Current; + var Target = TransformSet.TargetStages[0]; + if(Target.Pos.X !== null) { Current.Pos.X = Lerp(Initial.Pos.X, t, Target.Pos.X); Result = true; } + if(Target.Pos.Y !== null) { Current.Pos.Y = Lerp(Initial.Pos.Y, t, Target.Pos.Y); Result = true; } + + if(Target.Scale.X !== null) { Current.Scale.X = Lerp(Initial.Scale.X, t, Target.Scale.X); Result = true; } + if(Target.Scale.Y !== null) { Current.Scale.Y = Lerp(Initial.Scale.Y, t, Target.Scale.Y); Result = true; } + + if(Target.Rotation.X !== null) { Current.Rotation.X = Lerp(Initial.Rotation.X, t, Target.Rotation.X); Result = true; } + if(Target.Rotation.Y !== null) { Current.Rotation.Y = Lerp(Initial.Rotation.Y, t, Target.Rotation.Y); Result = true; } + if(Target.Rotation.Z !== null) { Current.Rotation.Z = Lerp(Initial.Rotation.Z, t, Target.Rotation.Z); Result = true; } + + if(Target.Opacity !== null) { Current.Opacity = Lerp(Initial.Opacity, t, Target.Opacity); Result = true; } + + if(Target.ScrollX !== null) { Current.ScrollX = Lerp(Initial.ScrollX, t, Target.ScrollX); Result = true; } + + if(Target.ZIndex !== null) { Current.ZIndex = Lerp(Initial.ZIndex, t, Target.ZIndex); Result = true; } + } + return Result; +} + +function +ShiftStage() +{ + if(Nav.Transition.StageDurations.length) { Nav.Transition.StageDurations.shift(); } + if(Nav.Transition.Transforms.ButtonsTransitionContainer.TargetStages.length) { Nav.Transition.Transforms.ButtonsTransitionContainer.TargetStages.shift(); } + if(Nav.Transition.Transforms.ButtonsContainer.TargetStages.length) { Nav.Transition.Transforms.ButtonsContainer.TargetStages.shift(); } + if(Nav.Transition.Transforms.ButtonsContainerClone.TargetStages.length) { Nav.Transition.Transforms.ButtonsContainerClone.TargetStages.shift(); } + if(Nav.Transition.Transforms.RelevantButton.TargetStages.length) { Nav.Transition.Transforms.RelevantButton.TargetStages.shift(); } +} + +function +MergeTransform(Dest, Src) +{ + if(Src.Pos.X !== null) { Dest.Pos.X = Src.Pos.X; } + if(Src.Pos.Y !== null) { Dest.Pos.Y = Src.Pos.Y; } + + if(Src.Scale.X !== null) { Dest.Scale.X = Src.Scale.X; } + if(Src.Scale.Y !== null) { Dest.Scale.Y = Src.Scale.Y; } + + if(Src.Rotation.X !== null) { Dest.Rotation.X = Src.Rotation.X; } + if(Src.Rotation.Y !== null) { Dest.Rotation.Y = Src.Rotation.Y; } + if(Src.Rotation.Z !== null) { Dest.Rotation.Z = Src.Rotation.Z; } + + if(Src.Opacity !== null) { Dest.Opacity = Src.Opacity; } + + if(Src.ScrollX !== null) { Dest.ScrollX = Src.ScrollX; } + + if(Src.ZIndex !== null) { Dest.ZIndex = Src.ZIndex; } +} + +function +DoTransitionStage(Now) +{ + if(Nav.Transition.StageDurations.length) + { + if(Nav.Transition.StartTime === undefined) + { + Nav.Transition.StartTime = Now; + } + + var Elapsed = Now - Nav.Transition.StartTime; + var Duration = Nav.Transition.StageDurations[0]; + if(Duration === 0) + { + // Instant transform + if(Nav.Transition.Transforms.ButtonsTransitionContainer.TargetStages.length) + { + MergeTransform(Nav.Transition.Transforms.ButtonsTransitionContainer.Current, Nav.Transition.Transforms.ButtonsTransitionContainer.TargetStages[0]); + ApplyTransform(Nav.Transition.ButtonsTransitionContainerElement, Nav.Transition.Transforms.ButtonsTransitionContainer.Current); + } + if(Nav.Transition.Transforms.ButtonsContainer.TargetStages.length) + { + MergeTransform(Nav.Transition.Transforms.ButtonsContainer.Current, Nav.Transition.Transforms.ButtonsContainer.TargetStages[0]); + ApplyTransform(Nav.ButtonsContainer, Nav.Transition.Transforms.ButtonsContainer.Current); + } + if(Nav.Transition.Transforms.ButtonsContainerClone.TargetStages.length) + { + MergeTransform(Nav.Transition.Transforms.ButtonsContainerClone.Current, Nav.Transition.Transforms.ButtonsContainerClone.TargetStages[0]); + ApplyTransform(Nav.Transition.ButtonsContainerCloneElement, Nav.Transition.Transforms.ButtonsContainerClone.Current); + } + if(Nav.Transition.Transforms.RelevantButton.TargetStages.length) + { + MergeTransform(Nav.Transition.Transforms.RelevantButton.Current, Nav.Transition.Transforms.RelevantButton.TargetStages[0]); + ApplyTransform(Nav.Transition.RelevantButtonElement, Nav.Transition.Transforms.RelevantButton.Current); + } + } + else + { + // Lerp + var t = Clamp01(Elapsed / Duration); + if(LerpTransforms(Nav.Transition.Transforms.ButtonsTransitionContainer, t)) + { + ApplyTransform(Nav.Transition.ButtonsTransitionContainerElement, Nav.Transition.Transforms.ButtonsTransitionContainer.Current); + } + if(LerpTransforms(Nav.Transition.Transforms.ButtonsContainer, t)) + { + ApplyTransform(Nav.ButtonsContainer, Nav.Transition.Transforms.ButtonsContainer.Current); + } + if(LerpTransforms(Nav.Transition.Transforms.ButtonsContainerClone, t)) + { + ApplyTransform(Nav.Transition.ButtonsContainerCloneElement, Nav.Transition.Transforms.ButtonsContainerClone.Current); + } + if(LerpTransforms(Nav.Transition.Transforms.RelevantButton, t)) + { + ApplyTransform(Nav.Transition.RelevantButtonElement, Nav.Transition.Transforms.RelevantButton.Current); + } + } + if(TransformsComplete(Nav.Transition.Transforms)) + { + FinaliseTransforms(Nav.Transition.Transforms); + } + Nav.Transition.RequestedFrame = window.requestAnimationFrame(DoTransitionStage); + } + else + { + Nav.Transition.RequestedFrame = undefined; + ResetButtonsContainerClone(); + DequeueInteraction(); + } +} + +function +CompressTransitionStages() +{ + while(Nav.Transition.StageDurations.length > 1) + { + Nav.Transition.StageDurations.shift() + } + Nav.Transition.StageDurations[0] = 0; + + while(Nav.Transition.Transforms.ButtonsTransitionContainer.TargetStages.length > 1) + { + Nav.Transition.Transforms.ButtonsTransitionContainer.TargetStages.shift(); + } + + while(Nav.Transition.Transforms.ButtonsContainer.TargetStages.length > 1) + { + Nav.Transition.Transforms.ButtonsContainer.TargetStages.shift(); + } + + while(Nav.Transition.Transforms.ButtonsContainerClone.TargetStages.length > 1) + { + Nav.Transition.Transforms.ButtonsContainerClone.TargetStages.shift(); + } + + while(Nav.Transition.Transforms.RelevantButton.TargetStages.length > 1) + { + Nav.Transition.Transforms.RelevantButton.TargetStages.shift(); + } +} + +function +DoTransition() +{ + if(Nav.Transition.Enabled == false) + { + CompressTransitionStages(); + } + + CopyTransform(Nav.Transition.Transforms.ButtonsTransitionContainer.Initial, Nav.Transition.Transforms.ButtonsTransitionContainer.Current); + CopyTransform(Nav.Transition.Transforms.ButtonsContainer.Initial, Nav.Transition.Transforms.ButtonsContainer.Current); + CopyTransform(Nav.Transition.Transforms.ButtonsContainerClone.Initial, Nav.Transition.Transforms.ButtonsContainerClone.Current); + CopyTransform(Nav.Transition.Transforms.RelevantButton.Initial, Nav.Transition.Transforms.RelevantButton.Current); + + Nav.Transition.StartTime = undefined; + Nav.Transition.RequestedFrame = window.requestAnimationFrame(DoTransitionStage); +} + +function +NodesMatch(A, B) +{ + var Result = false; + var i = 0; + if(A && B) + { + Result = true; + for(; i < A.length && i < B.length && A[i] == B[i];) + { + ++i; + } + if(i != A.length || i != B.length) + { + Result = false; + } + } + else if(!A && !B) + { + Result = true; + } + return Result; +} + +function +ButtonAndLevelMatch(Button, Level) +{ + var Result = true; + if(Button && Level) + { + var ButtonProjects = Button.Projects; + var ButtonEntries = Button.Entries; + if(Button.Projects && Button.Projects.length === undefined) + { + ButtonProjects = Button.Projects.querySelectorAll(":scope > .cineraIndexProject"); + ButtonEntries = Button.Projects.querySelectorAll(":scope > .cineraIndexEntries > div"); + if(ButtonProjects.length == 0) + { + ButtonProjects = null; + } + if(ButtonEntries.length == 0) + { + ButtonEntries = null; + } + } + + if(!NodesMatch(Level.Projects, ButtonProjects)) + { + Result = false; + } + + if(!NodesMatch(Level.Entries, ButtonEntries)) + { + Result = false; + } + + if(Button.HeadIndex != Level.HeadIndex) + { + Result = false; + } + if(Button.TailIndex != Level.TailIndex) + { + Result = false; + } + } + return Result; +} + +function +NullTarget() +{ + let Result = { + Pos: { X: null, Y: null, }, Scale: { X: null, Y: null, }, Rotation: { X: null, Y: null, Z: null, }, + Opacity: null, ScrollX: null, ZIndex: null, + }; + return Result; +} + +function +ComputeButtonGeometryRelativeToGrid(Button) +{ + var Result = { + Pos: { + X: null, + Y: null, + }, + Scale: { + X: null, + Y: null, + }, + }; + + var ButtonStyle = window.getComputedStyle(Button); + + var GridDimX = Nav.GridWidth; + var GridDimY = Nav.GridWidth; + + var ButtonDimX = parseInt(ButtonStyle.width); + var ButtonDimY = parseInt(ButtonStyle.height); + + Result.Scale.X = ButtonDimX / GridDimX; + Result.Scale.Y = ButtonDimY / GridDimY; + + var ButtonPosX = Button.offsetLeft; + var ButtonPosY = Button.offsetTop; + + if(!Nav.SortChronological) + { + ButtonPosX = GridDimX - ButtonPosX - ButtonDimX; + ButtonPosY = GridDimY - ButtonPosY - ButtonDimY; + } + + var ButtonCentreX = ButtonDimX / 2 + ButtonPosX; + var ButtonCentreY = ButtonDimY / 2 + ButtonPosY; + + var GridCentreX = GridDimX / 2; + var GridCentreY = GridDimY / 2; + + Result.Pos.X = ButtonCentreX - GridCentreX; + Result.Pos.Y = ButtonCentreY - GridCentreY; + + return Result; +} + +function +GetItemType(Level) +{ + var Result = null; + if(Level.Projects && Level.Projects.length !== undefined && Level.Projects.length > 0) + { + Result = item_type.PROJECT; + } + else if(Level.Entries && Level.Entries.length !== undefined && Level.Entries.length > 0) + { + Result = item_type.ENTRY; + } + return Result; +} + +function +GetTraversalLevelBundle() +{ + var Result = { + Generation: null, + This: null, + Parent: null, + Type: null + }; + Result.Generation = Nav.TraversalStack.length; + Result.This = Nav.TraversalStack[Result.Generation - 1]; + Result.Parent = Nav.TraversalStack[Result.Generation - 2]; + Result.Type = GetItemType(Result.This); + return Result; +} + +function +FitText(Element) +{ + if(Element.textContent.includes("Day 068") || Element.textContent.includes("Day 136")) + { + if(Element.textContent.includes("Day 068")) + { + Say("FitText(068)"); + } + else + { + Say("FitText(136)"); + } + var Style = window.getComputedStyle(Element); + Say("Height: " + Style.height); + var ParentStyle = window.getComputedStyle(Element.parentElement); + Say("Parent Height: " + ParentStyle.height); + } + + Element.style.alignItems = "flex-start"; + var FontSize = parseInt(window.getComputedStyle(Element).fontSize); + while(FontSize > 10 && IsOverflowed(Element)) + { + FontSize -= 0.2; + Element.style.fontSize = FontSize + "px"; + } + + if(IsOverflowed(Element)) + { + Element.style.fontWeight = "normal"; + } + Element.style.alignItems = null; +} + +function +UpdateButtons(TransitionType, RelevantButton, PoppedLevel) +{ + var LevelBundle = GetTraversalLevelBundle(); + + Nav.Controls.GridTraversal.Prev.children[0].textContent = "←"; + Nav.Controls.GridTraversal.PrevAscends = false; + Nav.Controls.GridTraversal.NextAscends = false; + if(LevelBundle.Generation <= 1) + { + Nav.Controls.GridTraversal.Ascend.classList.add("nowhere"); + Nav.Controls.GridTraversal.Prev.classList.add("nowhere"); + Nav.Controls.GridTraversal.Next.classList.add("nowhere"); + Nav.Controls.GridTraversal.Next.classList.remove("ascension"); + } + else + { + Nav.Controls.GridTraversal.Ascend.classList.remove("nowhere"); + Nav.Controls.GridTraversal.Prev.classList.remove("nowhere"); + Nav.Controls.GridTraversal.Next.classList.remove("nowhere"); + + if(!HasPrevSibling(LevelBundle.This)) + { + Nav.Controls.GridTraversal.Prev.classList.add("nowhere"); + } + else if(SiblingIsLeaf(siblings.PREV)) + { + Nav.Controls.GridTraversal.PrevAscends = true; + Nav.Controls.GridTraversal.Prev.children[0].textContent = "↰"; + } + + if(!HasNextSibling(LevelBundle.This)) + { + Nav.Controls.GridTraversal.Next.classList.add("nowhere"); + Nav.Controls.GridTraversal.Next.classList.remove("ascension"); + } + else if(SiblingIsLeaf(siblings.NEXT)) + { + Nav.Controls.GridTraversal.NextAscends = true; + Nav.Controls.GridTraversal.Next.classList.add("ascension"); + } + else + { + Nav.Controls.GridTraversal.Next.classList.remove("ascension"); + } + } + + var Distribution = ComputeItemDistribution(LevelBundle.This); + + // NOTE(matt): Centre-alignment. If people would prefer left-alignment, we do that here + // + // We're doing simple 1D centring here, so would need to do correct 2D centring + var HalfEmptyButtonCount = (Nav.Buttons.length - (Distribution.ProjectsToPlace + Distribution.EntriesToPlace)) / 2; + var EmptyPadding = 0; + //EmptyPadding = Math.floor(HalfEmptyButtonCount); // NOTE(matt): Comment out to disable centring + // + + var Prev = null; + var ButtonInfo = { HeadIndex: null, TailIndex: null, ItemCount: null, Theme: null, }; + var DoingEntries = false; + + if(TransitionType !== undefined) + { + CloneButtonsContainer(); + } + + for(var ButtonIndex = 0; ButtonIndex < Nav.Buttons.length; ++ButtonIndex) + { + let This = Nav.Buttons[ButtonIndex]; + EmptyAndResetButton(This); + if(EmptyPadding > 0) + { + --EmptyPadding; + } + else + { + if(Distribution.ProjectsToPlace > 0 || Distribution.EntriesToPlace > 0) + { + if(Distribution.ProjectsToPlace > 0) + { + This.Projects = LevelBundle.This.Projects; + if(Distribution.ProjectsToPlace == 1 || Distribution.ProjectsToPlace == Distribution.ButtonsForProjects - ButtonIndex) + { + Distribution.FullButtonProjectCount = 1; + } + + ButtonInfo = SetButtonInfo(This, Prev, LevelBundle.This, Distribution); + if(ButtonInfo.ItemCount == 1) + { + This.Projects = LevelBundle.This.Projects[ButtonInfo.HeadIndex]; + } + Distribution.ProjectsToPlace -= ButtonInfo.ItemCount; + + if(Distribution.FullButtonProjectCount == 1) + { + This.Element.classList.add("leaf"); + var TextElement = document.createElement("p"); + TextElement.classList.add("text"); + if(!Nav.SortChronological) + { + TextElement.style.transform = "rotate3d(0, 0, 1, 180deg)"; + } + var Text = LevelBundle.This.Projects[ButtonInfo.HeadIndex].querySelector(".cineraProjectTitle").innerText; + var TextNode = document.createTextNode(Text); + TextElement.appendChild(TextNode); + This.Element.appendChild(TextElement); + FitText(This.Element); + } + else + { + var HeadEntryElement = document.createElement("div"); + HeadEntryElement.classList.add("text"); + HeadEntryElement.classList.add("head-item"); + if(!Nav.SortChronological) + { + HeadEntryElement.style.transform = "rotate3d(0, 0, 1, 180deg)"; + } + var HeadEntry = This.Element.appendChild(HeadEntryElement); + + var TailEntryElement = document.createElement("div"); + TailEntryElement.classList.add("text"); + TailEntryElement.classList.add("tail-item"); + if(!Nav.SortChronological) + { + TailEntryElement.style.transform = "rotate3d(0, 0, 1, 180deg)"; + } + var TailEntry = This.Element.appendChild(TailEntryElement); + + var HeadText = LevelBundle.This.Projects[ButtonInfo.HeadIndex].querySelector(".cineraProjectTitle").innerText; + HeadEntry.textContent = HeadText; + + var TailText = LevelBundle.This.Projects[ButtonInfo.TailIndex].querySelector(".cineraProjectTitle").innerText; + TailEntry.textContent = TailText; + + FitText(HeadEntry); + FitText(TailEntry); + } + } + else + { + This.Entries = LevelBundle.This.Entries; + if(!DoingEntries) + { + Prev = null; + DoingEntries = true; + } + + if(Distribution.EntriesToPlace == 1 || Distribution.EntriesToPlace == Distribution.ButtonsForEntries - ButtonIndex) + { + Distribution.FullButtonEntryCount = 1; + } + + ButtonInfo = SetButtonInfo(This, Prev, LevelBundle.This, Distribution); + if(ButtonInfo.ItemCount == 1) + { + This.Entries = This.Entries[ButtonInfo.HeadIndex]; + } + Distribution.EntriesToPlace -= ButtonInfo.ItemCount; + + if(Distribution.FullButtonEntryCount == 1) + { + This.Element.classList.add("leaf"); + var ButtonLink = document.createElement("a"); + ButtonLink.classList.add("text"); + if(!Nav.SortChronological) + { + ButtonLink.style.transform = "rotate3d(0, 0, 1, 180deg)"; + } + var EntryAddress = LevelBundle.This.Entries[ButtonInfo.HeadIndex].lastElementChild.getAttribute("href"); + ButtonLink.setAttribute("href", EntryAddress); + var Text = LevelBundle.This.Entries[ButtonInfo.HeadIndex].innerText; + var TextNode = document.createTextNode(Text); + ButtonLink.appendChild(TextNode); + + This.Element.appendChild(ButtonLink); + FitText(This.Element); + } + else + { + var HeadEntryElement = document.createElement("div"); + HeadEntryElement.classList.add("text"); + HeadEntryElement.classList.add("head-item"); + var HeadEntry = This.Element.appendChild(HeadEntryElement); + HeadEntry.textContent = LevelBundle.This.Entries[ButtonInfo.HeadIndex].innerText; + FitText(HeadEntry); + if(!Nav.SortChronological) + { + HeadEntryElement.style.transform = "rotate3d(0, 0, 1, 180deg)"; + } + + var TailEntryElement = document.createElement("div"); + TailEntryElement.classList.add("text"); + TailEntryElement.classList.add("tail-item"); + var TailEntry = This.Element.appendChild(TailEntryElement); + TailEntry.textContent = LevelBundle.This.Entries[ButtonInfo.TailIndex].innerText; + FitText(TailEntry); + if(!Nav.SortChronological) + { + TailEntryElement.style.transform = "rotate3d(0, 0, 1, 180deg)"; + } + } + } + + This.Element.classList.add(ButtonInfo.Theme); + Prev = ButtonInfo; + } + else + { + This.Element.classList.add(Nav.Nexus.classList[0]); + } + } + if(PoppedLevel && !Nav.Transition.RelevantButtonElement && ButtonAndLevelMatch(This, PoppedLevel)) + { + Nav.Transition.RelevantButtonElement = This.Element; + } + } + if(TransitionType !== undefined) + { + switch(TransitionType) + { + case transition_type.SIBLING_SHIFT_PREV: + { + // Init targets + //// ButtonsTransitionContainer + let TargetA0 = NullTarget(); + var Padding = Nav.GridColumnGap; + if(Nav.SortChronological) + { + TargetA0.ScrollX = 0; + } + else + { + TargetA0.ScrollX = Nav.GridWidth + Padding; + } + + Nav.Transition.StageDurations.push(320); + Nav.Transition.Transforms.ButtonsTransitionContainer.TargetStages.push(TargetA0); + + // Prep + Nav.Transition.ButtonsContainerCloneElement.style.position = "relative"; + + var ScrollX; + if(Nav.SortChronological) + { + Nav.ButtonsContainer.style.order = 0; + Nav.Transition.ButtonsContainerCloneElement.style.order = 1; + ScrollX = Nav.GridWidth + Padding; + } + else + { + Nav.ButtonsContainer.style.order = 1; + Nav.Transition.ButtonsContainerCloneElement.style.order = 0; + ScrollX = 0; + } + + Nav.Transition.ButtonsContainerCloneElement.style.paddingLeft = Padding + "px"; + + Nav.Transition.Transforms.ButtonsTransitionContainer.Current.ScrollX = ScrollX; + ApplyTransform(Nav.Transition.ButtonsTransitionContainerElement, Nav.Transition.Transforms.ButtonsTransitionContainer.Current); + } break; + case transition_type.SIBLING_SHIFT_NEXT: + { + // Init targets + //// + let TargetA0 = NullTarget(); + var Padding = Nav.GridColumnGap; + if(Nav.SortChronological) + { + TargetA0.ScrollX = Nav.GridWidth + Padding; + } + else + { + TargetA0.ScrollX = 0; + } + + Nav.Transition.StageDurations.push(320); + Nav.Transition.Transforms.ButtonsTransitionContainer.TargetStages.push(TargetA0); + + // Prep + Nav.Transition.ButtonsContainerCloneElement.style.position = "relative"; + + var ScrollX; + + if(Nav.SortChronological) + { + Nav.ButtonsContainer.style.order = 1; + Nav.Transition.ButtonsContainerCloneElement.style.order = 0; + ScrollX = 0; + } + else + { + Nav.ButtonsContainer.style.order = 0; + Nav.Transition.ButtonsContainerCloneElement.style.order = 1; + ScrollX = Nav.GridWidth + Padding; + } + + Nav.Transition.ButtonsContainerCloneElement.style.paddingRight = Padding + "px"; + + Nav.Transition.Transforms.ButtonsTransitionContainer.Current.ScrollX = ScrollX; + ApplyTransform(Nav.Transition.ButtonsTransitionContainerElement, Nav.Transition.Transforms.ButtonsTransitionContainer.Current); + } break; + case transition_type.PROJECT_ENTRY: + { + // Init targets + //// ButtonsContainer + let TargetA0 = NullTarget(); + let TargetA1 = NullTarget(); + let TargetA2 = NullTarget(); + let TargetA3 = NullTarget(); + + if(Nav.SortChronological) + { + TargetA0.Rotation.Y = 90; + } + else + { + TargetA0.Rotation.Y = -90; + } + TargetA1.ZIndex = 1; + TargetA2.Rotation.Y = 0; + TargetA3.Pos.X = 0; + TargetA3.Pos.Y = 0; + TargetA3.Scale.X = 1; + TargetA3.Scale.Y = 1; + + //// RelevantButton + let TargetB0 = NullTarget(); + let TargetB1 = NullTarget(); + let TargetB2 = NullTarget(); + + if(Nav.SortChronological) + { + TargetB0.Rotation.Y = -90; + TargetB2.Rotation.Y = -180; + } + else + { + TargetB0.Rotation.Y = 90; + TargetB2.Rotation.Y = 180; + } + + //// ButtonsContainerClone + let TargetC0 = NullTarget(); + let TargetC1 = NullTarget(); + + TargetC1.ZIndex = 0; + + + Nav.Transition.StageDurations.push(80); + Nav.Transition.StageDurations.push(0); + Nav.Transition.StageDurations.push(80); + Nav.Transition.StageDurations.push(160); + + Nav.Transition.Transforms.ButtonsContainer.TargetStages.push(TargetA0); + Nav.Transition.Transforms.ButtonsContainer.TargetStages.push(TargetA1); + Nav.Transition.Transforms.ButtonsContainer.TargetStages.push(TargetA2); + Nav.Transition.Transforms.ButtonsContainer.TargetStages.push(TargetA3); + Nav.Transition.Transforms.RelevantButton.TargetStages.push(TargetB0); + Nav.Transition.Transforms.RelevantButton.TargetStages.push(TargetB1); + Nav.Transition.Transforms.RelevantButton.TargetStages.push(TargetB2); + Nav.Transition.Transforms.ButtonsContainerClone.TargetStages.push(TargetC0); + Nav.Transition.Transforms.ButtonsContainerClone.TargetStages.push(TargetC1); + + // Prep + var RelevantButtonIndex = GetIndexOfButton(RelevantButton); + Nav.Transition.RelevantButtonElement = Nav.Transition.ButtonsContainerCloneElement.children[RelevantButtonIndex]; + + var ButtonGeometry = ComputeButtonGeometryRelativeToGrid(Nav.Transition.RelevantButtonElement); + + Nav.Transition.Transforms.ButtonsContainer.Current.Pos.X = ButtonGeometry.Pos.X; + Nav.Transition.Transforms.ButtonsContainer.Current.Pos.Y = ButtonGeometry.Pos.Y; + Nav.Transition.Transforms.ButtonsContainer.Current.Scale.X = ButtonGeometry.Scale.X; + Nav.Transition.Transforms.ButtonsContainer.Current.Scale.Y = ButtonGeometry.Scale.Y; + if(Nav.SortChronological) + { + Nav.Transition.Transforms.ButtonsContainer.Current.Rotation.Y = 180; + } + else + { + Nav.Transition.Transforms.ButtonsContainer.Current.Rotation.Y = -180; + } + + Nav.Transition.Transforms.ButtonsContainer.Current.ZIndex = 0; + + Nav.Transition.Transforms.ButtonsContainerClone.Current.ZIndex = 1; + + ApplyTransform(Nav.Transition.ButtonsContainerCloneElement, Nav.Transition.Transforms.ButtonsContainerClone.Current); + ApplyTransform(Nav.ButtonsContainer, Nav.Transition.Transforms.ButtonsContainer.Current); + } break; + case transition_type.PROJECT_EXIT: + { + // Init targets + //// ButtonsContainer + let TargetA0 = NullTarget(); + let TargetA1 = NullTarget(); + let TargetA2 = NullTarget(); + TargetA2.ZIndex = 1; + + //// RelevantButton + let TargetB0 = NullTarget(); + let TargetB1 = NullTarget(); + let TargetB2 = NullTarget(); + let TargetB3 = NullTarget(); + + if(Nav.SortChronological) + { + TargetB1.Rotation.Y = -90; + } + else + { + TargetB1.Rotation.Y = 90; + } + TargetB3.Rotation.Y = 0; + + //// ButtonsContainerClone + let TargetC0 = NullTarget(); + let TargetC1 = NullTarget(); + let TargetC2 = NullTarget(); + let TargetC3 = NullTarget(); + + var ButtonGeometry = ComputeButtonGeometryRelativeToGrid(Nav.Transition.RelevantButtonElement); + TargetC0.Pos.X = ButtonGeometry.Pos.X; + TargetC0.Pos.Y = ButtonGeometry.Pos.Y; + TargetC0.Scale.X = ButtonGeometry.Scale.X; + TargetC0.Scale.Y = ButtonGeometry.Scale.Y; + if(Nav.SortChronological) + { + TargetC1.Rotation.Y = 90; + TargetC3.Rotation.Y = 180; + } + else + { + TargetC1.Rotation.Y = -90; + TargetC3.Rotation.Y = -180; + } + TargetC2.ZIndex = 0; + + + Nav.Transition.StageDurations.push(160); + Nav.Transition.StageDurations.push(80); + Nav.Transition.StageDurations.push(0); + Nav.Transition.StageDurations.push(80); + + Nav.Transition.Transforms.ButtonsContainer.TargetStages.push(TargetA0); + Nav.Transition.Transforms.ButtonsContainer.TargetStages.push(TargetA1); + Nav.Transition.Transforms.ButtonsContainer.TargetStages.push(TargetA2); + Nav.Transition.Transforms.RelevantButton.TargetStages.push(TargetB0); + Nav.Transition.Transforms.RelevantButton.TargetStages.push(TargetB1); + Nav.Transition.Transforms.RelevantButton.TargetStages.push(TargetB2); + Nav.Transition.Transforms.RelevantButton.TargetStages.push(TargetB3); + Nav.Transition.Transforms.ButtonsContainerClone.TargetStages.push(TargetC0); + Nav.Transition.Transforms.ButtonsContainerClone.TargetStages.push(TargetC1); + Nav.Transition.Transforms.ButtonsContainerClone.TargetStages.push(TargetC2); + Nav.Transition.Transforms.ButtonsContainerClone.TargetStages.push(TargetC3); + + // Prep + if(Nav.SortChronological) + { + Nav.Transition.Transforms.RelevantButton.Current.Rotation.Y = -180; + } + else + { + Nav.Transition.Transforms.RelevantButton.Current.Rotation.Y = 180; + } + ApplyTransform(Nav.Transition.RelevantButtonElement, Nav.Transition.Transforms.RelevantButton.Current); + + Nav.Transition.Transforms.ButtonsContainer.Current.ZIndex = 0; + ApplyTransform(Nav.ButtonsContainer, Nav.Transition.Transforms.ButtonsContainer.Current); + + Nav.Transition.Transforms.ButtonsContainerClone.Current.ZIndex = 1; + ApplyTransform(Nav.Transition.ButtonsContainerCloneElement, Nav.Transition.Transforms.ButtonsContainerClone.Current); + } break; + case transition_type.SUBDIVISION_DESCENT: + { + // Init targets + //// ButtonsContainer + let TargetA0 = NullTarget(); + let TargetA1 = NullTarget(); + + TargetA0.Opacity = 1; + TargetA1.Pos.X = 0; + TargetA1.Pos.Y = 0; + TargetA1.Scale.X = 1; + TargetA1.Scale.Y = 1; + + Nav.Transition.StageDurations.push(160); + Nav.Transition.StageDurations.push(160); + + Nav.Transition.Transforms.ButtonsContainer.TargetStages.push(TargetA0); + Nav.Transition.Transforms.ButtonsContainer.TargetStages.push(TargetA1); + + // Prep + var RelevantButtonIndex = GetIndexOfButton(RelevantButton); + Nav.Transition.RelevantButtonElement = Nav.Transition.ButtonsContainerCloneElement.children[RelevantButtonIndex]; + + var ButtonGeometry = ComputeButtonGeometryRelativeToGrid(Nav.Transition.RelevantButtonElement); + + Nav.Transition.Transforms.ButtonsContainer.Current.Pos.X = ButtonGeometry.Pos.X; + Nav.Transition.Transforms.ButtonsContainer.Current.Pos.Y = ButtonGeometry.Pos.Y; + Nav.Transition.Transforms.ButtonsContainer.Current.Scale.X = ButtonGeometry.Scale.X; + Nav.Transition.Transforms.ButtonsContainer.Current.Scale.Y = ButtonGeometry.Scale.Y; + Nav.Transition.Transforms.ButtonsContainer.Current.Opacity = 0; + Nav.Transition.Transforms.ButtonsContainer.Current.ZIndex = 1; + + Nav.Transition.Transforms.ButtonsContainerClone.Current.ZIndex = 0; + + ApplyTransform(Nav.Transition.ButtonsContainerCloneElement, Nav.Transition.Transforms.ButtonsContainerClone.Current); + ApplyTransform(Nav.ButtonsContainer, Nav.Transition.Transforms.ButtonsContainer.Current); + } break; + case transition_type.SUBDIVISION_ASCENT: + { + // Init targets + //// ButtonsContainer + let TargetA0 = NullTarget(); + let TargetA1 = NullTarget(); + let TargetA2 = NullTarget(); + TargetA2.ZIndex = 1; + + //// ButtonsContainerClone + let TargetC0 = NullTarget(); + let TargetC1 = NullTarget(); + let TargetC2 = NullTarget(); + + var ButtonGeometry = ComputeButtonGeometryRelativeToGrid(Nav.Transition.RelevantButtonElement); + TargetC0.Pos.X = ButtonGeometry.Pos.X; + TargetC0.Pos.Y = ButtonGeometry.Pos.Y; + TargetC0.Scale.X = ButtonGeometry.Scale.X; + TargetC0.Scale.Y = ButtonGeometry.Scale.Y; + TargetC1.Opacity = 0; + TargetC2.ZIndex = 0; + + + Nav.Transition.StageDurations.push(160); + Nav.Transition.StageDurations.push(160); + Nav.Transition.StageDurations.push(0); + + Nav.Transition.Transforms.ButtonsContainer.TargetStages.push(TargetA0); + Nav.Transition.Transforms.ButtonsContainer.TargetStages.push(TargetA1); + Nav.Transition.Transforms.ButtonsContainer.TargetStages.push(TargetA2); + Nav.Transition.Transforms.ButtonsContainerClone.TargetStages.push(TargetC0); + Nav.Transition.Transforms.ButtonsContainerClone.TargetStages.push(TargetC1); + Nav.Transition.Transforms.ButtonsContainerClone.TargetStages.push(TargetC2); + + // Prep + Nav.Transition.Transforms.ButtonsContainer.Current.ZIndex = 0; + Nav.Transition.Transforms.ButtonsContainerClone.Current.ZIndex = 1; + ApplyTransform(Nav.Transition.ButtonsContainerCloneElement, Nav.Transition.Transforms.ButtonsContainerClone.Current); + ApplyTransform(Nav.ButtonsContainer, Nav.Transition.Transforms.ButtonsContainer.Current); + } break; + } + DoTransition(); + } +} + +function +PushButton(ButtonElement, Button) +{ + var Level = { + Projects: Button.Projects, + Entries: Button.Entries, + HeadIndex: Button.HeadIndex, + TailIndex: Button.TailIndex, + }; + + var TransitionType = undefined; + if(Level.Projects !== null || Level.Entries !== null) + { + if(Level.Projects !== null) + { + if(Level.Projects.length === undefined) + { + var Entries = Level.Projects.querySelectorAll(":scope > .cineraIndexEntries > div"); + var Projects = Level.Projects.querySelectorAll(":scope > .cineraIndexProject"); + Level.Entries = Entries.length ? Entries : null; + Level.Projects = Projects.length ? Projects : null; + TransitionType = transition_type.PROJECT_ENTRY; + } + else + { + TransitionType = transition_type.SUBDIVISION_DESCENT; + } + Nav.TraversalStack.push(Level); + UpdateButtons(TransitionType, Button); + } + else + { + if(Level.Entries.length === undefined) + { + var Address = ButtonElement.lastElementChild.getAttribute("href"); + location = Address; + } + else + { + Nav.TraversalStack.push(Level); + TransitionType = transition_type.SUBDIVISION_DESCENT; + UpdateButtons(TransitionType, Button); + } + } + } +} + +function +RangeContains(Range, Target) +{ + return (Range.HeadIndex == null && Range.TailIndex == null) || (Range.HeadIndex <= Target && Range.TailIndex >= Target); +} + +function +LengthOf(Level) +{ + var Result = 0; + var Items = Level.Projects && Level.Projects.length > 0 ? Level.Projects : Level.Entries; + if(Items) + { + if(Level.HeadIndex !== null && Level.TailIndex !== null) + { + Result = Diff(Level.HeadIndex, Level.TailIndex) + 1; + } + else if(Items.length !== undefined) + { + Result = Items.length; + } + else + { + Result = 1; + } + } + return Result; +} + +function +DereferenceLevel(Generation) +{ + var Level = Nav.TraversalStack[Generation - 1]; + var Result = { + Projects: Level.Projects, + Entries: Level.Entries, + HeadIndex: Level.HeadIndex, + TailIndex: Level.TailIndex, + }; + return Result; +} + +function +GetSubdivisionFor(LevelBundle, Index, TargetGeneration) +{ + var Result = { + Projects: LevelBundle.This.Projects, + Entries: LevelBundle.This.Entries, + HeadIndex: null, + TailIndex: null, + }; + + var GenerationsToDistribute = 1; + var Generation = TargetGeneration ? TargetGeneration : LevelBundle.Generation; + var Parent = DereferenceLevel(Generation - GenerationsToDistribute); + while(!RangeContains(Parent, Index)) + { + ++GenerationsToDistribute; + Parent = DereferenceLevel(Generation - GenerationsToDistribute); + } + + while(GenerationsToDistribute > 0) + { + var Distribution = ComputeItemDistribution(Parent); + + var ToPlace = null; + var FullButtonItemCount = null; + var ButtonCount = null; + if(LevelBundle.Type == item_type.PROJECT) + { + ToPlace = Distribution.ProjectsToPlace; + FullButtonItemCount = Distribution.FullButtonProjectCount; + ButtonCount = Distribution.ButtonsForProjects; + } + else if(LevelBundle.Type = item_type.ENTRY) + { + ToPlace = Distribution.EntriesToPlace; + FullButtonItemCount = Distribution.FullButtonEntryCount; + ButtonCount = Distribution.ButtonsForEntries; + } + + Result.HeadIndex = null; + Result.TailIndex = null; + for(var i = 0; i < ButtonCount; ++i) + { + if(ToPlace == ButtonCount - i) { FullButtonItemCount = 1; } + Result.HeadIndex = Result.TailIndex !== null ? Result.TailIndex + 1 : Parent.HeadIndex !== null ? Parent.HeadIndex : 0; + Result.TailIndex = Result.HeadIndex + Math.min(FullButtonItemCount, ToPlace) - 1; + if(RangeContains(Result, Index)) { break; } + ToPlace -= LengthOf(Result); + } + --GenerationsToDistribute; + Parent.HeadIndex = Result.HeadIndex; + Parent.TailIndex = Result.TailIndex; + } + + return Result; +} + +function +SiblingIsLeaf(SiblingID) +{ + var LevelBundle = GetTraversalLevelBundle(); + return LengthOf(GetSubdivisionFor(LevelBundle, + SiblingID == siblings.PREV ? LevelBundle.This.HeadIndex - 1 : LevelBundle.This.TailIndex + 1)) == 1; +} + +function +ShiftToSibling(SiblingID) +{ + var LevelBundle = GetTraversalLevelBundle(); + + if((SiblingID == siblings.PREV && HasPrevSibling(LevelBundle.This)) || + (SiblingID == siblings.NEXT && HasNextSibling(LevelBundle.This))) + { + var TransitionType = SiblingID == siblings.PREV ? transition_type.SIBLING_SHIFT_PREV : transition_type.SIBLING_SHIFT_NEXT; + var CurrentItem = LevelBundle.Type == item_type.PROJECT ? LevelBundle.This.Projects : LevelBundle.This.Entries; + + var TargetIndex = SiblingID == siblings.PREV ? LevelBundle.This.HeadIndex - 1 : LevelBundle.This.TailIndex + 1; + LevelBundle.Parent = Nav.TraversalStack[0]; + var TransitionLevel; + for(let i = 0; i < LevelBundle.Generation; ++i) + { + LevelBundle.This = Nav.TraversalStack[i]; + + if((LevelBundle.Type == item_type.PROJECT && CurrentItem == LevelBundle.Parent.Projects) || + (LevelBundle.Type == item_type.ENTRY && CurrentItem == LevelBundle.Parent.Entries)) + { + if(!RangeContains(LevelBundle.This, TargetIndex)) + { + Nav.TraversalStack[i] = GetSubdivisionFor(LevelBundle, TargetIndex, i + 1); + if(LengthOf(Nav.TraversalStack[i]) == 1) + { + TransitionType = transition_type.SUBDIVISION_ASCENT; + TransitionLevel = LevelBundle.This; + Nav.TraversalStack.pop(); + } + } + } + LevelBundle.Parent = Nav.TraversalStack[i]; + } + UpdateButtons(TransitionType, undefined, TransitionLevel); + } + else + { + DequeueInteraction(); + } +} + +function +Ascend() +{ + var LevelBundle = GetTraversalLevelBundle(); + if(LevelBundle.Generation > 1) + { + var TransitionType = LevelBundle.This.HeadIndex !== null && LevelBundle.This.TailIndex !== null ? transition_type.SUBDIVISION_ASCENT : transition_type.PROJECT_EXIT; + var PoppedLevel = Nav.TraversalStack.pop(); + UpdateButtons(TransitionType, undefined, PoppedLevel); + } +} + +function +ShiftToPrevSibling() +{ + var LevelBundle = GetTraversalLevelBundle(); + if(LevelBundle.Generation > 1) + { + if(Nav.Controls.GridTraversal.PrevAscends == true) + { + Ascend(); + } + } + ShiftToSibling(siblings.PREV); +} + +function +ShiftToNextSibling() +{ + ShiftToSibling(siblings.NEXT); +} + +function +InputIsFocused() +{ + return document.activeElement == Search.QueryElement; +} + +function +ShouldFireGridEvents() +{ + return Nav.ViewType == view_type.GRID && !InputIsFocused() && !IsQuery(); +} + +function +ModifyButtonKeybinding(Event) +{ + // TODO(matt): Settle on the final sets of bindings + var Chron = Nav.SortChronological; + var Key = Event.key; + var IT = interaction_type.PUSH_BUTTON; + var ID = { Element: null, Button: null, }; // NOTE(matt): InteractionData + + switch(Key) + { + case "?": if(!InputIsFocused()) { Nav.Controls.HelpDocumentation.classList.toggle("visible"); } break; + case "t": if(!InputIsFocused()) { EnqueueInteraction(interaction_type.SORT); } break; + case "y": if(!InputIsFocused()) { ToggleView(); } break; + case "m": if(!InputIsFocused()) { ToggleAnimations(); } break; + case "h": if(ShouldFireGridEvents()) { EnqueueInteraction(Chron ? interaction_type.SIBLING_SHIFT_PREV : interaction_type.SIBLING_SHIFT_NEXT); } break; + case "k": if(ShouldFireGridEvents()) { EnqueueInteraction(interaction_type.ASCEND); } break; + case "l": if(ShouldFireGridEvents()) { EnqueueInteraction(Chron ? interaction_type.SIBLING_SHIFT_NEXT : interaction_type.SIBLING_SHIFT_PREV); } break; + } + + if(Nav.GridSize == 2) + { + switch(Key) + { + case "1": if(ShouldFireGridEvents()) { ID.Element = Nav.Buttons[Chron ? 0 : 3].Element; ID.Button = Nav.Buttons[Chron ? 0 : 3]; EnqueueInteraction(IT, ID); } break; + case "2": if(ShouldFireGridEvents()) { ID.Element = Nav.Buttons[Chron ? 1 : 2].Element; ID.Button = Nav.Buttons[Chron ? 1 : 2]; EnqueueInteraction(IT, ID); } break; + + case "q": if(ShouldFireGridEvents()) { ID.Element = Nav.Buttons[Chron ? 2 : 1].Element; ID.Button = Nav.Buttons[Chron ? 2 : 1]; EnqueueInteraction(IT, ID); } break; + case "w": if(ShouldFireGridEvents()) { ID.Element = Nav.Buttons[Chron ? 3 : 0].Element; ID.Button = Nav.Buttons[Chron ? 3 : 0]; EnqueueInteraction(IT, ID); } break; + } + } + else if(Nav.GridSize == 3) + { + switch(Key) + { + case "1": if(ShouldFireGridEvents()) { ID.Element = Nav.Buttons[Chron ? 0 : 8].Element; ID.Button = Nav.Buttons[Chron ? 0 : 8]; EnqueueInteraction(IT, ID); } break; + case "2": if(ShouldFireGridEvents()) { ID.Element = Nav.Buttons[Chron ? 1 : 7].Element; ID.Button = Nav.Buttons[Chron ? 1 : 7]; EnqueueInteraction(IT, ID); } break; + case "3": if(ShouldFireGridEvents()) { ID.Element = Nav.Buttons[Chron ? 2 : 6].Element; ID.Button = Nav.Buttons[Chron ? 2 : 6]; EnqueueInteraction(IT, ID); } break; + + case "q": if(ShouldFireGridEvents()) { ID.Element = Nav.Buttons[Chron ? 3 : 5].Element; ID.Button = Nav.Buttons[Chron ? 3 : 5]; EnqueueInteraction(IT, ID); } break; + case "w": if(ShouldFireGridEvents()) { ID.Element = Nav.Buttons[Chron ? 4 : 4].Element; ID.Button = Nav.Buttons[Chron ? 4 : 4]; EnqueueInteraction(IT, ID); } break; + case "e": if(ShouldFireGridEvents()) { ID.Element = Nav.Buttons[Chron ? 5 : 3].Element; ID.Button = Nav.Buttons[Chron ? 5 : 3]; EnqueueInteraction(IT, ID); } break; + + case "a": if(ShouldFireGridEvents()) { ID.Element = Nav.Buttons[Chron ? 6 : 2].Element; ID.Button = Nav.Buttons[Chron ? 6 : 2]; EnqueueInteraction(IT, ID); } break; + case "s": if(ShouldFireGridEvents()) { ID.Element = Nav.Buttons[Chron ? 7 : 1].Element; ID.Button = Nav.Buttons[Chron ? 7 : 1]; EnqueueInteraction(IT, ID); } break; + case "d": if(ShouldFireGridEvents()) { ID.Element = Nav.Buttons[Chron ? 8 : 0].Element; ID.Button = Nav.Buttons[Chron ? 8 : 0]; EnqueueInteraction(IT, ID); } break; + } + } + else if(Nav.GridSize == 4) + { + switch(Key) + { + case "1": if(ShouldFireGridEvents()) { ID.Element = Nav.Buttons[Chron ? 0 : 15].Element; ID.Button = Nav.Buttons[Chron ? 0 : 15]; EnqueueInteraction(IT, ID); } break; + case "2": if(ShouldFireGridEvents()) { ID.Element = Nav.Buttons[Chron ? 1 : 14].Element; ID.Button = Nav.Buttons[Chron ? 1 : 14]; EnqueueInteraction(IT, ID); } break; + case "3": if(ShouldFireGridEvents()) { ID.Element = Nav.Buttons[Chron ? 2 : 13].Element; ID.Button = Nav.Buttons[Chron ? 2 : 13]; EnqueueInteraction(IT, ID); } break; + case "4": if(ShouldFireGridEvents()) { ID.Element = Nav.Buttons[Chron ? 3 : 12].Element; ID.Button = Nav.Buttons[Chron ? 3 : 12]; EnqueueInteraction(IT, ID); } break; + + case "q": if(ShouldFireGridEvents()) { ID.Element = Nav.Buttons[Chron ? 4 : 11].Element; ID.Button = Nav.Buttons[Chron ? 4 : 11]; EnqueueInteraction(IT, ID); } break; + case "w": if(ShouldFireGridEvents()) { ID.Element = Nav.Buttons[Chron ? 5 : 10].Element; ID.Button = Nav.Buttons[Chron ? 5 : 10]; EnqueueInteraction(IT, ID); } break; + case "e": if(ShouldFireGridEvents()) { ID.Element = Nav.Buttons[Chron ? 6 : 9].Element; ID.Button = Nav.Buttons[Chron ? 6 : 9]; EnqueueInteraction(IT, ID); } break; + case "r": if(ShouldFireGridEvents()) { ID.Element = Nav.Buttons[Chron ? 7 : 8].Element; ID.Button = Nav.Buttons[Chron ? 7 : 8]; EnqueueInteraction(IT, ID); } break; + + case "a": if(ShouldFireGridEvents()) { ID.Element = Nav.Buttons[Chron ? 8 : 7].Element; ID.Button = Nav.Buttons[Chron ? 8 : 7]; EnqueueInteraction(IT, ID); } break; + case "s": if(ShouldFireGridEvents()) { ID.Element = Nav.Buttons[Chron ? 9 : 6].Element; ID.Button = Nav.Buttons[Chron ? 9 : 6]; EnqueueInteraction(IT, ID); } break; + case "d": if(ShouldFireGridEvents()) { ID.Element = Nav.Buttons[Chron ? 10 : 5].Element; ID.Button = Nav.Buttons[Chron ? 10 : 5]; EnqueueInteraction(IT, ID); } break; + case "f": if(ShouldFireGridEvents()) { ID.Element = Nav.Buttons[Chron ? 11 : 4].Element; ID.Button = Nav.Buttons[Chron ? 11 : 4]; EnqueueInteraction(IT, ID); } break; + + case "z": if(ShouldFireGridEvents()) { ID.Element = Nav.Buttons[Chron ? 12 : 3].Element; ID.Button = Nav.Buttons[Chron ? 12 : 3]; EnqueueInteraction(IT, ID); } break; + case "x": if(ShouldFireGridEvents()) { ID.Element = Nav.Buttons[Chron ? 13 : 2].Element; ID.Button = Nav.Buttons[Chron ? 13 : 2]; EnqueueInteraction(IT, ID); } break; + case "c": if(ShouldFireGridEvents()) { ID.Element = Nav.Buttons[Chron ? 14 : 1].Element; ID.Button = Nav.Buttons[Chron ? 14 : 1]; EnqueueInteraction(IT, ID); } break; + case "v": if(ShouldFireGridEvents()) { ID.Element = Nav.Buttons[Chron ? 15 : 0].Element; ID.Button = Nav.Buttons[Chron ? 15 : 0]; EnqueueInteraction(IT, ID); } break; + } + } +} + +function +BindKeys() +{ + document.addEventListener("keydown", ModifyButtonKeybinding); +} + +function +UnbindKeys() +{ + document.removeEventListener("keydown", ModifyButtonKeybinding); +} + +function +RebindKeys() +{ + UnbindKeys(); + BindKeys(); +} + +function +DoRotationStage(Now) +{ + if(Nav.Transition.StageDurations.length) + { + if(Nav.Transition.StartTime === undefined) + { + Nav.Transition.StartTime = Now; + } + + var Elapsed = Now - Nav.Transition.StartTime; + var Duration = Nav.Transition.StageDurations[0]; + + if(Duration === 0) + { + if(Nav.Transition.Transforms.ButtonsContainer.TargetStages.length) + { + MergeTransform(Nav.Transition.Transforms.ButtonsContainer.Current, Nav.Transition.Transforms.ButtonsContainer.TargetStages[0]); + ApplyTransform(Nav.ButtonsContainer, Nav.Transition.Transforms.ButtonsContainer.Current); + } + } + else + { + var t = Clamp01(Elapsed / Duration); + if(LerpTransforms(Nav.Transition.Transforms.ButtonsContainer, t)) + { + ApplyTransform(Nav.ButtonsContainer, Nav.Transition.Transforms.ButtonsContainer.Current); + } + } + + for(var i = 0; i < Nav.ButtonsContainer.children.length; ++i) + { + var This = Nav.ButtonsContainer.children[i]; + var ThisTexts = This.querySelectorAll(".text"); + for(var j = 0; j < ThisTexts.length; ++j) + { + ThisTexts[j].style.transform = "rotate3d(0, 0, 1, " + -Nav.Transition.Transforms.ButtonsContainer.Current.Rotation.Z + "deg)"; + } + } + + if(TransformsComplete(Nav.Transition.Transforms)) + { + FinaliseTransforms(Nav.Transition.Transforms); + } + Nav.Transition.RequestedFrame = window.requestAnimationFrame(DoRotationStage); + } + else + { + Nav.Transition.RequestedFrame = undefined; + ResetButtonsContainerClone(); + DequeueInteraction(); + Nav.Controls.Header.style.overflow = null; + } +} + +function +RotateButtons(Initialising) +{ + // TODO(matt): Consider pushing this through DoTransition(), aware that it'd need to transform ".text" children + CopyTransform(Nav.Transition.Transforms.ButtonsContainer.Initial, Nav.Transition.Transforms.ButtonsContainer.Current); + Nav.Transition.StartTime = undefined; + if(!Initialising && Nav.Transition.Enabled) + { + Nav.Transition.StageDurations.push(320); + } + else + { + Nav.Transition.StageDurations.push(0); + } + + let Target = NullTarget(); + if(Nav.SortChronological) + { + Target.Rotation.Z = 0; + } + else + { + Target.Rotation.Z = 180; + } + Nav.Transition.Transforms.ButtonsContainer.TargetStages.push(Target); + + CopyTransform(Nav.Transition.Transforms.ButtonsContainer.Initial, Nav.Transition.Transforms.ButtonsContainer.Current); + Nav.Transition.RequestedFrame = window.requestAnimationFrame(DoRotationStage); +} + +function +AddAnimationClass() +{ + Nav.Nexus.classList.add("anim"); +} + +function +Sort(Initialising) +{ + ResetButtonsContainerClone(); + + if(Initialising && Nav.Transition.Enabled) { Nav.Nexus.classList.remove("anim"); } + if(Nav.SortChronological) + { + Nav.Controls.Sort.textContent = "Sort: New to Old ⏷"; + Nav.Nexus.classList.add("reversed"); + for(var i = 0; i < Search.Projects.length; ++i) + { + if(Search.Projects[i].entriesContainer) + { + Search.Projects[i].entriesContainer.style.flexFlow = "column-reverse"; + } + } + } + else + { + Nav.Controls.Sort.textContent = "Sort: Old to New ⏶"; + Nav.Nexus.classList.remove("reversed"); + for(var i = 0; i < Search.Projects.length; ++i) + { + if(Search.Projects[i].entriesContainer) + { + Search.Projects[i].entriesContainer.style.flexFlow = "column"; + } + } + } + if(Initialising && Nav.Transition.Enabled) { setTimeout(AddAnimationClass, 320); } + + UnbindKeys(); + Nav.SortChronological = !Nav.SortChronological; + if(MaintainingState()) + { + if(Nav.SortChronological) { ClearStateBit(state_bit.SORT_REVERSED); } + else { SetStateBit(state_bit.SORT_REVERSED); } + } + RotateButtons(Initialising); + RebindKeys(); + runSearch(true); +} + +function +DequeueInteraction() +{ + if(Nav.InteractionQueue.length) + { + var I = Nav.InteractionQueue.shift(); + + switch(I.Type) + { + case interaction_type.PUSH_BUTTON: { PushButton(I.Data.Element, I.Data.Button); } break; + case interaction_type.SIBLING_SHIFT_PREV: { ShiftToPrevSibling(); } break; + case interaction_type.SIBLING_SHIFT_NEXT: { ShiftToNextSibling(); } break; + case interaction_type.ASCEND: { Ascend(); } break; + case interaction_type.SORT: { Sort(); } break; + } + } +} + +function +EnqueueInteraction(InteractionType, Data) +{ + // TODO(matt): Maybe see about interpolating out of interrupted transitions? + // + // Interruptability: + // Reversible Pairs: + // SIBLING_SHIFT_PREV by SIBLING_SHIFT_NEXT + // SIBLING_SHIFT_NEXT (when SHIFT) by SIBLING_SHIFT_PREV + // SIBLING_SHIFT_NEXT (when ASCEND) by PUSH_BUTTON (of the corresponding button) + // PUSH_BUTTON by ASCEND + // ASCEND by PUSH_BUTTON (of the corresponding button) + // SORT by SORT + // + // Mixtures: + // SIBLING_SHIFT_PREV – SORT + // + + var I = { + Type: InteractionType, + Data: Data, + }; + + Nav.InteractionQueue.push(I); + + if(!Nav.Transition.RequestedFrame) + { + DequeueInteraction(); + } +} + +function +InitNexus() +{ + Nav.List.classList.add("hidden"); + var ButtonsContainerPrototype = document.createElement("div"); + ButtonsContainerPrototype.setAttribute("id", "cineraIndexGrid"); + + var ButtonsPrototype = document.createElement("div"); + ButtonsPrototype.classList.add("cineraButtons"); + + var ButtonsClonePrototype = document.createElement("div"); + ButtonsClonePrototype.classList.add("cineraButtons"); + + ButtonsContainerPrototype.appendChild(ButtonsClonePrototype); + ButtonsContainerPrototype.appendChild(ButtonsPrototype); + Nav.Transition.ButtonsTransitionContainerElement = Nav.GridContainer.appendChild(ButtonsContainerPrototype); + Nav.Grid = Nav.Transition.ButtonsTransitionContainerElement; + + Nav.Transition.ButtonsContainerCloneElement = Nav.Transition.ButtonsTransitionContainerElement.querySelectorAll(".cineraButtons")[0]; + Nav.ButtonsContainer = Nav.Transition.ButtonsTransitionContainerElement.querySelectorAll(".cineraButtons")[1]; + Nav.GridColumnGap = parseInt(window.getComputedStyle(Nav.ButtonsContainer).gridColumnGap); + Nav.GridRowGap = parseInt(window.getComputedStyle(Nav.ButtonsContainer).gridRowGap); + + // NOTE(matt): We ResetButtonsContainerClone() anyway, but without cycling this classList Safari seems to do transitions + // based on the wrong size grid + Nav.GridContainer.classList.add("hidden"); + ResetButtonsContainerClone(); + Nav.GridContainer.classList.remove("hidden"); + + Nav.Nexus.classList.add("anim"); + + ScrollCondition = true; // NOTE(matt): Variable in cinera_pre.js, we init with the Grid which is the view we want to auto-scroll + if(CineraProps.IsMobile) + { + switch(CineraProps.Orientation) + { + case orientations.LANDSCAPE_LEFT: + { + Nav.GridContainer.classList.add("Landscape", "Left"); + } break; + case orientations.LANDSCAPE_RIGHT: + { + Nav.GridContainer.classList.add("Landscape", "Right"); + } break; + case orientations.PORTRAIT: + { + Nav.GridContainer.classList.add("Portrait"); + } break; + } + } +} + +function +ComputeOptimalGridSize() +{ + var Result = null; + var DimOffset = { + X: 0, + Y: Nav.Controls.Header.offsetHeight, + }; + + if(CineraProps.Orientation == orientations.LANDSCAPE_LEFT || CineraProps.Orientation == orientations.LANDSCAPE_RIGHT) + { + DimOffset.X += Nav.Controls.GridTraversal.Header.offsetWidth; + } + else + { + DimOffset.Y += Nav.Controls.GridTraversal.Header.offsetHeight; + } + + var MaxWidth = MaxWidthOfElement(Nav.Nexus) - DimOffset.X; + var MaxHeight = MaxHeightOfElement(Nav.Nexus) - DimOffset.Y; + if(Nav.Nexus.parentNode == document.body) + { + // TODO(matt): Robustify this + var BodyStyle = window.getComputedStyle(document.body); + MaxWidth -= parseInt(BodyStyle.marginRight); + MaxHeight -= parseInt(BodyStyle.marginBottom); + } + + if(MaxWidth > MaxHeight) + { + MaxWidth = MaxHeight; + } + else if(MaxHeight > MaxWidth) + { + MaxHeight = MaxWidth; + } + + var MinButtonSize = 100; + + var MinGridSize = 2; + var MaxGridSize = 4; + + Result = Clamp(MinGridSize, Math.floor(MaxWidth / MinButtonSize), MaxGridSize); + var Dim = (MaxWidth - Nav.GridRowGap * (Result - 1)) / Result; + + // TODO(matt): If Safari's head-item sizing bug proves to be a problem, use this: + /* + var ButtonStyleRuleSelector = "#cineraIndex #cineraIndexGrid .cineraButton.subdivision"; + var ButtonStyleRule = GetOrSetRule(ButtonStyleRuleSelector); + + ButtonStyleRule.style.width = Dim + "px"; + ButtonStyleRule.style.height = Dim + "px"; + */ + + var GridTemplateColumnsStyle = "repeat(" + Result + ", 1fr)"; + Nav.ButtonsContainer.style.gridTemplateColumns = GridTemplateColumnsStyle; + + var GridTemplateRowsStyle = "repeat(" + Result + ", " + Dim + "px)"; + Nav.ButtonsContainer.style.gridTemplateRows = GridTemplateRowsStyle; + + Nav.ButtonsContainer.style.width = MaxWidth + "px"; + Nav.ButtonsContainer.style.height = MaxHeight + "px"; + + Nav.Transition.ButtonsTransitionContainerElement.style.width = MaxWidth + "px"; + Nav.Transition.ButtonsTransitionContainerElement.style.height = MaxHeight + "px"; + + Nav.GridWidth = MaxWidth; + return Result; +} + +function +GetScrollToElement(NodeList, UpperIndex) +{ + var Result = undefined; + if(UpperIndex === null) + { + if(NodeList.length) + { + Result = NodeList[0].closest(".cineraIndexProject"); + } + else + { + Result = NodeList.closest(".cineraIndexProject"); + } + } + else + { + Result = NodeList[UpperIndex]; + } + return Result; +} + +function +EmptyTraversalStack() +{ + while(Nav.TraversalStack.length > 1) + { + Nav.TraversalStack.pop(); + } +} + +function +GetContainingProjectOfLevel(Level) +{ + var Result = null; + if(Level.Projects !== null) + { + if(Level.Projects.length) + { + Result = Level.Projects[0].parentNode; + } + else + { + Result = Level.Projects.parentNode; + } + } + else if(Level.Entries !== null) + { + if(Level.Entries.length) + { + Result = Level.Entries[0].closest(".cineraIndexProject"); + } + else + { + Result = Level.Entries.closest(".cineraIndexProject"); + } + } + return Result; +} + +function +GetContainingProject(ProjectElement) +{ + var Result = null; + if(ProjectElement.parentNode.classList.contains("cineraIndexProject")) + { + Result = ProjectElement.parentNode; + } + return Result; +} + +function +PushProjectOntoStack(Stack, ContainingProject) +{ + var Project = { + Element: ContainingProject, + Projects: ContainingProject.querySelectorAll(":scope > .cineraIndexProject"), + Entries: ContainingProject.querySelectorAll(":scope > .cineraIndexEntries > div"), + Index: null, + }; + + if(Project.Projects.length == 0) { Project.Projects = null; } + if(Project.Entries.length == 0) { Project.Entries = null; } + + var Siblings = ContainingProject.parentNode.querySelectorAll(":scope > .cineraIndexProject"); + Project.Index = GetIndexOfElement(Siblings, ContainingProject); + Stack.push(Project); +} + +function +BuildProjectsStack(TargetLevel) +{ + let ProjectsStack = []; + var ContainingProject = GetContainingProjectOfLevel(TargetLevel); + + PushProjectOntoStack(ProjectsStack, ContainingProject); + + ContainingProject = GetContainingProject(ProjectsStack[ProjectsStack.length - 1].Element); + while(ContainingProject) + { + PushProjectOntoStack(ProjectsStack, ContainingProject); + ContainingProject = GetContainingProject(ProjectsStack[ProjectsStack.length - 1].Element); + } + + return ProjectsStack; +} + +function +EmptyTraversalStackIntoProjectsStack() +{ + let ProjectsStack = []; + while(Nav.TraversalStack.length > 1) + { + let ThisLevel = Nav.TraversalStack[Nav.TraversalStack.length - 1]; + PushLevelProjectUniquely(ProjectsStack, ThisLevel); + Nav.TraversalStack.pop(); + } + return ProjectsStack; +} + +function +DeriveTraversalStack(ProjectsStack, TargetLevel) +{ + if(ProjectsStack && TargetLevel) + { + while(ProjectsStack.length > 0) + { + var ThisProject = ProjectsStack[ProjectsStack.length - 1]; + + var ButtonInfo = { HeadIndex: null, TailIndex: null, ItemCount: null, }; + let PopulationData = { + Distribution: null, + ThisLevel: null, + Prev: null, + DoingEntries: false, + }; + PopulationData.ThisLevel = Nav.TraversalStack[Nav.TraversalStack.length - 1]; + PopulationData.Distribution = ComputeItemDistribution(PopulationData.ThisLevel); + + for(var ButtonIndex = 0; ButtonIndex < Nav.Buttons.length; ++ButtonIndex) + { + let Button = { + Projects: null, + Entries: null, + HeadIndex: null, + TailIndex: null, + }; + + if(PopulationData.Distribution.ProjectsToPlace > 0 || PopulationData.Distribution.EntriesToPlace > 0) + { + ButtonInfo = PopulateButton(PopulationData, Button, ButtonIndex); + if(ButtonInfo.HeadIndex <= ThisProject.Index && ButtonInfo.TailIndex >= ThisProject.Index) + { + var FoundProject = PseudoPushButton(Button); + if(FoundProject) + { + ProjectsStack.pop(); + } + break; + } + PopulationData.Prev = ButtonInfo; + } + } + } + + if(TargetLevel.HeadIndex !== null && TargetLevel.TailIndex !== null) + { + var Descended = false; + while(true) + { + let PopulationData = { + Distribution: null, + ThisLevel: null, + Prev: null, + DoingEntries: false, + }; + PopulationData.ThisLevel = Nav.TraversalStack[Nav.TraversalStack.length - 1]; + PopulationData.Distribution = ComputeItemDistribution(PopulationData.ThisLevel); + + for(var ButtonIndex = 0; ButtonIndex < Nav.Buttons.length; ++ButtonIndex) + { + let Button = { + Projects: null, + Entries: null, + HeadIndex: null, + TailIndex: null, + }; + + if(PopulationData.Distribution.ProjectsToPlace > 0 || PopulationData.Distribution.EntriesToPlace > 0) + { + ButtonInfo = PopulateButton(PopulationData, Button, ButtonIndex); + if(ButtonInfo.HeadIndex <= TargetLevel.HeadIndex && ButtonInfo.TailIndex >= TargetLevel.TailIndex) + { + Descended = true; + PseudoPushButton(Button); + break; + } + PopulationData.Prev = ButtonInfo; + } + } + if(!Descended) + { + break; + } + else + { + Descended = false; + } + } + } + } +} + +function +GetGenerationOf(Element) +{ + var Result = 0; + if(Element.classList.contains("cineraIndexProject")) + { + ++Result; + var ContainingProject = GetContainingProject(Element); + while(ContainingProject) + { + ++Result; + ContainingProject = GetContainingProject(ContainingProject); + } + } + return Result; +} + +function +ComputeTargetLevelForViewport() +{ + var Result = { + Projects: null, + Entries: null, + HeadIndex: null, + TailIndex: null, + }; + + var ViewportTop = window.scrollY; + var ViewportBottom = ViewportTop + window.innerHeight; + + var ControlsHeight = Nav.Controls.Header.offsetHeight; + ViewportTop += ControlsHeight; + + var Elements = []; + for(var ProjectIndex = 0; ProjectIndex < Search.Projects.length; ++ProjectIndex) + { + var ThisProject = Search.Projects[ProjectIndex]; + Elements.push(ThisProject.projectTitleElement); + if(ThisProject.entriesContainer) + { + var Entries = ThisProject.entriesContainer.querySelectorAll(":scope > div"); + if(Nav.SortChronological) + { + for(var EntryIndex = 0; EntryIndex < Entries.length; ++EntryIndex) + { + Elements.push(Entries[EntryIndex]); + } + } + else + { + for(var EntryIndex = Entries.length - 1; EntryIndex >= 0; --EntryIndex) + { + Elements.push(Entries[EntryIndex]); + } + } + } + } + + var Upper = { + Element: null, + Parent: null, + Type: null, + }; + var Lower = { + Element: null, + Parent: null, + Type: null, + }; + + for(var i = 0; i < Elements.length; ++i) + { + var ElementTop = getElementYOffsetFromPage(Elements[i]); + if(!Upper.Element) + { + if(ElementTop >= ViewportTop) + { + Upper.Element = Elements[i]; + Lower.Element = Upper.Element; + } + } + else + { + var ElementBottom = ElementTop + Elements[i].scrollHeight; + if(ElementBottom <= ViewportBottom) + { + Lower.Element = Elements[i]; + } + else + { + break; + } + } + } + + Upper.Type = Upper.Element.classList.contains("cineraProjectTitle") ? item_type.PROJECT : item_type.ENTRY; + Lower.Type = Lower.Element.classList.contains("cineraProjectTitle") ? item_type.PROJECT : item_type.ENTRY; + + Upper.Parent = Upper.Element.closest(".cineraIndexProject"); + Lower.Parent = Lower.Element.closest(".cineraIndexProject"); + + if(Upper.Parent == Lower.Parent) + { + if(Upper.Type == Lower.Type) + { + switch(Upper.Type) + { + case item_type.PROJECT: + { + Result.Projects = Upper.Parent.querySelectorAll(":scope > .cineraIndexProject"); + Result.HeadIndex = GetIndexOfElement(Result.Projects, Upper.Element); + Result.TailIndex = GetIndexOfElement(Result.Projects, Lower.Element); + } break; + case item_type.ENTRY: + { + Result.Entries = Upper.Parent.querySelectorAll(":scope > .cineraIndexEntries > div"); + Result.HeadIndex = GetIndexOfElement(Result.Entries, Upper.Element); + Result.TailIndex = GetIndexOfElement(Result.Entries, Lower.Element); + } break; + } + if(!Nav.SortChronological) + { + var Temp = Result.HeadIndex; + Result.HeadIndex = Result.TailIndex; + Result.TailIndex = Temp; + } + } + else + { + Result.Projects = Upper.Parent.querySelectorAll(":scope > .cineraIndexProject"); + if(Result.Projects.length == 0) { Result.Projects = null; } + Result.Entries = Upper.Parent.querySelectorAll(":scope > .cineraIndexEntries > div"); + if(Result.Entries.length == 0) { Result.Entries = null; } + } + } + else + { + var UpperGeneration = GetGenerationOf(Upper.Parent); + var LowerGeneration = GetGenerationOf(Lower.Parent); + while(UpperGeneration > LowerGeneration) + { + Upper.Parent = GetContainingProject(Upper.Parent); + --UpperGeneration; + } + while(LowerGeneration > UpperGeneration) + { + Lower.Parent = GetContainingProject(Lower.Parent); + --LowerGeneration; + } + + if(UpperGeneration == 0 && LowerGeneration == 0) + { + Upper.Parent = null; + Lower.Parent = null; + } + + while(Upper.Parent && Lower.Parent && Upper.Parent != Lower.Parent) + { + Upper.Parent = GetContainingProject(Upper.Parent); + Lower.Parent = GetContainingProject(Lower.Parent); + } + + if(Upper.Parent != null) + { + Result.Projects = Upper.Parent.querySelectorAll(":scope > .cineraIndexProject"); + if(Result.Projects.length == 0) { Result.Projects = null; } + Result.Entries = Upper.Parent.querySelectorAll(":scope > .cineraIndexEntries > div"); + if(Result.Entries.length == 0) { Result.Entries = null; } + } + } + + while(Elements.length > 0) + { + Elements.pop(); + } + + return Result; +} + +function +ScrollToWithOffset(Element, Offset) +{ + var ScrollTop = getElementYOffsetFromPage(Element); + ScrollTop -= Offset; + window.scrollTo(0, ScrollTop); +} + +function +ToggleView() +{ + // NOTE(matt): While we only have two views, a toggle will suffice + clearTimeout(ScrollerFunction); + ScrollTicking = false; + if(Nav.ViewType == view_type.GRID) + { + Nav.Controls.View.textContent = "View: List"; + Nav.ViewType = view_type.LIST; + if(MaintainingState()) + { + ClearStateBit(state_bit.VIEW_GRID); + SetStateBit(state_bit.VIEW_LIST); + } + + if(!IsQuery()) + { + Nav.List.classList.remove("hidden"); + Nav.GridContainer.classList.add("hidden"); + var LevelBundle = GetTraversalLevelBundle(); + if(LevelBundle.Generation > 1) + { + var Element; + if(LevelBundle.This.Entries !== null) + { + Element = GetScrollToElement(LevelBundle.This.Entries, Nav.SortChronological ? LevelBundle.This.HeadIndex : LevelBundle.This.TailIndex); + } + else if(LevelBundle.This.Projects !== null) + { + Element = GetScrollToElement(LevelBundle.This.Projects, Nav.SortChronological ? LevelBundle.This.HeadIndex : LevelBundle.This.TailIndex); + } + ScrollToWithOffset(Element, Nav.Controls.Header.offsetHeight); + } + } + } + else + { + Nav.Controls.View.textContent = "View: Grid"; + Nav.ViewType = view_type.GRID; + if(MaintainingState()) + { + ClearStateBit(state_bit.VIEW_LIST); + SetStateBit(state_bit.VIEW_GRID); + } + + if(!IsQuery()) + { + var TargetLevel = ComputeTargetLevelForViewport(); + EmptyTraversalStack(); + if(TargetLevel.Projects || TargetLevel.Entries) + { + var ProjectsStack = BuildProjectsStack(TargetLevel); + DeriveTraversalStack(ProjectsStack, TargetLevel); + } + UpdateButtons(); + + Nav.List.classList.add("hidden"); + Nav.GridContainer.classList.remove("hidden"); + + ScrollToWithOffset(Nav.Controls.GridTraversal.Header, Nav.Controls.Header.offsetHeight); + } + } + ScrollCondition = Nav.ViewType == view_type.GRID; +} + +function +ToggleAnimations() +{ + Nav.Transition.Enabled = !Nav.Transition.Enabled; + if(Nav.Transition.Enabled) + { + Nav.Controls.Anim.textContent = "Animations: ✔"; + Nav.Nexus.classList.add("anim"); + ClearStateBit(state_bit.DISABLE_ANIMATIONS); + } + else + { + Nav.Controls.Anim.textContent = "Animations: ✘"; + Nav.Nexus.classList.remove("anim"); + SetStateBit(state_bit.DISABLE_ANIMATIONS); + } +} + +function +ToggleSave() +{ + if(MaintainingState()) + { + Nav.Controls.Save.textContent = "Save Settings: ✘"; + Nav.State = 0; + Nav.State |= state_bit.NO_SAVE; + SaveState(); + } + else + { + Nav.Controls.Save.textContent = "Save Settings: ✔"; + Nav.State ^= state_bit.NO_SAVE; + if(!Nav.Transition.Enabled) { SetStateBit(state_bit.DISABLE_ANIMATIONS); } + if(!Nav.SortChronological) { SetStateBit(state_bit.SORT_REVERSED); } + + if(Nav.ViewType == view_type.LIST) { SetStateBit(state_bit.VIEW_LIST); } + else if(Nav.ViewType == view_type.GRID) { SetStateBit(state_bit.VIEW_GRID); } + } +} + +function +BindMenuItem(Item) +{ + // TODO(matt): Enable this to bind the "click" event, making it take a function and parameters + Item.addEventListener("mouseover", function(ev) { this.classList.add("focused"); }); + Item.addEventListener("mouseout", function(ev) { this.classList.remove("focused"); }); +} + +function +BindControls() +{ + var SettingsMenu = Nav.Controls.Header.querySelector(".cineraMenu.ViewSettings"); + var SettingsMenuContainer = SettingsMenu.querySelector(".cineraMenuContainer"); + + SettingsMenu.addEventListener("mouseenter", function(ev) { + SettingsMenuContainer.classList.add("visible"); + }); + + SettingsMenu.addEventListener("mouseleave", function(ev) { + SettingsMenuContainer.classList.remove("visible"); + }); + + SettingsMenu.addEventListener("click", function(ev) { + SettingsMenuContainer.classList.toggle("visible"); + }); + + BindMenuItem(Nav.Controls.Sort); + Nav.Controls.Sort.addEventListener("click", function(ev) { ev.stopPropagation(); EnqueueInteraction(interaction_type.SORT); }); + BindMenuItem(Nav.Controls.View); + Nav.Controls.View.addEventListener("click", function(ev) { ev.stopPropagation(); ToggleView(); }); + BindMenuItem(Nav.Controls.Anim); + Nav.Controls.Anim.addEventListener("click", function(ev) { ev.stopPropagation(); ToggleAnimations(); }); + BindMenuItem(Nav.Controls.Save); + Nav.Controls.Save.addEventListener("click", function(ev) { ev.stopPropagation(); ToggleSave(); }); + + var Filter = Nav.Controls.Header.querySelector(".cineraMenu.IndexFilter"); + if(Filter) + { + var FilterContainer = Filter.querySelector(".cineraMenuContainer"); + // TODO(matt): Once we have multiple menus, use a menuState on this page + // menuState.push(Filter); + + Filter.addEventListener("mouseenter", function(ev) { + FilterContainer.classList.add("visible"); + }); + + Filter.addEventListener("mouseleave", function(ev) { + FilterContainer.classList.remove("visible"); + }); + + Filter.addEventListener("click", function(ev) { + FilterContainer.classList.toggle("visible"); + }); + + var IndexFilterProjects = Filter.querySelectorAll(".cineraFilterProject"); + for(var i = 0; i < IndexFilterProjects.length; ++i) + { + IndexFilterProjects[i].addEventListener("mouseover", function(ev) { + ev.stopPropagation(); + this.classList.add("focused"); + focusSprite(this); + }); + IndexFilterProjects[i].addEventListener("mouseout", function(ev) { + ev.stopPropagation(); + this.classList.remove("focused"); + unfocusSprite(this); + }); + IndexFilterProjects[i].addEventListener("click", function(ev) { + ev.stopPropagation(); + toggleEntriesOfProjectAndChildren(this); + }); + } + } + + Search.QueryElement.addEventListener("input", function(ev) { + history.replaceState(null, null, "#" + encodeURIComponent(Search.QueryElement.value)); + runSearch(); + }); + + Nav.Controls.GridTraversal.Prev.addEventListener("click", function() { EnqueueInteraction(interaction_type.SIBLING_SHIFT_PREV) }); + Nav.Controls.GridTraversal.Ascend.addEventListener("click", function() { EnqueueInteraction(interaction_type.ASCEND) }); + Nav.Controls.GridTraversal.Next.addEventListener("click", function() { EnqueueInteraction(interaction_type.SIBLING_SHIFT_NEXT) }); + + BindHelp(Nav.Controls.Help, Nav.Controls.HelpDocumentation); +} + +function +InitButtons() +{ + var ButtonPrototype = document.createElement("div"); + ButtonPrototype.classList.add("cineraButton", "subdivision"); + for(var i = 0; i < Math.pow(Nav.GridSize, 2); ++i) + { + Nav.ButtonsContainer.appendChild(ButtonPrototype.cloneNode()); + } + + var Buttons = Nav.Nexus.querySelectorAll(".cineraButton.subdivision"); + for(let j = 0; j < Buttons.length; ++j) + { + let Button = { + Element: Buttons[j], + Projects: [], + Entries: [], + HeadIndex: null, + TailIndex: null, + }; + + Buttons[j].addEventListener("click", function() { + let InteractionData = { + Element: this, + Button: Button, + }; + EnqueueInteraction(interaction_type.PUSH_BUTTON, InteractionData); + }); + + Nav.Buttons.push(Button); + } + + BindKeys(); +} + +function +ReinitButtons() +{ + for(; Nav.Buttons.length > 0;) + { + Nav.Buttons[0].Element.remove(); + Nav.Buttons.shift(); + } + RebindKeys(); + InitButtons(); + UpdateButtons(); +} + +function +PushLevelProjectUniquely(Stack, Level) +{ + if(Level.HeadIndex == null && Level.TailIndex == null) + { + var Found = false; + var ContainingProject = GetContainingProjectOfLevel(Level); + + for(var i = 0; i < Stack.length; ++i) + { + if(ContainingProject === Stack[i].Element) + { + Found = true; + break; + } + } + + if(!Found) + { + PushProjectOntoStack(Stack, ContainingProject); + } + } +} + +function +ButtonContainsLevel(Button, Level) +{ + var Result = false; + if(NodesMatch(Button.Projects, Level.Projects) && + NodesMatch(Button.Entries, Level.Entries) && + Button.HeadIndex <= Level.HeadIndex && + Button.TailIndex >= Level.TailIndex) + { + Result = true; + } + return Result; +} + +function +PopulateButton(PopulationData, Button, ButtonIndex) +{ + var Result; + if(PopulationData.Distribution.ProjectsToPlace > 0) + { + Button.Projects = PopulationData.ThisLevel.Projects; + if(PopulationData.Distribution.ProjectsToPlace == 1 || PopulationData.Distribution.ProjectsToPlace == PopulationData.Distribution.ButtonsForProjects - ButtonIndex) + { + PopulationData.Distribution.FullButtonProjectCount = 1; + } + + Result = SetButtonInfo(Button, PopulationData.Prev, PopulationData.ThisLevel, PopulationData.Distribution); + if(Result.ItemCount == 1) + { + Button.Projects = PopulationData.ThisLevel.Projects[Result.HeadIndex]; + } + PopulationData.Distribution.ProjectsToPlace -= Result.ItemCount; + } + else + { + Button.Entries = PopulationData.ThisLevel.Entries; + if(!PopulationData.DoingEntries) + { + PopulationData.Prev = null; + PopulationData.DoingEntries = true; + } + + if(PopulationData.Distribution.EntriesToPlace == 1 || PopulationData.Distribution.EntriesToPlace == PopulationData.Distribution.ButtonsForEntries - ButtonIndex) + { + PopulationData.Distribution.FullButtonEntryCount = 1; + } + + Result = SetButtonInfo(Button, PopulationData.Prev, PopulationData.ThisLevel, PopulationData.Distribution); + if(Result.ItemCount == 1) + { + Button.Entries = Button.Entries[Result.HeadIndex]; + } + PopulationData.Distribution.EntriesToPlace -= Result.ItemCount; + } + return Result; +} + +function +PseudoPushButton(Button) +{ + var ButtonIsProjectOrEntry = false; + + var Level = { + Projects: Button.Projects, + Entries: Button.Entries, + HeadIndex: Button.HeadIndex, + TailIndex: Button.TailIndex, + }; + + if(Level.Projects !== null || Level.Entries !== null) + { + if(Level.Projects !== null) + { + if(Level.Projects.length === undefined) + { + var Entries = Level.Projects.querySelectorAll(":scope > .cineraIndexEntries > div"); + var Projects = Level.Projects.querySelectorAll(":scope > .cineraIndexProject"); + Level.Entries = Entries.length ? Entries : null; + Level.Projects = Projects.length ? Projects : null; + ButtonIsProjectOrEntry = true; + } + Nav.TraversalStack.push(Level); + } + else + { + if(Level.Entries.length === undefined) + { + ButtonIsProjectOrEntry = true; + } + else + { + Nav.TraversalStack.push(Level); + } + } + } + + return ButtonIsProjectOrEntry; +} + +function +InitResizeEventListener() +{ + window.addEventListener("resize", function() { + var NewGridSize = ComputeOptimalGridSize(); + if(Nav.GridSize !== NewGridSize) + { + Nav.GridSize = NewGridSize; + ReinitButtons(); + var TargetLevel = Nav.TraversalStack[Nav.TraversalStack.length - 1]; + var ProjectsStack = EmptyTraversalStackIntoProjectsStack(); + DeriveTraversalStack(ProjectsStack, TargetLevel); + SetHelpKeyAvailability(Nav.GridSize) + } + UpdateButtons(); + }); +} + +function +InitOrientationChangeListener() +{ + window.onorientationchange = function() + { + if(CineraProps.IsMobile) + { + CineraProps.Orientation = window.orientation; + Nav.GridContainer.classList.remove("Portrait", "Landscape", "Left", "Right"); + switch(CineraProps.Orientation) + { + case orientations.LANDSCAPE_LEFT: + { + Nav.GridContainer.classList.add("Landscape", "Left"); + } break; + case orientations.LANDSCAPE_RIGHT: + { + Nav.GridContainer.classList.add("Landscape", "Right"); + } break; + case orientations.PORTRAIT: + { + Nav.GridContainer.classList.add("Portrait"); + } break; + } + + var NewGridSize = ComputeOptimalGridSize(); + if(Nav.GridSize !== NewGridSize) + { + Nav.GridSize = NewGridSize; + ReinitButtons(); + var TargetLevel = Nav.TraversalStack[Nav.TraversalStack.length - 1]; + var ProjectsStack = EmptyTraversalStackIntoProjectsStack(); + DeriveTraversalStack(ProjectsStack, TargetLevel); + SetHelpKeyAvailability(Nav.GridSize) + } + UpdateButtons(); + } + }; +} +// +// Presenting / Navigating (Laying out and traversing the grid, and sorting)