// ==UserScript==
// @name Supercharged Local Directory File Browser
// @version 4.1.3.2
// @description Makes file:/// directory ("Index of...") pages (and many server-generated index pages) actually useful. Adds sidebar and preview pane; keyboard navigation and sorting; media playback with shuffle, loop, and playlist (m3u) support; preview, edit, and save markdown/plain text files; preview images and fonts, with grid view; user-defined bookmarks; more.
// @author gaspar_schot
// @license GPL-3.0-or-later
// @homepageURL https://openuserjs.org/scripts/gaspar_schot/Supercharged_Local_Directory_File_Browser
// @contributionURL https://paypal.me/mschrauzer
// @include file://*
// @include about:blank
// @require https://code.jquery.com/jquery-latest.min.js
// @require https://cdnjs.cloudflare.com/ajax/libs/markdown-it/9.0.1/markdown-it.js
// @require https://cdnjs.cloudflare.com/ajax/libs/markdown-it-footnote/3.0.2/markdown-it-footnote.min.js
// @require https://cdn.jsdelivr.net/npm/markdown-it-toc-done-right@2.1.0/dist/markdown-it-toc-made-right.min.js
// @require https://cdn.jsdelivr.net/npm/markdown-it-sub@1.0.0/dist/markdown-it-sub.min.js
// @require https://cdn.jsdelivr.net/npm/markdown-it-sup@1.0.0/dist/markdown-it-sup.min.js
// @require https://cdn.jsdelivr.net/npm/markdown-it-deflist@2.0.3/dist/markdown-it-deflist.min.js
// @require https://cdn.jsdelivr.net/npm/markdown-it-multimd-table@3.2.3/dist/markdown-it-multimd-table.min.js
// @require https://cdn.jsdelivr.net/npm/markdown-it-center-text@1.0.4/dist/markdown-it-center-text.min.js
// @require https://cdn.jsdelivr.net/npm/opentype.js@latest/dist/opentype.min.js
// UPDATE URL
// NOTE: This script was developed in Vivaldi, running on Mac OS High Sierra. It has been tested in various Chrome and Gecko-based browsers.
// It has been minimally tested on Windows and not at all on other OSes. It should work, but please report any issues.
// The script does not work on local directories in Safari because Safari does not allow local directories to be browsed, but it will work on remote directories (or on local directories through a local server).
// NOTE: By default, Greasemonkey and Tampermonkey will not run scripts on file:/// urls, so for this script to work, you will have to enable it first.
// For Tampermonkey, go to Chrome extension page, and tick the 'Allow access to file URLs' checkbox at the Tampermonkey extension section.
// For Greasemonkey, open about:config and change greasemonkey.fileIsGreaseable to true.
// @namespace https://greasyfork.org/users/16170
// @downloadURL none
// ==/UserScript==
(function() {
'use strict';
const $ = window.jQuery;
// ***** USER SETTINGS ***** //
const $settings = {
// Paste your exported settings between the two lines below:
//--------------------------------------------------------//
bookmarks: // N.B.: Directory links must end with "/", file links must end with another character.
// You may add as many menus and links as you like; just copy the example below and edit as needed.
// Local directory bookmarks must begin with "file:///"; external bookmarks must begin with the correct protocol ("http://" or "ftp://", etc.).
// Note that because of same-origin security concerns, the browser will not allow you to navigate from an external webpage to a local directory.
// But see this page for possible workarounds: https://stackoverflow.com/questions/39007243/cannot-open-local-file-chrome-not-allowed-to-load-local-resource
[
{
"menu_title":"My Sample Menu",
"links":
[
{ "link_name":"My Directory Link 1", "link":"file:///Path/To/My/Directory/" },
{ "link_name":"My Directory Link 2", "link":"file:///Path/To/My/Directory_2/" },
{ "link_name":"My External Link", "link":"https://www.mywebpage.com/" },
{ "link_name":"My File Link", "link":"file:///Path/To/My/File.ext" },
]
},
{
"menu_title":"My Second Sample Menu",
"links":
[
{ "link_name":"My Directory Link 1", "link":"file:///Path/To/My/Directory/" },
{ "link_name":"My Directory Link 2", "link":"file:///Path/To/My/Directory_2/" },
{ "link_name":"My External Link", "link":"https://www.mywebpage.com/" },
{ "link_name":"My File Link", "link":"file:///Path/To/My/File.ext" },
]
},
],
// GENERAL USER SETTINGS
alternate_background: true, // If true (default true), alternate sidebar row background color.
apps_as_dirs: false, // Un*x/Mac OS only: if true, treat apps as directories; allows app contents to be browsed. This is the default behavior for Chrome.
// If false (default), treat apps as ignored files.
autoload_media: true, // If true (default: true), the first audio or video file found in a directory will be automatically selected and loaded for playback; also, cover art (if any, will be loaded in the preview pane).
autoload_index_files: false, // If true (default: false), automatically select first "index.xxx" (.xxx !== .htm) file found in directory.
// Note: the browser will automatically load any index.html files it finds in the directory, so the script will not work properly in such cases.
theme: 'light', // Options: 'light' or 'dark'
sort_by: 'default', // Choose from: 'name', 'size', 'date', 'kind', 'ext', 'default'.
// default = Chrome sorting: dirs on top, files alphabetical.
dirs_on_top: false, // If true, directories will always be listed firs except when sorting by "name" (since otherwise sorting by "name" would equal "default").
// If false (default), directories and files will be sorted together. (In practice, dirs will typically still be separated when sorting by size, kind, and extension.)
grid_font_size: 1, // Default = 1
grid_image_size: 184, // Default = 184 (200px - 16px)
show_details: true, // If true (default), hide file and directory details; if false, show them.
show_ignored_files: false, // If true, ignored files will appear greyed-out.
// If false (default), ignored files will be completely hidden from the file list;
ignore_ignored_files: true, // If true (default), ignored files (see "$row_settings" below, after Keybindings and Changelog) will be greyed-out (default) in the file list and will not be loaded in the content pane when selected;
// If false, ignored files will be treated like normal files, so if they are selected, the browser will attempt to download any file types it can't handle (which makes keyboard navigation inconvenient but may be useful in some circumstances).
show_invisibles: true, // Un*x/Mac OS only: If true (default), files or directories beginning with a "." will be hidden.
show_numbers: true, // If true (default true), number index items
UI_font: 'system-ui, sans-serif', // Choose an installed font for the UI; if undefined, use browser defaults instead.
UI_font_size: '13px', // Choose a default UI font size; use any standard CSS units.
use_custom_icons: true, // if true (default), use custom icons for dirs and files
// if false, use browser/server default icons
// TEXT EDITING SETTINGS
enable_text_editing: true, // If true (default), allow plain text files to be edited.
default_text_view: 'preview_text', // Options: 'source_text' or 'preview_text' for text editor.
// Note that split_view = true overrides this setting.
split_view: true, // If true, show split view on plain text file load.
// if true (default), use default preview_text setting.
sync_scroll: true // If true (default: true), show split view on plain text file load
// if false, use default preview_text setting.
//--------------------------------------------------------//
// Paste your exported settings between the above two lines.
};
// $ROW_TYPES:
// DO NOT DELETE ANY EXISTING CATEGORIES!
// Add file extensions for sorting and custom icon display to the existing categories.
// You can also define your own new categories, but do not add an extension to more than one row_type category.
// Do not add leading "." to the extensions.
const $row_types = {
// myRowType: ['ext1','ext2'],
dir: ['/'],
app: ['app/','app','bat','cgi','com','exe','jar','msi','wsf'],
alias: ['alias','desktop','directory','lnk','symlink'],
archive: ['7z','archive','b6z','bin','bzip','bz2','cbr','dmg','gz','iso','mpkg','pkg','rar','sit','sitx','tar','tar.gz','zip','zipx'],
audio: ['aac','aif','aiff','ape','flac','m4a','mp3','ogg','opus','wav'],
bin: ['a','dll','dylib','icc','msi','o'],
code: ['bak','bash','bash_profile','bashrc','c','cfg','cnf','codes','coffee','conf','csh','cshrc','cson','css','custom_aliases','default','dist','editorconfig','emacs','example','gemspec','gitconfig','gitignore','gitignore_global','h','hd','ini','js','json','jsx','less','list','local','login','logout','lua','mkshrc','old','php','pl','plist','pre-oh-my-zsh','profile','pth','py','rb','rc','rdoc','sass','settings','sh','strings','taskrc','tcl','viminfo','vimrc','vue','xml','yaml','yml','zlogin','zlogout','zpreztorc','zprofile','zsh','zshenv','zshrc'],
database: ['accdb','db','dbf','mdb','pdb','sql', 'sqlite','sqlitedb','sqlite3'],
ebook: ['azw','azw1','azw3','azw4','epub','ibook','kfx','mobi','tpz'],
font: ['otf','ttf','woff','woff2','afm','pfb','pfm','tfm'],
graphics: ['afdesign','ai','book','dtp','eps','fm','icml','idml','indd','indt','inx','mif','pmd','pub','qxb','qxd','qxp','sla','swf'],
htm: ['htm','html','xhtm','xhtml'],
image: ['apng','bmp','gif','ico','jpeg','jpg','png','svg','webp'],
ignored_image: ['ai','arw','cr2','dng','eps','jpf','nef','psd','psd','raw','tif','tiff'],
markdown: ['md','markdown','mdown','mkdn','mkd','mdwn','mdtxt','mdtext'],
office: ['csv','doc','docx','epub','key','numbers','odf','ods','odt','pages','rtf','scriv','wpd','wps','xlr','xls','xlsx','xlm'],
pdf: ['pdf'],
system: ['DS_Store','ds_store','icon','ics'],
text: ['log','nfo','txt','m3u'],
video: ['m4v','mov','mp4','mpeg','webm']
};
// $ROW_SETTINGS: Ignore or Exclude files by extension
const $row_settings = {
// Ignore: $row_types or files with extensions added here will not be loaded if selected in the sidebar (prevents the browser from attempting to download the file).
ignore: $row_types.archive.concat( 'alias', $row_types.bin, $row_types.database, $row_types.graphics, $row_types.ignored_image, $row_types.office, $row_types.system),
// Exclude: Files with these exensions will not be inverted in dark mode
exclude: ['htm','html','xhtm','xhtml']
};
// ***** END USER SETTINGS ***** //
// ## FEATURES INCLUDE:
// - Resizable sidebar and directory/file preview pane.
// - Arrow navigation in sidebar:
// - Up and Down Arrows select next/prev item.
// - Left and Right Arrows select next/prev item of same type.
// - Navigate sidebar by typed string.
// - Show/Hide file details (size (if avail), date modified (if avail), kind, extension).
// - Sort sidebar items by name or file details.
// - Default sort = sort by name with folders on top.
// - Preview all file types supported by browser (html, text, images, pdf, audio, video, etc.) and preview fonts.
// - Preview and edit markdown and plain text files, with option to save files locally.
// - Markdown rendered with markdownit.js ( https://github.com/markdown-it/markdown-it ).
// - Uses Github Markdown styles for preview ( https://github.com/sindresorhus/github-markdown-css ), with a few customizations.
// - Support for:
// - TOC creation ( `${toc}` ) ( https://github.com/nagaozen/markdown-it-toc-done-right )
// - Multimarkdown table syntax ( https://github.com/RedBug312/markdown-it-multimd-table )
// - Live checkboxes ( `\[ ], [x]` ), allowed in lists and deflists.
// - Superscript ( `^sup^` ) ( https://github.com/markdown-it/markdown-it-sup )
// - Subscript ( `~sub~` ) ( https://github.com/markdown-it/markdown-it-sub )
// - Definition lists ( https://github.com/markdown-it/markdown-it-deflist; for syntax, see http://pandoc.org/MANUAL.html#definition-lists )
// - Centered text ( `->centered<-` ) ( https://github.com/jay-hodgson/markdown-it-center-text )
// - Footnotes ( https://github.com/markdown-it/markdown-it-footnote )
// - View source text, preview, or split pane with proportional sync scroll.
// - Save edited source text or previewed HTML.
// - Create and edit text in separate text editor.
// - Audio and video playback, with shuffle, loop, skip audio +/- 10 or 30 sec via keyboard.
// - Preview other files (e.g., lyrics or cover art) in same directory while playing audio.
// - User setting to autoload cover art (if any images in directory, load "cover.ext" or first image found)
// - Grid view for images and fonts.
// - User settings (see $settings in code; some settings can be changed via the main menu in the UI and will be remembered in URL query):
// - Light or Dark theme.
// - Bookmarks for local or remote directories.
// - Default image grid size.
// - Default UI font size and font-family.
// - Default UI font and font-size.
// - Default file sorting.
// - Sort with directories on top.
// - Treat apps as directories (MacOS and *nix only)
// - Show or hide invisible files.
// - Show or hide ignored files in the ignored files list (see $row_settings in code below $settings).
// - Show or hide file details.
// - Use custom file icons or browser defaults.
// - Autoload index.ext files.
// - Autoload cover art in directories with audio files.
// - Text editing default view: split, source, or preview.
// - Text editing sync scroll: on or off.
// ## KEYBINDINGS (These don't work in all browsers):
// - Arrow Up/Down: Select prev/next item.
// - If audio is playing, and prev/next file is also audio, it will be highlighted but not loaded in the audio player; press return to load it.
// - Arrow Left/Right: Select prev/next row of the same kind as the current selection.
// - If current selection is a media file, select and begin playback of the next media item.
// - Opt/Alt + Arrow Left/Right: Skip audio ±10s
// - Opt/Alt + Shift + Arrow Left/Right: Skip audio ±30s
// - Cmd/Ctrl + Arrow Up: Go to parent directory
// - Cmd/Ctrl + Arrow Down: Open selected directory
// - Return: Open selected directory, select file, or pause/play media.
// - Space: Pause/Play media files
// - Cmd/Ctrl + D: Toggle file details (size, date modified) in some index page types.
// - Cmd/Ctrl + E: Show text editor.
// - Cmd/Ctrl + G: Show or Reset Grid.
// - Cmd/Ctrl + I: Toggle Invisibles.
// - Cmd/Ctrl + Shift + O: Open selected item in new window/tab.
// - Cmd/Ctrl + R: Reload grids and previewed content, reset scaled images/fonts, reset media files to beginning.
// - Cmd/Ctrl + W: Close previewed content (doesn't work in all browsers; use close button instead), or close window if no content is being previewed.
// - Cmd/Ctrl + Shift + < or >: Scale preview items and grids.
// CHANGELOG:
// **VERSION 4.1.3.2**
// **FIXED:** Image grid didn't show SVG files if width and height were set to 100%.
// Added custom folder favicon for local directories.
// Other small style tweaks.
// **VERSION 4.1.3.1**
// **FIXED:** Images couldn't be scaled to less than 24 x 24px.
// **VERSION 4.1.3**
// **FIXED:** _Finally_ fixed image scaling and zooming. Sorry for the delay.
// **IMPROVED:** Allow image scaling > 100%.
// **IMPROVED:** Show image scale percentage in the content title bar.
// **IMPROVED:** Show custom file icons in content title bar.
// **IMPROVED:** Highlight grid button when grid is loaded (or hidden).
// **FIXED:** Invisible files were being selected even if "Show invisibles" was unchecked.
// Various other small fixes and style tweaks.
// **VERSION 4.1.2**
// **FIXED, CHANGED, & IMPROVED:** Image, font, and font glyph grids.
// - Added up/down arrow navigation. This is a change from the previous behavior, where up/down arrows always navigated the sidebar. This will also work if the grid is hidden after selecting an image or font glyph.
// - Also added arrow navigation for font glyphs, and :hover and selected styles.
// - Fixed a deeply-buried bug that prevented the selected grid item from scrolling into view with arrow navigation.
// - Fixed a stupid bug that prevented the first font in the directory from being included in the grid.
// - Fixed some problems with normal font file browsing after viewing font glyphs.
// - Improved styling for previewed and zoomed images.
//
// **FIXED and IMPROVED:** Playlists.
// - Window title and sidebar head are now reset to indicate presence of playlist.
// - First track wasn't being selected when "autoload_media" = true.
// - Directory stats weren't being reset after closing a playlist.
// - Grid button and "show invisibles" checkbox are hidden with open playlist.
//
// **IMPROVED:** Currently selected sidebar item will now scroll into view after various events, like sorting change, showing/hiding details, resizing sidebar, etc.
// **FIXED:** Several menu items weren't working: Default User Settings, Export User settings, Contact, and [Donate](https://paypal.me/mschrauzer) (that might explain a few things...).
// **FIXED:** Document title didn't include entire path.
// Many other small bug fixes and style tweaks, including some specifically for Firefox and Safari.
// **VERSION 4.1.1**
// A few small fixes and style tweaks.
// **VERSION 4.1.0**
// **NEW:** Basic support for media playlists (.m3u and .m3u8).
// - Added "Open Playlist..." item to the main menu.
// - Playlist items will replace the current directory items in the sidebar. Times (if available) will be displayed in the "size" column. "Default" sorting = original playlist sort.
// - Playlist can be closed via the "Close" button or shortcut, and the previous directory contents will be loaded.
// - Streaming links are not supported.
// - Beware of cross-origin limitations. For example, if your playlist includes locally-hosted media files, you will need to load it from a file:/// page in your browser.
// - For remote files, if you are using a javascript-blocker (like uMatrix or NoScript), you may have to allow scripts from the hosting site (e.g., archive.org) in order for playback to work.
// **NEW:** Open local fonts directly and view font information and complete glyph repertoire. (The previous ability to browse fonts in the directory list is unchanged.)
// - Added "Open Font..." item in the menu item.
// - View individual glyphs and save as SVG.
// **FIXED:** Apps weren't being properly classified in the index.
// **FIXED:** An issue with formatting the current directory name in the sidebar header.
// **IMPROVED:** Refreshed UI colors and icons; added icons for more file types.
// **IMPROVED:** Many styling adjustments, including setting numbers (for sizes and date, etc.) to tabular spacing.
// **CHANGED:** Renamed "shortcuts" user setting to "bookmarks"; if you use exported settings, you'll have to change this in your code.
// **INTERNALS:** Prettified and modularized some code. Removed some newly-unnecessary functions. Began to prune CSS.
// - Updated markdown-it to 9.1.0.
// ***** GENERAL SETUP ***** //
// ************************************ //
// DON'T EDIT ANYTHING BELOW THIS LINE. //
// ************************************ //
// PATHS
// Fix "%" error in file name; see https://stackoverflow.com/questions/7449588/why-does-decodeuricomponent-lock-up-my-browser
function decodeURIComponentSafe(s) {
if ( !s ) { return s; }
return decodeURIComponent(s.replace(/%(?![0-9a-fA-F]{2})/g, '%25') ); // replace % with %25 if not followed by two a-f/number
}
const $protocol = window.location.protocol;
const $origin = $protocol +'//'+ window.location.host;
const $location = decodeURIComponentSafe( [location.protocol, '//', location.host, location.pathname].join('') );
const $current_dir_path = $location.replace(/([\/|_|—])/g,'$1').replace(/\\/g,'/'); // URL w/o query string for display
function escapeStr(str) { str = str.replace(/([\^\$\|\?\*\+\(\)\[])/g,'\$1'); }
// if URL is a file, change window location to parent dir, add querystring of file name; then autoload file.
function loadFile() {
if ( $location.slice($location.lastIndexOf('/')).indexOf('.') !== -1 && !$location.endsWith('/') && window.top === window.self ) {
let $query_prefs = getQueryPrefs();
$query_prefs.set( 'file', $location.slice($location.lastIndexOf('/') + 1) );
window.location = $location.slice(0,$location.lastIndexOf('/') + 1) +'?'+ $query_prefs;
return;
}
}
loadFile();
// QUERY PREFS
function getQueryPrefs() { return new URL(window.location).searchParams; }
// const initialQueryPrefs = getQueryPrefs();
// set query key/value
function setQuery(key, value) {
let $query_prefs = getQueryPrefs();
$query_prefs.set( key, value );
updateQuery($query_prefs);
}
// get query value
function getQuery(key) {
let $query_prefs = getQueryPrefs();
let value = '';
if ( key === 'width' ) {
value = ( !$query_prefs.has(key) ? 30 : Math.round(100 * Number.parseInt($query_prefs.get('width'))/window.innerWidth) ); // number string
} else {
value = ( $query_prefs.has(key) ? $query_prefs.get(key) : $settings[key] !== undefined ? $settings[key].toString() : '' );
value = value.replace('%2F','').replace('/',''); // some servers add a '/' to end of query string
}
return value;
}
// toggle query key
function toggleQuery(key) {
let $query_prefs = getQueryPrefs();
let nonBoolPrefs = {
'theme_light': {'theme':'dark'},
'theme_dark': {'theme':'light'},
'source_text': {'default_text_view':'preview_text'},
'preview_text': {'default_text_view':'source_text'},
'sort_by_default': {'sort_by':'default'},
'sort_by_name': {'sort_by':'name'},
'sort_by_size': {'sort_by':'size'},
'sort_by_date': {'sort_by':'date'},
'sort_by_kind': {'sort_by':'kind'},
'sort_by_ext': {'sort_by':'ext'}
};
var value, queryValue, settingsValue;
if ( nonBoolPrefs[key] !== undefined ) {
value = Object.values(nonBoolPrefs[key]).toString();
key = Object.keys(nonBoolPrefs[key]).toString(); // must come after value: i.e., don't redefine key before getting value
if ( $settings[key] === value ) { $query_prefs.delete( key ); } else { $query_prefs.set( key, value ); }
} else {
queryValue = $query_prefs.get(key);
settingsValue = $settings[key];
value = ( queryValue === null ? settingsValue.toString() : queryValue.toString() );
value = ( value === 'true' ? 'false' : 'true' );
if ( ( queryValue !== null && queryValue !== settingsValue ) ) {
$query_prefs.delete( key );
} else {
$query_prefs.set( key, value );
}
}
updateQuery($query_prefs);
}
// remove query key
function removeQuery(key) {
let $query_prefs = getQueryPrefs();
$query_prefs.delete(key);
updateQuery($query_prefs);
}
// update query string
function updateQuery(querystr) {
querystr = querystr.toString().replace('%2F','').replace('/','');
window.history.replaceState({}, document.title, window.location.pathname +'?'+ querystr);
updateParentLinks();
}
// ***** SET UP UI ELEMENTS ***** //
// SIDEBAR ELEMENTS
// ***** BUILD MENUS ***** //
// Parent and Parents Menus
function updateQueryStr(str) {
str = str.replace(/([^\?]*)selected=[^&]*(.*)$/m,'$1$2') // delete current selected query, if any
.replace(/([^\?]*)history=(\d+)\+*([^&]*)(&*)(.*)/m,'$1$4$5&selected=$2&history=$3').replace(/&{2,}/g,'&').replace(/\?&/m,'\?'); // format query with selected and history at end
if ( str.endsWith('&history=') ) { str = str.replace(/(.+)&history=$/m,'$1'); } // if no history, delete query
return str;
}
// create links
function createParentLinks() {
let $links = [];
let str = decodeURIComponentSafe(window.location.search);
str = str.replace('/','').replace('%2F','');
let $linkPieces = $location.split('/');
$linkPieces = $linkPieces.slice(2,-2); // remove beginning and ending empty elements and current directory
while ( $linkPieces.length > 0 ) { // while there are link pieces...
str = updateQueryStr(str); // update selected and history
let link = $protocol +'//'+ $linkPieces.join('/') +'/'+ str; // assemble link
$links.push(link); // add to link array
$linkPieces.pop(); // remove last link piece and repeat...
}
return $links;
}
// create menu items
function createParentLinkItems() {
let $parent_link_menu_items = [];
let $links = createParentLinks();
$('#parent_dir_menu').find('a').attr( 'href', $links[0] ); // set parent link
for ( let i = 0; i < $links.length; i++ ) {
let display_name = $links[i].slice(0,$links[i].lastIndexOf('?') - 1);
display_name = display_name.replace(/\//g,'\/');
let menu_item = '
';
let sidebar_header_els = sidebar_header;
return sidebar_header_els;
};
// Dir List Elements
const SidebarDirListEls = function() {
let dir_list_body = '';
let dir_list_foot = '
';
let sidebar_dir_list_els = '
'+ dir_list_body + dir_list_foot +'
';
return sidebar_dir_list_els;
};
// CONTENT PANE ELEMENTS
const ContentHeaderEls = function() {
let title_buttons_left = '
';
let title = '
';
let title_buttons_right = '
';
let content_title = '
'+ title_buttons_left + title + title_buttons_right +'
';
let content_header_els = '
'+ content_title + ContentAudioEls() +'
';
return content_header_els;
};
// Content containers
const ContentEls = function() {
let content_grid = '';
let content_text = '';
let content_font = '
'+ ContentFontEls() +'
';
let content_image = '
';
let content_video = '';
let content_pdf = '';
let content_iframe = '';
let content_els = ''+ content_grid + content_text + content_font + content_image + content_pdf + content_video + content_iframe +'';
return content_els;
};
// Content Audio Els
const ContentAudioEls = function() {
let prev_track = '
';
let next_track = '
';
let audio_player = '';
let loop = '';
let shuffle = '';
let checkbox_cont = '
'+ loop + shuffle +'
';
let close_audio = '';
let content_audio_title = '
';
return content_audio_els;
};
// Content Font Els
const ContentFontEls = function() {
let sample_string = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz 0123456789 [(!@#$%^&*;:)]';
let specimen = '
'+ sample_string +'
';
let hamburger_string = '
Typography
The art of using types to produce impressions on paper, vellum, &c.
S P E C I M E N
Typography is the work of typesetters (also known as compositors), typographers, graphic designers, art directors, manga artists, comic book artists, graffiti artists, and, now, anyone who arranges words, letters, numbers, and symbols for publication, display, or distribution.
';
let lorem_string = 'Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.';
let lorem = '
'+ lorem_string +'
';
let lorem2 = lorem.replace('style="','style="columns:2;');
let lorem3 = lorem.replace('style="','style="columns:3;');
let hamburger = '
' );
return grid_item;
}
}
// OPEN FONT
/// opentype.js font parsing
$('#open_font_label').on('click',function(e) { $('.menu').hide(); });
// Open font
$('#bookmarks').on('change','#open_font',function(e) {
openFont(e);
});
function openFont(evt) {
if (window.File && window.FileReader && window.FileList && window.Blob && window.opentype ) {
let files = evt.target.files[0];
let reader = new FileReader();
reader.readAsArrayBuffer(files);
reader.onload = function(file) {
closeOtherContent();
closeGrid();
hideEditorOrGrid();
$content_pane.addClass('has_font');
$content_font.addClass('has_content has_font_viewer').removeClass('has_font');
parseFont(reader.result);
return true;
};
$('#open_font').val(''); // reset input to allow same font to be reopened immediately after closing.
} else {
alert('File APIs are not fully supported in this browser or opentype.js is not available.');
}
}
// Parse font
function parseFont(fontblob) {
let font = window.opentype.parse(fontblob);
getFontInfo(font);
let $glyphs_container = $('#glyphs_container');
$glyphs_container.empty();
let $glyph_container = $('');
let $glyph_canvas = $('');
let $glyph_info = $('');
let $glyph_viewer = $('#glyph_viewer');
$glyph_viewer.data('data-font-name',font.names.fullName.en);
let glyphs = font.glyphs;
$content_font.data('data-glyphs',glyphs); // add glyphs data to $content_font
// Draw glyphs
for ( let i = 0; i < glyphs.length; i++ ) {
let glyph = glyphs.glyphs[i];
// GLyph width
let boundingBox = glyph.getBoundingBox();
let glyphWidth = boundingBox.x2 - boundingBox.x1;
let contextX = (60 - glyphWidth/24);
// Add glyph info and append elements
let glyphUnicode = ( glyph.unicode !== undefined ? '#'+ glyph.unicode : glyph.unicode );
let glyphContainer = $glyph_container.clone();
let glyphCanvas = $glyph_canvas.clone();
let glyphInfo = $glyph_info.clone();
glyphInfo.text(glyph.index +': '+ glyph.name +', '+ glyphUnicode);
glyphContainer.append( glyphCanvas.clone().attr('id','glyph_'+ glyph.index ) ).append(glyphInfo);
$glyphs_container.append( glyphContainer );
// Draw glyph
let thisGlyph = document.getElementById('glyph_'+ glyph.index);
$(thisGlyph).data('contextX',contextX);
let context = thisGlyph.getContext('2d');
glyph.draw(context, contextX, 84, 72);
}
$('#title').removeAttr('data-after').find('span').empty().html(font.names.fullName.en);
setContentHeight();
}
// Get font info
function getFontInfo(font) {
let $font_names = font.names;
let $font_info = $('
FONT INFO: '+ font.names.fullName.en.toUpperCase() +'
');
for (let name in $font_names) {
let value = $font_names[name].en;
if ( name.endsWith('URL') ) {
let href = value;
if ( !value.startsWith('http') ) {
href = 'http://'+ value;
}
value = ''+ value +'';
}
$font_info.find('tbody').append('
'+ name +':
'+ value +'
');
}
let numGlyphs = font.numGlyphs; // glyph count
$font_info.find('tbody').append('
');
buttonsCont.append(toggleSrcBtn, togglePreviewBtn, toggleSplitBtn, syncScrollEl, saveBtn, clearTextBtn);
const textEditingUI = '';
// append the UI to the container_el
$(id).prepend(buttonsCont).append(textEditingUI);
}
// MD Set up UI
function MDsetupTextEditingUI(id,sourceText) {
$(id).find('pre').first().remove();
$(id).find('#content_source').val(sourceText); // set source text from pre
if ( getQuery('split_view') === 'true' ) { $('body').addClass('split_view'); } else { $('body').removeClass('split_view'); }
if ( getQuery('default_text_view') === 'preview' ) { $('body').addClass('preview_text').removeClass('source_text'); } else { $('body').addClass('source_text').removeClass('preview_text'); }
if ( getQuery('sync_scroll') === 'true' ) { $('#sync_scroll input').prop({checked:true}); }
}
// MD UI Buttons functions
function MDtoolBarFunctions(id) {
let $thisFileName;
let container_el = $(getElById(id)).closest('body');
let sourceEl = container_el.find('#content_source');
let previewEl = container_el.find('#content_preview');
if ( $body.hasClass('has_text') ) {
$thisFileName = 'untitled';
} else {
$thisFileName = decodeURI(window.location.pathname.slice(window.location.pathname.lastIndexOf('/') + 1));
}
const $saveHTMLOpen = '';
const $saveHTMLClose = '';
switch (id) {
case 'toggle_split':
$('body').toggleClass('split_view').find('#content_source,#content_preview,#text_editing_handle').attr('style','');
if ( container_el.hasClass('source_text') ) {
sourceEl.focus();
document.getElementById('content_source').setSelectionRange(0,0);
}
break;
case 'show_source':
container_el.removeClass('split_view preview_text').addClass('source_text').find('#content_source,#content_preview,#text_editing_handle').attr('style',''); // remove styles in case split has been resized
sourceEl.css({'width':'100%'}).focus();
document.getElementById('content_source').setSelectionRange(0,0);
break;
case 'show_preview':
container_el.removeClass('split_view source_text').addClass('preview_text').find('#content_source,#content_preview,#text_editing_handle').attr('style','');
break;
case 'clear_text':
container_el.addClass('has_warning').find('#warnings').removeClass().addClass('clear');
break;
case 'save_text':
saveMD( $thisFileName, sourceEl.val() );
break;
case 'save_HTML':
saveMD( $thisFileName.slice(0,$thisFileName.lastIndexOf('.') + 1) + 'html', $saveHTMLOpen + MDprepHTML(previewEl.html()) + $saveHTMLClose );
break;
}
}
// MD Custom pre- and post-processing for text.
function MDaddHeaderIDs(match, p1, p2, p3, offset, string) { // create header ids for TOC
return ''+ p3;
}
function MDcustomPreProcess(src) {
return src; // we're not doing anything here just yet...
}
function MDcustomPostProcess(html) {
html = html.replace(/<(p|li|dt|dd)>\-*\s*\[\s*x\s*\]\s*(.+?)<\/(p|li|dt|dd)>$/gm,'<$1 class="checklist">$3>') // checkboxes in p,li,dt,dd
.replace(/<(p|li|dt|dd)>-*\s*\[\s{1,}\]\s*(.+?)<\/(p|li|dt|dd)>$/gm,'<$1 class="checklist">$3>') // checkboxes
// .replace(/
"/g,'
')
.replace(/^]*)>([^<]+)/gm, MDaddHeaderIDs) // add header IDs;
.replace(/<\/h(\d)>/g,'↑');
return html;
}
//MD Render markdown from preprocessed source text
function MDmarkdown(sourceText,previewEl) {
const MDit = window.markdownit({linkify:false,typography:false,html:true})
.use(window.markdownitMultimdTable, {enableMultilineRows: true})
.use(window.markdownitSub)
.use(window.markdownitSup)
.use(window.markdownitFootnote)
.use(window.markdownitCentertext)
.use(window.markdownitDeflist)
.use(window.markdownitTocDoneRight)
;
let MDpreview = MDit.render( MDcustomPreProcess( sourceText ) );
previewEl.html( MDcustomPostProcess( MDpreview ) ); // set previewed html
}
// MD Live preview, add edited warning
function MDlivePreview(sourceEl,previewEl) {
MDmarkdown( sourceEl.val(),previewEl );
MDsetChecklistClass();
}
// MD Live Checkboxes prep: find each instance of [ ] or [x] and replace text in index = to clicked checkbox in Preview.
function MDreplaceAt(str, replacement, position) {
str = str.substring(0, position) + replacement + str.substring(position + replacement.length);
return str;
}
function MDreplaceNthSubStr(str,substr,replacement,index) {
let count = 0;
let found = substr.exec(str);
while ( found !== null ) {
if ( count === index ) {
return MDreplaceAt(str, replacement, found.index );
} else {
count++;
found = substr.exec(str);
}
}
}
// MD Live Checkboxes
function MDliveCheckBoxes(checkbox,sourceEl,previewEl) {
$('.checklist').removeClass('clicked');
checkbox.closest('p,li,dt,dd').addClass('clicked');
const thisIndex = previewEl.find('.checklist').index( $('.clicked') );
const srctext = sourceEl.val();
const substr = new RegExp(/\[\s*.\s*\]/g);
const replacement = ( checkbox.is(':checked') ? '[x]' : '[ ]' );
sourceEl.val( MDreplaceNthSubStr(srctext, substr, replacement, thisIndex) );
}
// MD Checkbox list class: Prevent checkbox lists from having list bullets
function MDsetChecklistClass() {
$('input[type="checkbox"]').closest('ul').addClass('no_list');
}
// MD Resize Split View
function MDresizeSplit(handle,sourceEl,previewEl) {
let $sidebarWidth = $('#sidebar').outerWidth();
let $pageWidth = window.innerWidth;
$(document).on('mousemove',function(e) {
e.stopPropagation();
e.preventDefault();
let pageX = e.pageX;
if ( pageX > $sidebarWidth + 100 && pageX < $pageWidth - 100 ) { // min widths
handle.css({'left': pageX - $sidebarWidth - 4 + 'px'});
sourceEl.css({'width': pageX - $sidebarWidth + 'px'});
previewEl.css({'left': sourceEl.outerWidth() + 'px'});
}
});
handle.on('mouseup',function() {
$(document).off('mousemove');
});
}
// MD UI Sync Scroll
function MDpercentage(el) { return (el.scrollTop / (el.scrollHeight - el.offsetHeight)); }
function MDsyncScroll(el1) {
let el2 = ( el1.getAttribute('id') === 'content_preview' ? document.getElementById('content_source') : document.getElementById('content_preview') );
if ( document.querySelector('input[name="sync_scroll"').checked ) {
el2.scrollTo( 0, (MDpercentage(el1) * (el2.scrollHeight - el2.offsetHeight)).toFixed(0) ); // toFixed(0) prevents scrolling feedback loop
}
}
// click TOC anchors
function MDtocClick(el,previewEl) {
let thisId = el.attr('href');
if ( thisId ) {
previewEl.scrollTop( $(thisId).offset().top - 48 );
}
}
// click Headers to return to TOC or top
function MDheaderClick(previewEl) {
if ( previewEl.find('.table-of-contents').length > 0 ) {
document.getElementsByClassName('table-of-contents')[0].scrollIntoView(true);
} else {
document.getElementById('preview').scroll(0,0);
}
}
// MD Clear text source
function clearText(container_el) {
if ( window.top !== window.self ) { // if iframe, send message to top (to remove iframe_edited class)
sendMessage('top','clear');
}
container_el.find('#content_source').show().focus().val('');
container_el.find('#content_preview').empty();
container_el.removeClass('edited has_warning');
}
// MD SAVE SOURCE or HTML
function MDprepHTML(data) {
data = data.replace(/.<\/span>/g,'');
return data;
}
function saveMD(filename, data) {
let blob = new Blob([data], {type: 'text/plain'});
let downloadEl = window.document.createElement('a');
downloadEl.href = window.URL.createObjectURL(blob);
downloadEl.download = filename;
document.body.appendChild(downloadEl);
downloadEl.click();
document.body.removeChild(downloadEl);
URL.revokeObjectURL(blob);
if ( window.top !== window.self ) { // if iframe, send message to top
sendMessage('top','clear');
}
$('body,#content_source,#content_text').removeClass('edited');
}
// list of functions to remember while sending messages and then execute after warning button click
function doFunction(funcName,args) {
var funcDictionary = { 'setLocation':setLocation, 'resetContent':resetContent, 'closeContent':closeContent, 'clickThis':clickThis, 'clickRow':clickRow, 'doubleClickRow':doubleClickRow, 'indexNavigation':indexNavigation, 'clearText':clearText, 'null':null };
return funcName === 'null' ? null : funcDictionary[funcName](args);
}
// Show warning after certain user actions if text editor or iframe has edited text; otherwise do the action.
function showWarning(funcName,args) {
// Don't show the warning if func = indexNavigation or clickRow; i.e., just hide text editor;
// In other words, only show warning when changing directories or if iframe content has been edited
if ( ( $('body').hasClass('edited') && funcName !== 'indexNavigation' && funcName !== 'clickRow' ) || $('body').hasClass('iframe_edited') ) {
if ( $('body').hasClass('edited') ) { // show warning and text editor (if hidden)
$body.addClass('has_warning').find('#warnings').removeClass().addClass('unloading');
$body.removeClass('has_hidden_text').addClass('has_text');
}
if ( $('body').hasClass('iframe_edited') ) { // if iframe is edited, send unloading message
sendMessage('iframe','unloading',funcName,args); // upon receipt of message, iframe will show its warning message, based on the funcName
}
} else {
doFunction(funcName,args);
}
}
// Send a message to iframe or parent
function sendMessage(target,message,funcName,args) {
var messageObj = { 'messageContent': message, 'functionName': funcName, 'arguments': args };
if ( target === 'iframe' ) {
let contentIFrame = document.getElementById('content_iframe');
contentIFrame.contentWindow.postMessage( messageObj, '*' );
}
if ( target === 'top' ) {
window.parent.postMessage( messageObj, '*');
}
}
// Receive a message from iframe or parent, do appropriate action
function receiveMessage(e) {
if ( e.origin === 'null' || e.origin === $origin ) {
let $message = e.data.messageContent;
let funcName = e.data.functionName;
let args = e.data.arguments;
if ( $message === 'split_view' ) {
$iframe_body.toggleClass('split_view');
}
if ( $message === 'default_text_view' ) {
$iframe_body.toggleClass('preview_text source_text').removeClass('split_view');
}
// warn iframe that user wants to change iframes
if ( $message === 'unloading' && !$iframe_body.hasClass('has_warning') ) {
$iframe_body.addClass('has_warning').find('#warnings').removeClass().addClass('unloading').attr('data-function_name',funcName).attr('data-args',args);
}
// let top know iframe text has been edited
if ( $message === 'iframe_edited' && !$('body#top').hasClass('iframe_edited') ) {
$('body#top').addClass('iframe_edited');
}
if ( $message === 'ignore' || $message === 'clear' ) {
$('body#top').removeClass('iframe_edited');
if ( $message === 'ignore' ) { doFunction(funcName,args); }
}
}
}
window.addEventListener('message',receiveMessage,false);
// Edited Warning buttons: what to do when the user clicks a warning button
function editedWarningButtons(id) {
let btn = $(document.getElementById(id));
let container_el = btn.closest('body');
let func = $('#warnings').attr('data-function_name');
let args = $('#warnings').attr('data-args');
switch(id) {
case 'warning_ignore_btn': // do the user initiated func without saving the edited text
if ( window.self !== window.top ) { // if iframe, send message to top
sendMessage('top','ignore',func,args);
}
container_el.removeClass('edited has_text has_warning');
clearText(container_el);
break;
case 'warning_cancel_btn': // cancel the func
container_el.removeClass('has_warning').find('#warnings').removeClass();
break;
case 'warning_clear_btn': // clear the text editor
clearText(container_el);
break;
case 'warning_save_btn': // save the text
if ( window.top !== window.self ) { // if iframe, send message to top
sendMessage('top','clear');
}
container_el.removeClass('edited has_warning');
$('#save_text').click();
break;
case 'warning_ok_btn': // clear the text editor
$('body').removeClass('has_warning').find('#warnings').removeClass('local');
break;
}
}
$('#warnings').on('click','button',function(e) {
e.preventDefault();
editedWarningButtons( $(this).attr('id') );
});
// Edited Warning overlay: prevent user clicks on rest of UI
$('#overlay').on('click mousedown mouseup',function(e) {
e.preventDefault();
e.stopPropagation();
return;
});
// END Text Editing
// EXPERIMENTAL: Open playlist
$('#open_playlist_label').on('click',function(e) { $('.menu').hide(); });
// Open font
$('#bookmarks').on('change','#open_playlist',function(e) {
openPlaylist(e);
});
function convertPlaylist(items) {
let preppedIndex = '';
let preppedRow = '';
let id = 0, rows, info, title, time = '0', display_time = '—', link, kind = '', display_kind, ext;
items = items.replace(/\s*#EXTM3U.*\n/,'\n').replace(/^\*\n{2,}/gm,'\n');
if ( items.indexOf('#EXTINF:') !== -1 ) {
rows = items.split('#EXTINF:');
} else {
rows = items.split('\n');
}
for ( let row of rows ) {
if ( row.indexOf('\n') !== -1 && row.trim().length > 0 ) {
row = row.trim().split('\n');
info = row[0];
time = info.slice(0,info.indexOf(','));
display_time = new Date(time * 1000).toISOString().substr(11, 8);
title = info.slice(info.indexOf(',') + 1);
link = row[1];
} else {
title = decodeURIComponentSafe(row.slice(row.lastIndexOf('/') + 1));
link = row;
}
ext = link.slice(link.lastIndexOf('.') + 1);
if ( $row_types.audio.includes( ext ) ) {
kind = 'audio';
display_kind = 'Audio';
} else if ( $row_types.video.includes( ext ) ) {
kind = 'video';
display_kind = 'Video';
}
if ( kind !== '' ) { // only allow supported media types
preppedRow = '