.m3u
files are just plain text, the browser interprets them as binary and will not open them; this is why the script has the "Open Playlist" menu item.
// However, if you change the playlist extension to .txt
, the script will be able to read the file as editable text as usual.
// What's new is that if you select such a renamed playlist in the sidebar and then double-click it or type Cmd/Ctr + Down Arrow
or Cmd/Ctr + Return
, the script will load the playlist in the sidebar.
// NOTE: The renamed .m3u file must be in EXTM3U format for this work.
// **CHANGED:** Reorganized main menu to make it more compact.
// **ADDED:** "Close Playlist" menu item and button which are visible when a playlist/filelist is open.
// **FIXED:** Cmd+W
did not close playlists.
// **ADDED:** Option to allow playback of all media files( i.e., audio and video), not just the currently selected media type.
// **ADDED:** Loop and shuffle playback options for video files.
// **FIXED:** Loop audio playback was broken.
// **ADDED:** New menu item: "Media Files", with options to "Play All Media Files," "Loop Media", and "Shuffle Media"; the latter two are useful when viewing video files.
// **FIXED:** Media tracks should scroll into view when track ends and next track begins playback.
// **FIXED:** Playlists: caps in file extensions caused incorrect file type detection.
// **FIXED:** Various issues with closing previewed content.
// **FIXED:** Setting text editing options was broken.
// **FIXED:** Handling of HTTP error pages wasn't working. Oh, the irony.
// **FIXED:** "View Sidebar Directory Source" wasn't working.
// **FIXED:** Some issues with autoloading files from file (i.e., not directory) URLs.
// **CHANGED:** Audio player now displayed at top of content pane, so that the content title is visually connected to the displayed content.
// **CHANGED:** Added .m3u
files to the list of ignored formats.
// **CHANGED:** Disabled media checkboxes in Firefox, because Firefox s*cks.
// **UPDATED** Help menu with additional information.
// **IMPROVED:** If you navigate up to a parent directory that contains audio, images, and directories, and if autoloadmedia === true, and if the script's navigation history includes the parent directory, the parent directory will be selected in the sidebar for navigation, while the audio and image file will be loaded and highlighted in the sidebar.
// **IMPROVED:** .webloc and .url files can be opened from preview pane.
// **IMPROVED:** Hovering specific items in the stats will scroll the first instance of that kind in the sidebar into view. Also limited max-height of stats and allowed scrolling.
// **OTHER:** Completely reworked the code for getting directory stats.
// **OTHER:** A few UI refinements.
// **OTHER:** More performance improvements: Removed a bunch of redundant code and function calls, reduced initial DOM manipulations, converted many strings to template literals for more efficient parsing and concatenation.
// **KNOWN ISSUES:**
// **Chrome:** Smooth scroll (for scrollIntoView() ) seems to be broken in the latest releases.
// **Firefox:** Clicking media checkbox es (to toggle selection) causes page reload.
// **Firefox:** Tabbing into previewed directories in content pane doesn't work; click in content pane instead in order to use arrow navigation.
// ************ J + M + J ************* //
// ************************************ //
// DON'T EDIT ANYTHING BELOW THIS LINE. //
// ************************************ //
// If window.location points to a file, change window location to file container dir, add search_param of file name; then load file container directory and load file in content pane.
function loadFileURL() {
let search_params = getSearchParams();
search_params.set( 'file', window.location.pathname.split('/').reverse()[0] );
window.location = window.location.pathname.slice( 0,window.location.pathname.lastIndexOf('/') ) +'/?'+ search_params ;
return;
}
if ( !window.location.pathname.endsWith('/') && window.top === window.self ) { loadFileURL(); } // load file urls
// ***** GENERAL SETUP ***** //
function getBrowser() {
switch(true) {
case navigator.userAgent.search('Chrome') >= 0: return 'is_chrome';
case navigator.userAgent.search('Firefox') >= 0: return 'is_gecko';
case navigator.userAgent.search('MSIE') >= 0: return 'is_explorer';
case navigator.userAgent.search('Opera') >= 0: return 'is_opera';
case navigator.userAgent.search('Safari') >= 0 && navigator.userAgent.search('Chrome') < 0: return 'is_safari';
}
}
function getOS() { // modded from https://stackoverflow.com/questions/38241480/detect-macos-ios-windows-android-and-linux-os-with-js
var platform = window.navigator.platform, macos_platforms = ['Macintosh', 'MacIntel', 'MacPPC', 'Mac68K'], windows_platforms = ['Win32', 'Win64', 'Windows', 'WinCE'], os = null;
switch(true) {
case macos_platforms.indexOf(platform) !== -1: os = 'macos'; break;
case windows_platforms.indexOf(platform) !== -1: os = 'windows'; break;
case !os && /Linux/.test(platform): os = 'linux'; break;
}
return os;
}
// PATHS
const newURL = function(link) {
try { return new URL(link,document.baseURI); }
catch(error) { return; } //console.log('This link is invalid. Please check the file.'); }
};
function decodeURIComponentSafe(str) { // Fix "%" error in file name; see https://stackoverflow.com/questions/7449588/why-does-decodeuricomponent-lock-up-my-browser
if ( !str ) { return str; }
try {
return decodeURIComponent(str.replace(/%(?![0-9a-fA-F]{2})/g,'%25') ); // replace % with %25 if not followed by two a-f/number
} catch(e) {
return str;
}
}
const $protocol = window.location.protocol;
const $origin = $protocol +'//'+ window.location.host;
let current_location = decodeURIComponentSafe( [location.protocol, '//', location.host, location.pathname].join('') );
const current_dir_path = current_location.replace(/([/|_|—])/g,'$1SCRIPT HOMEPAGE (openuserjs.org)
KEYBOARD SHORTCUTS | DESCRIPTION |
↑ or ↓ | Select the previous/next sidebar item or previewed directory item. If audio is playing, and the previous/next file is also audio, the file will be highlighted but not loaded in the audio player; press return to load it. |
← or → | Select prev/next item of the same kind as the current selection. If current selection is a media file, select and begin playback of the next media item. |
⌘/Ctrl + ↑ | Go to parent directory |
⌘/Ctrl + ↓ | Open selected sidebar directory |
⌘/Ctrl + → | Open selected subdirectory in sidebar. |
⌘/Ctrl + ← | Close selected subdirectory in sidebar or jump to parent directory. |
⌥ + ← or → | Skip audio/video ±10s |
⌥ + ⇧ + ← or → | Skip audio/video ±30s |
Escape | Close menus and help, unfocus textareas and content pane, etc. |
Return | Open selected directory, select file, or pause/play media. |
Space | Pause/Play media files (if media player loaded). |
Tab | Toggle focus between sidebar and content pane. |
⌘/Ctrl + D | Toggle file details (size, date modified, kind) in some index page types. |
⌘/Ctrl + E | Toggle main menu. |
⇧ + ⌘/Ctrl + E | Show text editor. |
⌘/Ctrl + G | Show or reload image or font grids. |
⌘/Ctrl + I | Toggle invisible files. |
⇧ + ⌘/Ctrl + O | Open selected sidebar item in new window/tab. |
⌘/Ctrl + R | Reload grids and previewed content, reset scaled images/fonts, reset media files to beginning. |
⌘/Ctrl + W | Close previewed content (doesn't work in all browsers; use close button instead), or close window if no content is being previewed. |
⌘/Ctrl + ⇧ + < or > | Scale preview items and grids. |
⌘/Ctrl + \\ | Toggle sidebar. |
⇧ + ⌘/Ctrl + \\ | Toggle text editor split view. |
Cmd/Ctr + Down Arrow
or Cmd/Ctr + Return
, will open the playlist/filelist in the sidebar.${ total } Items: ${ total_dirs } Directories, ${ total_files } Files | ||||||
---|---|---|---|---|---|---|
${ total } Items (${ total_dirs_invisible + total_files_invisible } ignored or invisible) | ||||||
Dirs (${ total_dirs_invisible } ignored or invisible) | ||||||
Files (${ total_files_invisible } ignored or invisible) | ||||||
${ display_name.toUpperCase() } ${ display_name.slice(0,font_family.lastIndexOf(".")) }` ); return $grid_item; } } // Open Font File (req: opentype.js font parsing) $('#open_font_label').on('click',function(e) { e.stopPropagation(); }); // Open font $('#menu').on('change','#open_font',function(e) { $('body').removeClass('has_menu faded'); openFile(e,'font'); }); // Open Font File function openFontFile(files,reader) { closeContent(); hideGrid(); $content_pane.attr('data-content','has_font_file'); $content_font.addClass('has_content'); parseFont(reader.result); $('#title span').html( files.name ); $('#open_font').val(''); // reset input to allow same font to be reopened immediately after closing. focusContent(); } // Parse font (req opentype.js) function parseFont(fontblob) { let font = window.opentype.parse(fontblob); getFontInfo(font); let $glyphs_container = $('#glyphs_container'); $glyphs_container.empty(); let $glyph_container_el = $(''); let $glyph_canvas_el = $(''); let $glyph_info_el = $(''); 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 let glyph, glyph_width, context_X, bounding_box, glyph_unicode, $glyph_container, this_glyph, context, $glyph_info; for ( let i = 0; i < glyphs.length; i++ ) { glyph = glyphs.glyphs[i]; // Glyph width bounding_box = glyph.getBoundingBox(); glyph_width = bounding_box.x2 - bounding_box.x1; context_X = (60 - glyph_width/24); // Add glyph info and append elements glyph_unicode = ( glyph.unicode !== undefined ? '#'+ glyph.unicode : glyph.unicode ); $glyph_container = $glyph_container_el.clone(); $glyph_info = $glyph_info_el.clone(); $glyph_info.text(glyph.index +': '+ glyph.name +', '+ glyph_unicode); $glyph_container.attr('id','glyph_container_'+ glyph.index ).attr('data-id','glyph_container_'+ glyph.index); $glyph_container.append( $glyph_canvas_el.clone().attr('id','glyph_'+ glyph.index ) ).append($glyph_info); $glyphs_container.append( $glyph_container ); // Draw glyph this_glyph = document.getElementById('glyph_'+ glyph.index); $(this_glyph).data('contextX',context_X); context = this_glyph.getContext('2d'); glyph.draw(context, context_X, 84, 72); } } // Get font info function getFontInfo(font) { let font_names = font.names; let $font_info = $('
'+ name +': | '+ value +' | numGlyphs: | '+ num_glyphs +' | '+ text_editing_UI_els +' '+ warnings +' '); // add the UI
$('#text_source').val(source_text); // set the source text value
// no break
case $('#text_source').val().trim().startsWith('#EXTM3U'): // playlists and filelists
content = $('#text_source').val().trim(); // get m3u.txt file for processing
sendMessage('top','iframe_playlist','',content);
break;
}
switch(true) { // assemble text editing body classes
case getSearchParam('enable_text_editing') === 'false': // text editing disabled
$('body').removeClass('split_view preview_text preview_html');
body_classes.push('text_editing_disabled source_text'); // show the source text
$('#text_source').prop('disabled','disabled'); // diable textarea editing
break;
default:
if ( getSearchParam('split_view') === 'true' ) { body_classes.push('split_view'); }
if ( getSearchParam('sync_scroll') === 'true' ) { body_classes.push('sync_scroll'); $('#sync_scroll input').click(); }//$('#sync_scroll input').prop({'checked':true}); }
if ( getSearchParam('default_text_view') === 'preview_source' ) { body_classes.push('source_text'); }
if ( getSearchParam('default_text_view') === 'preview_text' ) { body_classes.push('preview_text'); }
if ( getSearchParam('default_text_view') === 'preview_html' ) { body_classes.push('preview_html'); }
if ( getSearchParam('editor_theme') === 'default' )
{ body_classes.push('editor_theme_'+ getSearchParam('theme')); } else { body_classes.push( 'editor_theme_'+ getSearchParam('editor_theme') ); }
}
$('body').addClass( body_classes.join(' ') ); // add text editor body classes
focusTextEditorPanes();
TextEditing(); // call text editing functions
}
// setup and show top level text editor
$('#text_editor, #text_editor_row').on('click', function(e) { e.preventDefault(); showContent( $(this).attr('id') ); });
// Main Text Editing Function
function TextEditing() {
let $toolbar = $('#toolbar'), $source = $('#text_source'), $preview = $('#text_preview'), $html = $('#html_preview'), $MDhandle = $('#text_editing_handle');
// Toolbar button functions
$toolbar.on('mousedown', function(e) { e.preventDefault(); }); // prevent textarea from losing focus when clicking sidebar
// Resize
$MDhandle.on('mousedown', function(e) { e.stopPropagation(); MDresizeSplit($MDhandle,$source); });
$(window).on('resize', function() { $source.add($preview).add($MDhandle).attr('style',''); }); // reset split to 50/50 on window resize;
// Click labels to toggle checkboxes
$preview.add($toolbar).on('click','label', function(e) { e.stopPropagation(); $(this).siblings('input').click(); });
// Sync scroll
$source.on('scroll', function() { MDsyncScroll(this); });
$preview.on('scroll', function() { MDsyncScroll(this); });
$html.on('scroll', function() { MDsyncScroll(this); });
// Generate Markdown Preview
let source_text = ( $source.length === 0 ? '' : $source.val() );
MDmarkdown( source_text, $preview );
// Live preview update, and set edited classes for unsaved warning
$source.on('input', function() { // only add class or send message once after editing
if ( !$('body').hasClass('edited') ) {
$('body').addClass('edited'); // add edited class
if ( window.top !== window.self ) { sendMessage('top','iframe_edited','',''); } // send edited message to top
}
MDlivePreview($source,$preview);
});
// Checklists
MDsetChecklistClass();
$preview.on('click','.checklist input', function(e) { e.stopPropagation(); MDliveCheckBoxes($(this),$source,$preview); }); // Live checkboxes
$preview.on('click','.table-of-contents a', function(e) { e.preventDefault(); MDtocClick($(this),$preview); }); // Preview TOC click navigation
$preview.on('click','.uplink', function(e) { e.stopPropagation(); MDheaderClick($preview); }); // Click header uplinks
}
///// END MAIN MD FUNCTION
// MARKDOWN Functions
// Focus Text
function focusTextEditorPanes() {
switch(true) {
case $('body').hasClass('split_view'): case $('body').hasClass('source_text'): $('#text_source').focus(); break;
case $('body').hasClass('preview_html'): $('#html_preview').focus(); break;
case $('body').hasClass('preview_text'): $('#text_preview').focus(); break;
}
}
// toggle text editor panes (on tab)
function toggleTextEditorPanes() {
switch(true) {
case document.activeElement.id === 'text_preview' && getFocusableEls('#text_preview').length > 0: // focus focusable elements in text preview
getFocusableEls('#text_preview').first().focus(); break;
case ( /text_preview|html_preview|text_source]/.test(document.activeElement.id) && !$('body').hasClass('split_view') ): // text editor: if not split view, focus sidebar
sendMessage('top','focus_sidebar'); break;
case $('body').hasClass('split_view'):
switch(true) {
case document.activeElement.id === 'text_source': // text editor: if text source has focus with split, focus the other pane
if ( $('body').hasClass('preview_html') ) { $('#html_preview').focus(); } else { $('#text_preview').focus(); } break;
case ( /text_preview|html_preview/.test(document.activeElement.id) ): // text editor: if text preview has focus with split, focus text source
$('#text_source').focus(); break;
}
break;
}
}
// Select Textarea Content
function selectTextareaContent(id) {
let $textarea = document.getElementById(id);
$textarea.focus();
$textarea.select();
$textarea.scrollTop = 0;
}
function saveBtn(id) {
let data, ext, file_name;
switch(true) {
case $('#content_pane').attr('data-content') === 'has_text_editor': file_name = 'untitled'; break;
default: file_name = decodeURIComponentSafe(window.location.pathname.split('/').reverse()[0]);
file_name = file_name.slice(0,file_name.lastIndexOf('.'));
}
switch(true) {
case id === 'save_text': data = $('#text_source').val(); ext = '.md'; break;
case id === 'save_HTML': data = MDprepHTML($('#text_preview').html()); ext = '.html';
}
saveMD( data, file_name + ext );
}
$('#content_text').on('click','#save_btn li',function() { saveBtn($(this).attr('id')); }); // save text editor content
// MD SAVE SOURCE or HTML
function MDprepHTML(data) {
const save_HTML_open = `"/g,' ')
.replace(/^ |