Merge branch 'develop' into master

This commit is contained in:
James Barnsley
2018-11-23 07:59:47 +13:00
committed by GitHub
152 changed files with 13129 additions and 9363 deletions

View File

@ -20,7 +20,8 @@ class AlbumGrid extends React.Component{
e: e,
context: 'album',
uris: [item.uri],
items: [item]
items: [item],
tracklist_uri: item.uri
}
this.props.uiActions.showContextMenu(data)
}

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,7 @@
import React, { PropTypes } from 'react'
import { connect } from 'react-redux'
import { Link } from 'react-router'
import Link from './Link'
import { createStore, bindActionCreators } from 'redux'
import * as helpers from '../helpers'
@ -51,7 +51,7 @@ class DebugInfo extends React.Component{
renderLoadQueue(){
if (!this.props.ui.load_queue){
return <div className="debug-info-item grey-text">Nothing loading</div>;
return <div className="debug-info-item mid_grey-text">Nothing loading</div>;
}
var load_queue = this.props.ui.load_queue
@ -69,7 +69,7 @@ class DebugInfo extends React.Component{
</div>
)
} else {
return <div className="debug-info-item grey-text">Nothing loading</div>;
return <div className="debug-info-item mid_grey-text">Nothing loading</div>;
}
}

View File

@ -1,25 +1,40 @@
import React from 'react'
import React from 'react';
export default class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
this.state = {
hasError: false,
error: null,
info: null
};
}
componentDidCatch(error, info) {
// Display fallback UI
this.setState({ hasError: true });
// You can also log the error to an error reporting service
//logErrorToMyService(error, info);
console.error(error,info);
componentDidCatch(error, info){
this.setState({
hasError: true,
error: error,
info: info
});
console.error(error, info);
}
render() {
if (this.state.hasError){
// You can render any custom fallback UI
return <p className="grey-text">Failed to render</p>;
return (
<div className="error-boundary">
<h4 className="error-boundary__title">
<i className="icon icon--material">error</i>
{this.state.error ? this.state.error.toString() : "Unknown error"}
</h4>
{this.state.info ? <pre className="error-boundary__trace">{this.state.info.componentStack}</pre> : null}
</div>
);
}
return this.props.children;
}

View File

@ -2,8 +2,9 @@
import React, { PropTypes } from 'react'
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
import { Link, hashHistory } from 'react-router'
import { hashHistory } from 'react-router'
import Link from '../Link';
import ArtistSentence from './../ArtistSentence'
import * as helpers from '../../helpers'
import * as coreActions from '../../services/core/actions'
@ -84,13 +85,13 @@ class AddSeedField extends React.Component{
return (
<div className="type">
<h4 className="grey-text">{type}</h4>
<h4 className="mid_grey-text">{type}</h4>
{
items.map(item => {
return (
<div className="result" key={item.uri} onClick={e => this.handleSelect(e,item)}>
{item.name}
{type == 'tracks' ? <span className="grey-text"> <ArtistSentence artists={item.artists} nolinks /></span> : null}
{type == 'tracks' ? <span className="mid_grey-text"> <ArtistSentence artists={item.artists} nolinks /></span> : null}
</div>
)
})

View File

@ -2,7 +2,8 @@
import React, { PropTypes } from 'react'
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
import { Link, hashHistory } from 'react-router'
import { hashHistory } from 'react-router'
import Link from './Link'
import ArtistSentence from './../ArtistSentence'
import * as helpers from '../../helpers'
@ -96,13 +97,13 @@ class AddSeedField extends React.Component{
return (
<div className="type">
<h4 className="grey-text">{type}</h4>
<h4 className="mid_grey-text">{type}</h4>
{
items.map(item => {
return (
<div className="result" key={item.uri} onClick={e => this.handleSelect(e,item)}>
{item.name}
{type == 'tracks' ? <span className="grey-text"> <ArtistSentence artists={item.artists} nolinks /></span> : null}
{type == 'tracks' ? <span className="mid_grey-text"> <ArtistSentence artists={item.artists} nolinks /></span> : null}
</div>
)
})

View File

@ -0,0 +1,56 @@
import React from 'react';
import Icon from '../Icon';
export default class ColourField extends React.Component{
constructor(props){
super(props);
}
render(){
var colours = [
'',
'white',
'mid_grey',
'grey',
'dark_grey',
'black',
'turquoise',
'green',
'blue',
'light_blue',
'yellow',
'orange',
'red'
];
return (
<div className="colour-field">
{
colours.map(colour => {
switch (colour){
case 'yellow':
case 'white':
case 'light_blue':
var text_colour = "black";
break;
default:
var text_colour = "white";
break;
}
return (
<div
key={colour}
className={"colour-field__option "+(colour ? colour+"-background " : "")+(this.props.colour == colour ? "colour-field__option--selected" : "")}
onClick={e => this.props.onChange(colour)}>
{this.props.colour == colour ? <Icon name="check" className={"colour-field__option__icon "+text_colour+"-text"} /> : null}
</div>
);
})
}
</div>
);
}
}

View File

@ -118,7 +118,7 @@ export default class DropdownField extends React.Component{
options.push({
value: 'select-all',
label: 'Select all',
className: 'grey-text'
className: 'mid_grey-text'
});
}

View File

@ -1,7 +1,7 @@
import React, { PropTypes } from 'react'
import { Link } from 'react-router'
import Link from '../Link';
import Icon from '../Icon';
export default class FilterField extends React.Component{
@ -17,11 +17,12 @@ export default class FilterField extends React.Component{
}
componentWillMount(){
window.addEventListener("keyup", this.handleKeyUp, false)
window.addEventListener("keyup", this.handleKeyUp, false);
this.setState({value: this.props.initialValue});
}
componentWillUnmount(){
window.removeEventListener("keyup", this.handleKeyUp, false)
window.removeEventListener("keyup", this.handleKeyUp, false);
}
handleKeyUp(e){
@ -59,7 +60,6 @@ export default class FilterField extends React.Component{
return (
<span className={"filter-field "+(this.state.active ? 'active' : '')} onClick={e => this.activate()}>
<form>
<Icon name="search" type="material" />
<input
type="text"
placeholder="Filter"
@ -68,6 +68,7 @@ export default class FilterField extends React.Component{
onBlur={e => this.handleBlur()}
onChange={e => this.handleChange(e.target.value)}
/>
<Icon name="search" type="material" />
</form>
</span>
)

View File

@ -1,9 +1,10 @@
import React, { PropTypes } from 'react'
import { connect } from 'react-redux'
import { Link } from 'react-router'
import { createStore, bindActionCreators } from 'redux'
import Link from '../Link';
import * as helpers from '../../helpers'
import * as uiActions from '../../services/ui/actions'
import * as spotifyActions from '../../services/spotify/actions'

View File

@ -0,0 +1,971 @@
import React from 'react';
import Icon from '../Icon';
export default class ColourField extends React.Component{
constructor(props){
super(props);
}
icons(){
if (this.props.icons){
return this.props.icons;
} else {
return [
"3d_rotation",
"ac_unit",
"access_alarm",
"access_alarms",
"access_time",
"accessibility",
"accessible",
"account_balance",
"account_balance_wallet",
"account_box",
"account_circle",
"adb",
"add",
"add_a_photo",
"add_alarm",
"add_alert",
"add_box",
"add_circle",
"add_circle_outline",
"add_location",
"add_shopping_cart",
"add_to_photos",
"add_to_queue",
"adjust",
"airline_seat_flat",
"airline_seat_flat_angled",
"airline_seat_individual_suite",
"airline_seat_legroom_extra",
"airline_seat_legroom_normal",
"airline_seat_legroom_reduced",
"airline_seat_recline_extra",
"airline_seat_recline_normal",
"airplanemode_active",
"airplanemode_inactive",
"airplay",
"airport_shuttle",
"alarm",
"alarm_add",
"alarm_off",
"alarm_on",
"album",
"all_inclusive",
"all_out",
"android",
"announcement",
"apps",
"archive",
"arrow_back",
"arrow_downward",
"arrow_drop_down",
"arrow_drop_down_circle",
"arrow_drop_up",
"arrow_forward",
"arrow_upward",
"art_track",
"aspect_ratio",
"assessment",
"assignment",
"assignment_ind",
"assignment_late",
"assignment_return",
"assignment_returned",
"assignment_turned_in",
"assistant",
"assistant_photo",
"attach_file",
"attach_money",
"attachment",
"audiotrack",
"autorenew",
"av_timer",
"backspace",
"backup",
"battery_alert",
"battery_charging_full",
"battery_full",
"battery_std",
"battery_unknown",
"beach_access",
"beenhere",
"block",
"bluetooth",
"bluetooth_audio",
"bluetooth_connected",
"bluetooth_disabled",
"bluetooth_searching",
"blur_circular",
"blur_linear",
"blur_off",
"blur_on",
"book",
"bookmark",
"bookmark_border",
"border_all",
"border_bottom",
"border_clear",
"border_color",
"border_horizontal",
"border_inner",
"border_left",
"border_outer",
"border_right",
"border_style",
"border_top",
"border_vertical",
"branding_watermark",
"brightness_1",
"brightness_2",
"brightness_3",
"brightness_4",
"brightness_5",
"brightness_6",
"brightness_7",
"brightness_auto",
"brightness_high",
"brightness_low",
"brightness_medium",
"broken_image",
"brush",
"bubble_chart",
"bug_report",
"build",
"burst_mode",
"business",
"business_center",
"cached",
"cake",
"call",
"call_end",
"call_made",
"call_merge",
"call_missed",
"call_missed_outgoing",
"call_received",
"call_split",
"call_to_action",
"camera",
"camera_alt",
"camera_enhance",
"camera_front",
"camera_rear",
"camera_roll",
"cancel",
"card_giftcard",
"card_membership",
"card_travel",
"casino",
"cast",
"cast_connected",
"center_focus_strong",
"center_focus_weak",
"change_history",
"chat",
"chat_bubble",
"chat_bubble_outline",
"check",
"check_box",
"check_box_outline_blank",
"check_circle",
"chevron_left",
"chevron_right",
"child_care",
"child_friendly",
"chrome_reader_mode",
"class",
"clear",
"clear_all",
"close",
"closed_caption",
"cloud",
"cloud_circle",
"cloud_done",
"cloud_download",
"cloud_off",
"cloud_queue",
"cloud_upload",
"code",
"collections",
"collections_bookmark",
"color_lens",
"colorize",
"comment",
"compare",
"compare_arrows",
"computer",
"confirmation_number",
"contact_mail",
"contact_phone",
"contacts",
"content_copy",
"content_cut",
"content_paste",
"control_point",
"control_point_duplicate",
"copyright",
"create",
"create_new_folder",
"credit_card",
"crop",
"crop_16_9",
"crop_3_2",
"crop_5_4",
"crop_7_5",
"crop_din",
"crop_free",
"crop_landscape",
"crop_original",
"crop_portrait",
"crop_rotate",
"crop_square",
"dashboard",
"data_usage",
"date_range",
"dehaze",
"delete",
"delete_forever",
"delete_sweep",
"description",
"desktop_mac",
"desktop_windows",
"details",
"developer_board",
"developer_mode",
"device_hub",
"devices",
"devices_other",
"dialer_sip",
"dialpad",
"directions",
"directions_bike",
"directions_boat",
"directions_bus",
"directions_car",
"directions_railway",
"directions_run",
"directions_subway",
"directions_transit",
"directions_walk",
"disc_full",
"dns",
"do_not_disturb",
"do_not_disturb_alt",
"do_not_disturb_off",
"do_not_disturb_on",
"dock",
"domain",
"done",
"done_all",
"donut_large",
"donut_small",
"drafts",
"drag_handle",
"drive_eta",
"dvr",
"edit",
"edit_location",
"eject",
"email",
"enhanced_encryption",
"equalizer",
"error",
"error_outline",
"euro_symbol",
"ev_station",
"event",
"event_available",
"event_busy",
"event_note",
"event_seat",
"exit_to_app",
"expand_less",
"expand_more",
"explicit",
"explore",
"exposure",
"exposure_neg_1",
"exposure_neg_2",
"exposure_plus_1",
"exposure_plus_2",
"exposure_zero",
"extension",
"face",
"fast_forward",
"fast_rewind",
"favorite",
"favorite_border",
"featured_play_list",
"featured_video",
"feedback",
"fiber_dvr",
"fiber_manual_record",
"fiber_new",
"fiber_pin",
"fiber_smart_record",
"file_download",
"file_upload",
"filter",
"filter_1",
"filter_2",
"filter_3",
"filter_4",
"filter_5",
"filter_6",
"filter_7",
"filter_8",
"filter_9",
"filter_9_plus",
"filter_b_and_w",
"filter_center_focus",
"filter_drama",
"filter_frames",
"filter_hdr",
"filter_list",
"filter_none",
"filter_tilt_shift",
"filter_vintage",
"find_in_page",
"find_replace",
"fingerprint",
"first_page",
"fitness_center",
"flag",
"flare",
"flash_auto",
"flash_off",
"flash_on",
"flight",
"flight_land",
"flight_takeoff",
"flip",
"flip_to_back",
"flip_to_front",
"folder",
"folder_open",
"folder_shared",
"folder_special",
"font_download",
"format_align_center",
"format_align_justify",
"format_align_left",
"format_align_right",
"format_bold",
"format_clear",
"format_color_fill",
"format_color_reset",
"format_color_text",
"format_indent_decrease",
"format_indent_increase",
"format_italic",
"format_line_spacing",
"format_list_bulleted",
"format_list_numbered",
"format_paint",
"format_quote",
"format_shapes",
"format_size",
"format_strikethrough",
"format_textdirection_l_to_r",
"format_textdirection_r_to_l",
"format_underlined",
"forum",
"forward",
"forward_10",
"forward_30",
"forward_5",
"free_breakfast",
"fullscreen",
"fullscreen_exit",
"functions",
"g_translate",
"gamepad",
"games",
"gavel",
"gesture",
"get_app",
"gif",
"golf_course",
"gps_fixed",
"gps_not_fixed",
"gps_off",
"grade",
"gradient",
"grain",
"graphic_eq",
"grid_off",
"grid_on",
"group",
"group_add",
"group_work",
"hd",
"hdr_off",
"hdr_on",
"hdr_strong",
"hdr_weak",
"headset",
"headset_mic",
"healing",
"hearing",
"help",
"help_outline",
"high_quality",
"highlight",
"highlight_off",
"history",
"home",
"hot_tub",
"hotel",
"hourglass_empty",
"hourglass_full",
"http",
"https",
"image",
"image_aspect_ratio",
"import_contacts",
"import_export",
"important_devices",
"inbox",
"indeterminate_check_box",
"info",
"info_outline",
"input",
"insert_chart",
"insert_comment",
"insert_drive_file",
"insert_emoticon",
"insert_invitation",
"insert_link",
"insert_photo",
"invert_colors",
"invert_colors_off",
"iso",
"keyboard",
"keyboard_arrow_down",
"keyboard_arrow_left",
"keyboard_arrow_right",
"keyboard_arrow_up",
"keyboard_backspace",
"keyboard_capslock",
"keyboard_hide",
"keyboard_return",
"keyboard_tab",
"keyboard_voice",
"kitchen",
"label",
"label_outline",
"landscape",
"language",
"laptop",
"laptop_chromebook",
"laptop_mac",
"laptop_windows",
"last_page",
"launch",
"layers",
"layers_clear",
"leak_add",
"leak_remove",
"lens",
"library_add",
"library_books",
"library_music",
"lightbulb_outline",
"line_style",
"line_weight",
"linear_scale",
"link",
"linked_camera",
"list",
"live_help",
"live_tv",
"local_activity",
"local_airport",
"local_atm",
"local_bar",
"local_cafe",
"local_car_wash",
"local_convenience_store",
"local_dining",
"local_drink",
"local_florist",
"local_gas_station",
"local_grocery_store",
"local_hospital",
"local_hotel",
"local_laundry_service",
"local_library",
"local_mall",
"local_movies",
"local_offer",
"local_parking",
"local_pharmacy",
"local_phone",
"local_pizza",
"local_play",
"local_post_office",
"local_printshop",
"local_see",
"local_shipping",
"local_taxi",
"location_city",
"location_disabled",
"location_off",
"location_on",
"location_searching",
"lock",
"lock_open",
"lock_outline",
"looks",
"looks_3",
"looks_4",
"looks_5",
"looks_6",
"looks_one",
"looks_two",
"loop",
"loupe",
"low_priority",
"loyalty",
"mail",
"mail_outline",
"map",
"markunread",
"markunread_mailbox",
"memory",
"menu",
"merge_type",
"message",
"mic",
"mic_none",
"mic_off",
"mms",
"mode_comment",
"mode_edit",
"monetization_on",
"money_off",
"monochrome_photos",
"mood",
"mood_bad",
"more",
"more_horiz",
"more_vert",
"motorcycle",
"mouse",
"move_to_inbox",
"movie",
"movie_creation",
"movie_filter",
"multiline_chart",
"music_note",
"music_video",
"my_location",
"nature",
"nature_people",
"navigate_before",
"navigate_next",
"navigation",
"near_me",
"network_cell",
"network_check",
"network_locked",
"network_wifi",
"new_releases",
"next_week",
"nfc",
"no_encryption",
"no_sim",
"not_interested",
"note",
"note_add",
"notifications",
"notifications_active",
"notifications_none",
"notifications_off",
"notifications_paused",
"offline_pin",
"ondemand_video",
"opacity",
"open_in_browser",
"open_in_new",
"open_with",
"pages",
"pageview",
"palette",
"pan_tool",
"panorama",
"panorama_fish_eye",
"panorama_horizontal",
"panorama_vertical",
"panorama_wide_angle",
"party_mode",
"pause",
"pause_circle_filled",
"pause_circle_outline",
"payment",
"people",
"people_outline",
"perm_camera_mic",
"perm_contact_calendar",
"perm_data_setting",
"perm_device_information",
"perm_identity",
"perm_media",
"perm_phone_msg",
"perm_scan_wifi",
"person",
"person_add",
"person_outline",
"person_pin",
"person_pin_circle",
"personal_video",
"pets",
"phone",
"phone_android",
"phone_bluetooth_speaker",
"phone_forwarded",
"phone_in_talk",
"phone_iphone",
"phone_locked",
"phone_missed",
"phone_paused",
"phonelink",
"phonelink_erase",
"phonelink_lock",
"phonelink_off",
"phonelink_ring",
"phonelink_setup",
"photo",
"photo_album",
"photo_camera",
"photo_filter",
"photo_library",
"photo_size_select_actual",
"photo_size_select_large",
"photo_size_select_small",
"picture_as_pdf",
"picture_in_picture",
"picture_in_picture_alt",
"pie_chart",
"pie_chart_outlined",
"pin_drop",
"place",
"play_arrow",
"play_circle_filled",
"play_circle_outline",
"play_for_work",
"playlist_add",
"playlist_add_check",
"playlist_play",
"plus_one",
"poll",
"polymer",
"pool",
"portable_wifi_off",
"portrait",
"power",
"power_input",
"power_settings_new",
"pregnant_woman",
"present_to_all",
"print",
"priority_high",
"public",
"publish",
"query_builder",
"question_answer",
"queue",
"queue_music",
"queue_play_next",
"radio",
"radio_button_checked",
"radio_button_unchecked",
"rate_review",
"receipt",
"recent_actors",
"record_voice_over",
"redeem",
"redo",
"refresh",
"remove",
"remove_circle",
"remove_circle_outline",
"remove_from_queue",
"remove_red_eye",
"remove_shopping_cart",
"reorder",
"repeat",
"repeat_one",
"replay",
"replay_10",
"replay_30",
"replay_5",
"reply",
"reply_all",
"report",
"report_problem",
"restaurant",
"restaurant_menu",
"restore",
"restore_page",
"ring_volume",
"room",
"room_service",
"rotate_90_degrees_ccw",
"rotate_left",
"rotate_right",
"rounded_corner",
"router",
"rowing",
"rss_feed",
"rv_hookup",
"satellite",
"save",
"scanner",
"schedule",
"school",
"screen_lock_landscape",
"screen_lock_portrait",
"screen_lock_rotation",
"screen_rotation",
"screen_share",
"sd_card",
"sd_storage",
"search",
"security",
"select_all",
"send",
"sentiment_dissatisfied",
"sentiment_neutral",
"sentiment_satisfied",
"sentiment_very_dissatisfied",
"sentiment_very_satisfied",
"settings",
"settings_applications",
"settings_backup_restore",
"settings_bluetooth",
"settings_brightness",
"settings_cell",
"settings_ethernet",
"settings_input_antenna",
"settings_input_component",
"settings_input_composite",
"settings_input_hdmi",
"settings_input_svideo",
"settings_overscan",
"settings_phone",
"settings_power",
"settings_remote",
"settings_system_daydream",
"settings_voice",
"share",
"shop",
"shop_two",
"shopping_basket",
"shopping_cart",
"short_text",
"show_chart",
"shuffle",
"signal_cellular_4_bar",
"signal_cellular_connected_no_internet_4_bar",
"signal_cellular_no_sim",
"signal_cellular_null",
"signal_cellular_off",
"signal_wifi_4_bar",
"signal_wifi_4_bar_lock",
"signal_wifi_off",
"sim_card",
"sim_card_alert",
"skip_next",
"skip_previous",
"slideshow",
"slow_motion_video",
"smartphone",
"smoke_free",
"smoking_rooms",
"sms",
"sms_failed",
"snooze",
"sort",
"sort_by_alpha",
"spa",
"space_bar",
"speaker",
"speaker_group",
"speaker_notes",
"speaker_notes_off",
"speaker_phone",
"spellcheck",
"star",
"star_border",
"star_half",
"stars",
"stay_current_landscape",
"stay_current_portrait",
"stay_primary_landscape",
"stay_primary_portrait",
"stop",
"stop_screen_share",
"storage",
"store",
"store_mall_directory",
"straighten",
"streetview",
"strikethrough_s",
"style",
"subdirectory_arrow_left",
"subdirectory_arrow_right",
"subject",
"subscriptions",
"subtitles",
"subway",
"supervisor_account",
"surround_sound",
"swap_calls",
"swap_horiz",
"swap_vert",
"swap_vertical_circle",
"switch_camera",
"switch_video",
"sync",
"sync_disabled",
"sync_problem",
"system_update",
"system_update_alt",
"tab",
"tab_unselected",
"tablet",
"tablet_android",
"tablet_mac",
"tag_faces",
"tap_and_play",
"terrain",
"text_fields",
"text_format",
"textsms",
"texture",
"theaters",
"thumb_down",
"thumb_up",
"thumbs_up_down",
"time_to_leave",
"timelapse",
"timeline",
"timer",
"timer_10",
"timer_3",
"timer_off",
"title",
"toc",
"today",
"toll",
"tonality",
"touch_app",
"toys",
"track_changes",
"traffic",
"train",
"tram",
"transfer_within_a_station",
"transform",
"translate",
"trending_down",
"trending_flat",
"trending_up",
"tune",
"turned_in",
"turned_in_not",
"tv",
"unarchive",
"undo",
"unfold_less",
"unfold_more",
"update",
"usb",
"verified_user",
"vertical_align_bottom",
"vertical_align_center",
"vertical_align_top",
"vibration",
"video_call",
"video_label",
"video_library",
"videocam",
"videocam_off",
"videogame_asset",
"view_agenda",
"view_array",
"view_carousel",
"view_column",
"view_comfy",
"view_compact",
"view_day",
"view_headline",
"view_list",
"view_module",
"view_quilt",
"view_stream",
"view_week",
"vignette",
"visibility",
"visibility_off",
"voice_chat",
"voicemail",
"volume_down",
"volume_mute",
"volume_off",
"volume_up",
"vpn_key",
"vpn_lock",
"wallpaper",
"warning",
"watch",
"watch_later",
"wb_auto",
"wb_cloudy",
"wb_incandescent",
"wb_iridescent",
"wb_sunny",
"wc",
"web",
"web_asset",
"weekend",
"whatshot",
"widgets",
"wifi",
"wifi_lock",
"wifi_tethering",
"work",
"wrap_text",
"youtube_searched_for",
"zoom_in",
"zoom_out",
"zoom_out_map"
];
}
}
render(){
return (
<div className="icon-field">
{
this.icons().map(icon => {
return (
<div
key={icon}
className={"icon-field__option "+(this.props.icon == icon ? "icon-field__option--selected" : "")}
onClick={e => this.props.onChange(icon)}>
<Icon name={icon} className="icon-field__option__icon" />
</div>
);
})
}
</div>
);
}
}

View File

@ -1,9 +1,9 @@
import React, { PropTypes } from 'react'
import { connect } from 'react-redux'
import { Link } from 'react-router'
import { createStore, bindActionCreators } from 'redux'
import Link from '../Link';
import * as helpers from '../../helpers'
import * as uiActions from '../../services/ui/actions'
import * as lastfmActions from '../../services/lastfm/actions'

View File

@ -57,12 +57,13 @@ class OutputControl extends React.Component{
}
renderOutputs(){
var has_outputs = false;
var clients = [];
for (var key in this.props.snapcast_clients){
if (this.props.snapcast_clients.hasOwnProperty(key)){
var client = this.props.snapcast_clients[key];
if (client.connected){
if (client.connected || this.props.show_disconnected_clients){
clients.push(client);
}
}
@ -70,27 +71,17 @@ class OutputControl extends React.Component{
var snapcast_clients = null;
if (clients.length > 0){
has_outputs = true;
snapcast_clients = (
<div>
{
clients.map(client => {
var commands = {};
if (this.props.snapcast_client_commands[client.id]){
commands = this.props.snapcast_client_commands[client.id];
}
return (
<div className={"output-control__item outputs__item--snapcast"+(commands && commands.power ? " output-control__item--has-power-button": '')} key={client.id}>
<div className="output-control__item outputs__item--snapcast" key={client.id}>
<div className="output-control__item__name">
{client.name}
</div>
<div className="output-control__item__controls">
{commands && commands.power ? <SnapcastPowerButton
className="output-control__item__power"
client={client}
onClick={e => this.props.snapcastActions.sendClientCommand(client.id, commands.power)}
/> : null}
<MuteControl
className="output-control__item__mute"
noTooltip={true}
@ -114,6 +105,7 @@ class OutputControl extends React.Component{
var local_streaming = null;
if (this.props.http_streaming_enabled){
has_outputs = true;
local_streaming = (
<div className="output-control__item outputs__item--icecast">
<div className="output-control__item__actions">
@ -137,7 +129,39 @@ class OutputControl extends React.Component{
);
}
if (!local_streaming && !snapcast_clients){
var commands = null;
if (this.props.pusher_commands){
var commands_items = [];
for (var key in this.props.pusher_commands){
if (this.props.pusher_commands.hasOwnProperty(key)){
commands_items.push(this.props.pusher_commands[key]);
}
}
if (commands_items.length > 0){
has_outputs = true;
commands = (
<div className="output-control__item output-control__item--commands commands">
{
commands_items.map(command => {
return (
<div
key={command.id}
className="commands__item commands__item--interactive"
onClick={e => this.props.pusherActions.sendCommand(command.id)}>
<Icon className="commands__item__icon" name={command.icon} />
<span className={command.colour+'-background commands__item__background'}></span>
</div>
);
})
}
</div>
);
}
}
if (!has_outputs){
return (
<div className="output-control__items output-control__items--no-results">
<p className="no-results">No outputs</p>
@ -146,6 +170,7 @@ class OutputControl extends React.Component{
} else {
return (
<div className="output-control__items">
{commands}
{local_streaming}
{snapcast_clients}
</div>
@ -188,8 +213,9 @@ const mapStateToProps = (state, ownProps) => {
http_streaming_mute: state.core.http_streaming_mute,
pusher_connected: state.pusher.connected,
snapcast_enabled: (state.pusher.config ? state.pusher.config.snapcast_enabled : null),
show_disconnected_clients: (state.ui.snapcast_show_disconnected_clients !== undefined ? state.ui.snapcast_show_disconnected_clients : false),
snapcast_clients: state.snapcast.clients,
snapcast_client_commands: (state.snapcast.client_commands ? state.snapcast.client_commands : {})
pusher_commands: (state.pusher.commands ? state.pusher.commands : {})
}
}

View File

@ -25,6 +25,11 @@ export default class GridItem extends React.Component{
handleClick(e){
if (this.props.onClick && e.target.tagName.toLowerCase() !== 'a'){
// Scroll to the top
helpers.scrollTo();
// And then trigger the click event
this.props.onClick(e);
}
}
@ -57,8 +62,8 @@ export default class GridItem extends React.Component{
case 'artist':
return (
<span>
{item.followers ? item.followers.toLocaleString()+' followers' : null}
{item.albums_uris ? item.albums_uris.length+' albums' : null}
{item.followers !== undefined ? item.followers.toLocaleString()+' followers' : null}
{item.albums_uris !== undefined ? item.albums_uris.length+' albums' : null}
</span>
)
break
@ -66,7 +71,7 @@ export default class GridItem extends React.Component{
case 'album':
return (
<span>
{item.artists ? <ArtistSentence artists={item.artists} /> : null}
{item.artists !== undefined ? <ArtistSentence artists={item.artists} /> : null}
</span>
)
break
@ -74,8 +79,8 @@ export default class GridItem extends React.Component{
default:
return (
<span>
{ item.artists ? <ArtistSentence artists={ item.artists } /> : null }
{ item.followers ? item.followers.toLocaleString()+' followers' : null }
{ item.artists !== undefined ? <ArtistSentence artists={ item.artists } /> : null }
{ item.followers !== undefined ? item.followers.toLocaleString()+' followers' : null }
</span>
)
}
@ -93,16 +98,20 @@ export default class GridItem extends React.Component{
}
var images = null
if (this.props.item.images){
images = this.props.item.images
if (Array.isArray(this.props.item.images)){
images = this.props.item.images[0];
} else {
images = this.props.item.images;
}
} else if (this.props.item.icons){
images = this.props.item.icons
images = this.props.item.icons;
}
return (
<div className={"grid-item "+this.props.type+"-grid-item"} onClick={e => this.handleClick(e)} onContextMenu={e => this.handleContextMenu(e)}>
<Thumbnail size="medium" images={images} />
<div className="name">
{item.name ? item.name : <span className="dark-grey-text">{item.uri}</span>}
{item.name ? item.name : <span className="opaque-text">{item.uri}</span>}
</div>
<div className="secondary">
{this.props.show_source_icon ? <Icon name={helpers.sourceIcon(item.uri)} type="fontawesome" className="source" /> : null}

View File

@ -2,7 +2,8 @@
import React, { PropTypes } from 'react'
import { connect } from 'react-redux'
import { createStore, bindActionCreators } from 'redux'
import { Link, hashHistory } from 'react-router'
import { hashHistory } from 'react-router'
import Link from './Link'
import ArtistSentence from './ArtistSentence'
import Thumbnail from './Thumbnail'

View File

@ -1,8 +1,8 @@
import React, { PropTypes } from 'react'
import Icon from './Icon'
import ContextMenuTrigger from './ContextMenuTrigger'
import Icon from './Icon';
import ContextMenuTrigger from './ContextMenuTrigger';
export default class Header extends React.Component{

View File

@ -25,9 +25,19 @@ export default class Icon extends React.Component{
var src = require('../../assets/icons/'+this.props.name+'.svg');
return <img className={className} src={src} onClick={e => this.handleClick(e)} />;
case 'gif':
var src = require('../../assets/icons/'+this.props.name+'.gif');
return <img className={className} src={src} onClick={e => this.handleClick(e)} />;
case 'fontawesome':
return <FontAwesome className={className} type="fontawesome" name={this.props.name} onClick={e => this.handleClick(e)} />;
case 'css':
switch (this.props.name){
case 'playing':
return <i className={className + " icon--playing"}><span></span><span></span><span></span></i>
}
default:
return <i className={className} onClick={e => this.handleClick(e)}>{this.props.name}</i>;
}

View File

@ -10,16 +10,17 @@ export default class LazyLoadListener extends React.Component{
listening: (this.props.loadKey ? true : false),
loadKey: this.props.loadKey
}
this.handleScroll = helpers.throttle(this.handleScroll.bind(this), 50);
}
componentWillMount(){
window.addEventListener("scroll", this.handleScroll, false);
componentDidMount(){
this.element = document.getElementById('main');
this.element.addEventListener("scroll", this.handleScroll, false);
}
componentWillUnmount(){
window.removeEventListener("scroll", this.handleScroll, false);
this.element.removeEventListener("scroll", this.handleScroll, false);
}
componentWillReceiveProps(nextProps){
@ -35,7 +36,7 @@ export default class LazyLoadListener extends React.Component{
if (this.state.listening){
// At, or nearly at the bottom of the page
if ((window.innerHeight + window.scrollY) >= (document.body.offsetHeight - 80)){
if (this.element.scrollTop > (this.element.scrollHeight - this.element.offsetHeight - 100)){
// Immediately stop listening to avoid duplicating pagination requests
this.setState(

View File

@ -1,8 +1,14 @@
import React from 'react'
import { Link } from 'react-router'
import React from 'react';
import {Link} from 'react-router';
export default class Link extends React.Component{
import * as helpers from '../helpers';
/**
* Extends react-router's Link but provides the ability to hook in to the navigation event
* which lets us scroll to the top of our <main> for a more traditional navigation experience
**/
export default class extends React.Component{
constructor(props){
super(props);
@ -10,16 +16,17 @@ export default class Link extends React.Component{
handleClick(e){
if (!this.props.retainScroll){
window.scrollTo(0, 0);
helpers.scrollTo(this.props.scrollTo, (this.props.scrollTo));
}
}
render(){
render(){
return (
<Link
onClick={e => this.handleClick(e)}
className={this.props.className ? this.props.className : null}
to={to}>
className={this.props.className ? this.props.className : null}
activeClassName={this.props.activeClassName}
to={this.props.to}>
{this.props.children}
</Link>
);

View File

@ -9,6 +9,7 @@ import Dater from './Dater'
import URILink from './URILink'
import ContextMenuTrigger from './ContextMenuTrigger'
import Icon from './Icon'
import Thumbnail from './Thumbnail'
import Popularity from './Popularity'
import * as helpers from '../helpers'
@ -26,6 +27,7 @@ class List extends React.Component{
if (e.target.tagName.toLowerCase() !== 'a'){
e.preventDefault();
hashHistory.push((this.props.link_prefix ? this.props.link_prefix : '') + encodeURIComponent(uri));
helpers.scrollTo();
}
}
@ -35,6 +37,7 @@ class List extends React.Component{
if (e.target.tagName.toLowerCase() !== 'a'){
e.preventDefault();
hashHistory.push((this.props.link_prefix ? this.props.link_prefix : '') + encodeURIComponent(uri));
helpers.scrollTo();
}
}
@ -45,38 +48,23 @@ class List extends React.Component{
}
}
renderHeader(){
if (!this.props.columns || this.props.noheader) return null
return (
<div className="list__item header cf">
<div className="liner">
{
this.props.columns.map((col, col_index) => {
var className = 'col '+col.name.replace('.','_')
return <div className={className} key={col_index}>{ col.label ? col.label : col.name }</div>
})
}
</div>
</div>
)
}
renderValue(row, key_string){
var key = key_string.split('.')
var value = row
var key = key_string.split('.');
var value = row;
for (var i = 0; i < key.length; i++){
if (value[key[i]] === undefined){
return <span>-</span>
return null
} else if (typeof(value[key[i]]) === 'string' && value[key[i]].replace(' ','') == ''){
return <span>-</span>
return null
} else {
value = value[key[i]]
}
}
if (key_string === 'added_at') return <span><Dater type="ago" data={value} /> ago</span>
if (key_string === 'tracks_total' || key_string === 'tracks_uris.length') return <span>{value} tracks</span>
if (key_string === 'followers') return <span>{value.toLocaleString()} followers</span>
if (key_string === 'added_at') return <span>Added <Dater type="ago" data={value} /> ago</span>
if (key_string === 'owner') return <URILink type="user" uri={value.uri}>{value.id}</URILink>
if (key_string === 'popularity') return <Popularity full popularity={value} />
if (key[0] === 'artists') return <ArtistSentence artists={value} />
@ -95,12 +83,25 @@ class List extends React.Component{
return (
<div className={className}>
{ this.renderHeader() }
{
this.props.rows.map((row, row_index) => {
var class_name = 'list__item'
if (row.type ) class_name += ' '+row.type
if (row.type){
class_name += ' list__item--'+row.type;
}
if (this.props.middle_column){
class_name += " list__item--has-middle-column";
}
if (this.props.thumbnail){
class_name += " list__item--has-thumbnail";
}
if (this.props.details){
class_name += " list__item--has-details";
}
return (
<div
@ -108,17 +109,60 @@ class List extends React.Component{
key={row_index}
onClick={e => this.handleClick(e, row.uri)}
onContextMenu={e => this.handleContextMenu(e,row)}>
{
this.props.columns.map((col, col_index) => {
var className = 'col '+col.name.replace('.','_')
return (
<div className={className} key={col_index}>
{ this.renderValue(row, col.name) }
</div>
)
})
}
{this.props.nocontext ? null : <ContextMenuTrigger onTrigger={e => this.handleContextMenu(e, row)} />}
<div className="list__item__column list__item__column--right">
{
(this.props.right_column ? this.props.right_column.map((item, index) => {
return (
<span className={'list__item__column__item list__item__column__item--'+item.replace('.','_')} key={index}>
{this.renderValue(row, item)}
</span>
)
}) : null)
}
{this.props.nocontext ? null : <ContextMenuTrigger className="list__item__column__item list__item__column__item--context-menu-trigger subtle" onTrigger={e => this.handleContextMenu(e, row)} />}
</div>
<div className="list__item__column list__item__column--name">
{this.props.thumbnail ? <Thumbnail className="list__item__column__item list__item__column__item--thumbnail" images={(row.images ? row.images : null)} size="small" /> : null}
<div className="list__item__column__item list__item__column__item--name">
{this.renderValue(row, 'name')}
</div>
{this.props.details ?<ul className="list__item__column__item list__item__column__item--details details">
{
this.props.details.map((item, index) => {
var value = this.renderValue(row, item);
if (!value){
return null;
}
return (
<li className={'details__item details__item--'+item.replace('.','_')} key={index}>
{value}
</li>
)
})
}
</ul> : null}
</div>
{this.props.middle_column ? <div className="list__item__column list__item__column--middle">
{
(this.props.middle_column ? this.props.middle_column.map((item, index) => {
return (
<span className={'list__item__column__item list__item__column__item--'+item.replace('.','_')} key={index}>
{this.renderValue(row, item)}
</span>
)
}) : null)
}
</div> : null}
</div>
)
})

View File

@ -1,6 +1,7 @@
import React, { PropTypes } from 'react';
import { Link, hashHistory } from 'react-router';
import { hashHistory } from 'react-router'
import Link from './Link';
import Icon from './Icon';
export default class Notifications extends React.Component{
@ -29,10 +30,6 @@ export default class Notifications extends React.Component{
this.props.geniusActions.importAuthorization(configuration.genius.authorization, configuration.genius.me);
}
if (configuration.snapcast_client_commands){
this.props.snapcastActions.clientCommandsUpdated(configuration.snapcast_client_commands);
}
this.props.uiActions.removeNotification(notification_key, true);
this.props.uiActions.createNotification({type: 'info', content: 'Import successful'});
}
@ -72,7 +69,6 @@ export default class Notifications extends React.Component{
{notification.configuration.spotify ? <li>Spotify</li> : null}
{notification.configuration.lastfm ? <li>LastFM</li> : null}
{notification.configuration.genius ? <li>Genius</li> : null}
{notification.configuration.snapcast_client_commands ? <li>Snapcast</li> : null}
</ul>
<p>Do you want to import this?</p>
</div>
@ -86,7 +82,7 @@ export default class Notifications extends React.Component{
<div className={"notification notification--"+notification.type+(notification.closing ? ' closing' : '')} key={notification.key} data-key={notification.key} data-duration={notification.duration}>
<Icon name="close" className="close-button" onClick={ e => this.props.uiActions.removeNotification(notification.key, true) } />
{notification.title ? <h4>{notification.title}</h4> : null}
{notification.content ? <p className="content" dangerouslySetInnerHTML={{__html: notification.content}}></p> :null}
{notification.content ? <p className="content" dangerouslySetInnerHTML={{__html: notification.content}}></p> : null}
{notification.description ? <p className="description" dangerouslySetInnerHTML={{__html: notification.description}}></p> : null }
</div>
)

View File

@ -6,220 +6,68 @@ export default class Parallax extends React.Component{
constructor(props){
super(props);
this._loading = false
this._loaded = false
this.state = {
scrollTop: 0,
windowWidth: 0,
windowHeight: 0,
canvas: {
width: 0,
height: 0
},
image: {},
loading: false,
url: false
}
if (!this.props.disabled){
// we need to manually bind this as eventListener doesn't work with anonymous functions
this.handleResize = this.handleResize.bind(this);
this.handleScroll = this.handleScroll.bind(this);
loaded: false,
url: null
}
}
componentDidMount(){
if (!this.props.disabled){
window.addEventListener("resize", this.handleResize);
window.addEventListener("scroll", this.handleScroll);
componentWillMount(){
if (this.props.image){
this.loadImage(this.props.image);
}
this._mounted = true;
if (this.props.image){
this._loading = true;
this.setState({url: this.props.image, image: false, loading: true});
this.loadImage(this.props.image)
.then(
response => {
if (this._mounted){
this._loading = false;
this._loaded = true;
this.setState({url: this.props.image, image: response, loading: false});
this.updateCanvas(response);
}
}
)
} else {
this._loaded = true;
this.state.image = false;
this.updateCanvas();
}
}
componentWillUnmount(){
this._mounted = false;
if (!this.props.disabled){
window.removeEventListener("resize", this.handleResize);
window.removeEventListener("scroll", this.handleScroll);
}
}
componentWillReceiveProps(nextProps){
if ((!this.state.url || nextProps.image != this.state.url ) && !this._loading && nextProps.image){
this._loading = true;
this.setState({ url: nextProps.image, image: false, loading: true });
this.loadImage(nextProps.image)
.then(
response => {
if (this._mounted){
this._loading = false;
this._loaded = true;
this.setState({ url: nextProps.image, image: response, loading: false });
this.updateCanvas(response);
}
}
)
if (nextProps.image != this.state.url){
this.loadImage(nextProps.image);
}
}
handleResize(e){
this.updateCanvas(this.state.image);
}
handleScroll(e){
// this DOES work, but is in no way high-performing and only on Firefox
if (this._loaded){
this.setState(
{scrollTop: window.scrollY},
this.updateCanvas(this.state.image)
)
}
}
loadImage(url){
return new Promise((resolve, reject) => {
// Reset our image to nothing
// This is needed when navigating to new view, but not unmounting the current components
// ie one artist to another
this.setState({
loaded: false,
url: null
});
if (url && url !== ""){
var self = this;
var imageObject = new Image();
imageObject.src = url;
// This seems to prevent Dirble images from loading.
// Other domains don't seem to mind any more, so perhaps we can remove for good.
// TODO: Re-test the nature of crossorigin images
// imageObject.crossOrigin = 'anonymous';
imageObject.onload = function(){
var image = {
width: imageObject.naturalWidth,
height: imageObject.naturalHeight,
original_width: imageObject.naturalWidth,
original_height: imageObject.naturalHeight,
object: imageObject
}
resolve(image);
self.setState({
loaded: true,
url: url
});
}
})
}
updateCanvas(image = false){
var canvasWidth = $('.parallax').outerWidth();
var canvasHeight = $('.parallax').outerHeight();
if (this.state.canvas.width != canvasWidth || this.state.canvas.height != canvasHeight){
this.setState({
canvas: {
width: canvasWidth,
height: canvasHeight
}
})
}
this.renderCanvas(image);
}
renderCanvas(image = false){
let self = this;
var canvasDOM = document.getElementById('parallax-canvas')
var context = canvasDOM.getContext('2d')
// Fill the background with mid-grey
context.beginPath();
context.rect(0, 0, this.state.canvas.width, this.state.canvas.height);
switch (this.props.theme){
case 'light':
context.fillStyle = "#EDEDED";
break;
case 'dark':
default:
context.fillStyle = "#121212";
}
context.fill();
if (image && !self.state.loading){
// zoom image to fill canvas, widthwise
if (image.width < this.state.canvas.width || image.width > this.state.canvas.width){
var scale = this.state.canvas.width / image.width;
image.width = image.width * scale;
image.height = image.height * scale;
}
// now check for fill heightwise, and zoom in if necessary
if (image.height < this.state.canvas.height){
var scale = this.state.canvas.height / image.height;
image.width = image.width * scale;
image.height = image.height * scale;
}
// figure out where we want the image to be, based on scroll position
var percent = Math.round(self.state.scrollTop / this.state.canvas.height * 100 );
var position = Math.round((this.state.canvas.height / 2) * (percent/100) ) - 100;
image.x = (this.state.canvas.width / 2 ) - (image.width / 2 );
image.y = ((this.state.canvas.height / 2 ) - (image.height / 2 ) ) + ((percent / 100 ) * 100);
// Actually draw the image on the canvas
context.drawImage(image.object, image.x, image.y, image.width, image.height);
// now update our component
self.setState({ image: image });
}
// Construct a gradient overlay
context.rect(0, 0, this.state.canvas.width, this.state.canvas.height);
let gradient = context.createLinearGradient(0, 0, 0, this.state.canvas.height);
switch (this.props.theme){
case 'light':
gradient.addColorStop(0, 'rgba(255,255,255,0.4)');
gradient.addColorStop(0.9, 'rgba(255,255,255,1)');
break;
case 'dark':
default:
gradient.addColorStop(0, 'rgba(24,24,24,0)');
gradient.addColorStop(0.9, 'rgba(24,24,24,1)');
}
context.fillStyle = gradient;
// And now drop it into place
context.fill();
}
render(){
return (
<div className={this.props.blur && !this.props.disabled ? "parallax blur" : "parallax"}>
var class_name = "parallax preserve-3d";
if (this.props.blur){
class_name += " parallax--blur";
}
if (this.state.loaded){
class_name += " parallax--loaded";
}
<canvas
id="parallax-canvas"
className={!this.state.loading ? 'loaded' : null}
width={this.state.canvas.width}
height={this.state.canvas.height}>
</canvas>
var style = {};
if (this.state.loaded && this.state.url){
style = {backgroundImage: 'url('+this.state.url+')'};
}
return (
<div className={class_name}>
<div className="parallax__inner preserve-3d">
<div className="parallax__image" style={style}></div>
<div className="parallax__overlay"></div>
</div>
</div>
);
}

View File

@ -1,7 +1,7 @@
import React, { PropTypes } from 'react'
import { connect } from 'react-redux'
import { Link } from 'react-router'
import Link from './Link'
import { bindActionCreators } from 'redux'
import ProgressSlider from './Fields/ProgressSlider'
@ -101,7 +101,7 @@ class PlaybackControls extends React.Component{
renderConsumeButton(){
var button = <a className="control tooltip" onClick={() => this.props.mopidyActions.setConsume(true)}><Icon name="restaurant" type="material" /><span className="tooltip__content">Consume</span></a>
if (this.props.consume){
button = <a className="control active tooltip" onClick={() => this.props.mopidyActions.setConsume(false)}><Icon name="restaurant" type="material" /><span className="tooltip__content">Consume</span></a>
button = <a className="control control--active tooltip" onClick={() => this.props.mopidyActions.setConsume(false)}><Icon name="restaurant" type="material" /><span className="tooltip__content">Consume</span></a>
}
return button;
}
@ -109,7 +109,7 @@ class PlaybackControls extends React.Component{
renderRandomButton(){
var button = <a className="control tooltip" onClick={() => this.props.mopidyActions.setRandom(true)}><Icon name="shuffle" type="material" /><span className="tooltip__content">Shuffle</span></a>
if (this.props.random){
button = <a className="control active tooltip" onClick={() => this.props.mopidyActions.setRandom(false)}><Icon name="shuffle" type="material" /><span className="tooltip__content">Shuffle</span></a>
button = <a className="control control--active tooltip" onClick={() => this.props.mopidyActions.setRandom(false)}><Icon name="shuffle" type="material" /><span className="tooltip__content">Shuffle</span></a>
}
return button;
}
@ -117,7 +117,7 @@ class PlaybackControls extends React.Component{
renderRepeatButton(){
var button = <a className="control tooltip" onClick={() => this.props.mopidyActions.setRepeat(true)}><Icon name="repeat" /><span className="tooltip__content">Repeat</span></a>
if (this.props.repeat){
button = <a className="control active tooltip" onClick={() => this.props.mopidyActions.setRepeat(false)}><Icon name="repeat" /><span className="tooltip__content">Repeat</span></a>
button = <a className="control control--active tooltip" onClick={() => this.props.mopidyActions.setRepeat(false)}><Icon name="repeat" /><span className="tooltip__content">Repeat</span></a>
}
return button;
}
@ -129,7 +129,7 @@ class PlaybackControls extends React.Component{
}
return (
<div className={(this.state.expanded ? "expanded playback-controls" : "playback-controls")}>
<div className={(this.state.expanded ? "playback-controls--expanded playback-controls" : "playback-controls")}>
{this.props.next_track && this.props.next_track.images ? <Thumbnail className="hide" size="large" images={this.props.next_track.images} /> : null}

View File

@ -20,7 +20,8 @@ class PlaylistGrid extends React.Component{
e: e,
context: 'playlist',
uris: [item.uri],
items: [item]
items: [item],
tracklist_uri: item.uri
}
this.props.uiActions.showContextMenu(data)
}

View File

@ -26,7 +26,7 @@ class PusherConnectionList extends React.Component{
render(){
if (!this.props.connected){
return <div className="pusher-connection-list grey-text">Not connected</div>;
return <div className="pusher-connection-list mid_grey-text">Not connected</div>;
}
var connections = [];
@ -37,7 +37,7 @@ class PusherConnectionList extends React.Component{
}
if (connections.length <= 0){
return <div className="pusher-connection-list grey-text">No connections</div>;
return <div className="pusher-connection-list mid_grey-text">No connections</div>;
}
return (
@ -51,10 +51,10 @@ class PusherConnectionList extends React.Component{
return (
<div className={is_me ? 'connection cf me':'connection cf'} key={connection.connection_id}>
<div className="col w30">{ connection.username } {is_me ? <span>(you)</span> : null }</div>
<div className="col w70">
<div className="col col--w30">{ connection.username } {is_me ? <span>(you)</span> : null }</div>
<div className="col col--w70">
{connection.ip}
<span className="grey-text"> ({connection.connection_id})</span>
<span className="mid_grey-text"> ({connection.connection_id})</span>
</div>
</div>
);

View File

@ -13,21 +13,27 @@ export default class RelatedArtists extends React.Component{
if (!this.props.artists ) return null
return (
<div className="list related-artist-list">
<div className="related-artists">
{
this.props.artists.map((artist, index) => {
var images = artist.images;
if (Array.isArray(images)){
images = images[0];
}
if (artist.uri){
return (
<URILink type="artist" uri={artist.uri} key={artist.uri} className="artist">
<Thumbnail circle={true} size="small" images={artist.images} />
<span className="name">{ artist.name }</span>
<URILink type="artist" uri={artist.uri} key={artist.uri} className="related-artists__item related-artists__item--link">
<Thumbnail className="related-artists__item__thumbnail" circle={true} size="small" images={images} />
<span className="related-artists__item__name">{ artist.name }</span>
</URILink>
)
} else {
return (
<span key={artist.uri} className="artist">
<Thumbnail circle={true} size="small" images={artist.images} />
<span className="name">{ artist.name }</span>
<span key={artist.uri} className="related-artists__item">
<Thumbnail className="related-artists__item__thumbnail" circle={true} size="small" images={images} />
<span className="related-artists__item__name">{ artist.name }</span>
</span>
)
}

View File

@ -1,7 +1,7 @@
import React, { PropTypes } from 'react'
import { connect } from 'react-redux'
import { Link } from 'react-router'
import Link from './Link'
import { bindActionCreators } from 'redux'
import Thumbnail from './Thumbnail'
@ -32,13 +32,13 @@ class Services extends React.Component{
}
componentDidMount(){
if (this.props.spotify.me && this.props.core.users[this.props.spotify.me.id] === undefined){
if ((this.props.spotify.enabled || this.props.spotify.authorization) && (!this.props.spotify.me || this.props.core.users[this.props.spotify.me.id] === undefined)){
this.props.spotifyActions.getMe();
}
if (this.props.lastfm.authorization && this.props.core.users["lastfm:user:"+this.props.lastfm.authorization.name] === undefined){
this.props.lastfmActions.getMe();
}
if (this.props.genius.me && this.props.core.users["genius:user:"+this.props.genius.me.id] === undefined){
if (this.props.genius.authorization && (!this.props.genius.me || this.props.core.users["genius:user:"+this.props.genius.me.id] === undefined)){
this.props.geniusActions.getMe();
}
}
@ -76,7 +76,7 @@ class Services extends React.Component{
<Thumbnail circle={true} size="small" images={user_object.images} />
<span className="user-name">
{user_object.name ? user_object.name : user_object.id}
{!this.props.spotify.authorization ? <span className="grey-text">&nbsp;&nbsp;(Limited access)</span> : null}
{!this.props.spotify.authorization ? <span className="mid_grey-text">&nbsp;&nbsp;(Limited access)</span> : null}
</span>
</URILink>
)
@ -295,51 +295,51 @@ class Services extends React.Component{
}
return (
<div className="menu">
<div className="menu" id="services-menu">
<div className="menu__inner">
<Link className={"menu-item"+(this.props.active == 'spotify' ? ' menu-item--active' : '')} to={this.props.active == 'spotify' ? global.baseURL+'settings' : global.baseURL+'settings/service/spotify'}>
<Link className={"menu-item"+(this.props.active == 'spotify' ? ' menu-item--active' : '')} to={this.props.active == 'spotify' ? global.baseURL+'settings' : global.baseURL+'settings/service/spotify'} scrollTo="services-menu">
<div className="menu-item__inner">
{spotify_icon}
<div className="menu-item__title">
Spotify
</div>
{this.props.spotify.authorization ? <span className="status green-text">Authorized</span> : <span className="status grey-text">Read-only</span>}
{this.props.spotify.authorization ? <span className="status green-text">Authorized</span> : <span className="status mid_grey-text">Read-only</span>}
</div>
</Link>
<Link className={"menu-item"+(this.props.active == 'lastfm' ? ' menu-item--active' : '')} to={this.props.active == 'lastfm' ? global.baseURL+'settings' : global.baseURL+'settings/service/lastfm'}>
<Link className={"menu-item"+(this.props.active == 'lastfm' ? ' menu-item--active' : '')} to={this.props.active == 'lastfm' ? global.baseURL+'settings' : global.baseURL+'settings/service/lastfm'} scrollTo="services-menu">
<div className="menu-item__inner">
{lastfm_icon}
<div className="menu-item__title">
LastFM
</div>
{this.props.lastfm.authorization ? <span className="status green-text">Authorized</span> : <span className="status grey-text">Read-only</span>}
{this.props.lastfm.authorization ? <span className="status green-text">Authorized</span> : <span className="status mid_grey-text">Read-only</span>}
</div>
</Link>
<Link className={"menu-item"+(this.props.active == 'genius' ? ' menu-item--active' : '')} to={this.props.active == 'genius' ? global.baseURL+'settings' : global.baseURL+'settings/service/genius'}>
<Link className={"menu-item"+(this.props.active == 'genius' ? ' menu-item--active' : '')} to={this.props.active == 'genius' ? global.baseURL+'settings' : global.baseURL+'settings/service/genius'} scrollTo="services-menu">
<div className="menu-item__inner">
{genius_icon}
<div className="menu-item__title">
Genius
</div>
{this.props.genius.authorization ? <span className="status green-text">Authorized</span> : <span className="status grey-text">Unauthorized</span>}
{this.props.genius.authorization ? <span className="status green-text">Authorized</span> : <span className="status mid_grey-text">Unauthorized</span>}
</div>
</Link>
<Link className={"menu-item"+(this.props.active == 'snapcast' ? ' menu-item--active' : '')} to={this.props.active == 'snapcast' ? global.baseURL+'settings' : global.baseURL+'settings/service/snapcast'}>
<Link className={"menu-item"+(this.props.active == 'snapcast' ? ' menu-item--active' : '')} to={this.props.active == 'snapcast' ? global.baseURL+'settings' : global.baseURL+'settings/service/snapcast'} scrollTo="services-menu">
<div className="menu-item__inner">
<Icon className="menu-item__icon" name="devices" />
<div className="menu-item__title">
Snapcast
</div>
{this.props.pusher.config.snapcast_enabled ? <span className="status green-text">Enabled</span> : <span className="status grey-text">Disabled</span>}
{this.props.pusher.config.snapcast_enabled ? <span className="status green-text">Enabled</span> : <span className="status mid_grey-text">Disabled</span>}
</div>
</Link>
<Link className={"menu-item"+(this.props.active == 'icecast' ? ' menu-item--active' : '')} to={this.props.active == 'icecast' ? global.baseURL+'settings' : global.baseURL+'settings/service/icecast'}>
<Link className={"menu-item"+(this.props.active == 'icecast' ? ' menu-item--active' : '')} to={this.props.active == 'icecast' ? global.baseURL+'settings' : global.baseURL+'settings/service/icecast'} scrollTo="services-menu">
<div className="menu-item__inner">
<Icon className="menu-item__icon" name="wifi_tethering" />
<div className="menu-item__title">
Icecast
</div>
{this.props.core.http_streaming_enabled ? <span className="status green-text">Enabled</span> : <span className="status grey-text">Disabled</span>}
{this.props.core.http_streaming_enabled ? <span className="status green-text">Enabled</span> : <span className="status mid_grey-text">Disabled</span>}
</div>
</Link>
</div>

View File

@ -2,8 +2,9 @@
import React, { PropTypes } from 'react'
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
import { Link, withRouter } from 'react-router'
import { withRouter } from 'react-router'
import Link from './Link';
import Icon from './Icon'
import Dropzones from './Fields/Dropzones'
import Thumbnail from './Thumbnail'

View File

@ -2,9 +2,11 @@
import React, { PropTypes } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { Link, hashHistory } from 'react-router';
import { hashHistory } from 'react-router'
import Link from './Link';
import VolumeControl from './Fields/VolumeControl';
import MuteControl from './Fields/MuteControl';
import LatencyControl from './Fields/LatencyControl';
import TextField from './Fields/TextField';
import DropdownField from './Fields/DropdownField';
@ -69,7 +71,7 @@ class Snapcast extends React.Component{
if (!clients || clients.length <= 0){
return (
<div className="text grey-text">
<div className="text mid_grey-text">
No clients
</div>
);
@ -87,12 +89,6 @@ class Snapcast extends React.Component{
}
if (this.state.clients_expanded.includes(client.id)){
var commands = {};
if (this.props.client_commands[client.id]){
commands = this.props.client_commands[client.id];
}
return (
<div className={class_name+" snapcast__client--expanded"} key={client.id}>
<div className="snapcast__client__header" onClick={e => this.toggleClientExpanded(client.id)}>
@ -140,12 +136,15 @@ class Snapcast extends React.Component{
Volume
</div>
<div className="input">
<MuteControl
className="snapcast__client__mute-control"
mute={client.mute}
onMuteChange={mute => this.props.snapcastActions.setClientMute(client.id, mute)}
/>
<VolumeControl
className="snapcast__client__volume-control"
volume={client.volume}
mute={client.mute}
onVolumeChange={percent => this.props.snapcastActions.setClientVolume(client.id, percent)}
onMuteChange={mute => this.props.snapcastActions.setClientMute(client.id, mute)}
/>
</div>
</div>
@ -167,21 +166,6 @@ class Snapcast extends React.Component{
/>
</div>
</div>
<div className="field">
<div className="name tooltip">
Power command
<span className="tooltip__content">
Ajax request to trigger power
</span>
</div>
<div className="input">
<TextField
onChange={value => this.props.snapcastActions.setClientCommand(client.id, {power: value})}
value={commands.power ? commands.power : ""}
placeholder='{"url":"https://myserver.local:8080/sendCommand/power"}'
/>
</div>
</div>
</div>
</div>
);
@ -274,7 +258,7 @@ class Snapcast extends React.Component{
<div className="input">
<div className="text">
{group.name} &nbsp;
<span className="grey-text">({group.id})</span>
<span className="mid_grey-text">({group.id})</span>
</div>
</div>
</div>
@ -301,12 +285,15 @@ class Snapcast extends React.Component{
Volume
</div>
<div className="input">
<MuteControl
className="snapcast__group__mute-control"
mute={group.muted}
onMuteChange={mute => this.props.snapcastActions.setGroupMute(group.id, mute)}
/>
<VolumeControl
className="snapcast__group__volume-control"
volume={group_volume}
mute={group.muted}
onVolumeChange={(percent, old_percent) => this.props.snapcastActions.setGroupVolume(group.id, percent, old_percent)}
onMuteChange={mute => this.props.snapcastActions.setGroupMute(group.id, mute)}
/>
</div>
</div>
@ -335,8 +322,7 @@ const mapStateToProps = (state, ownProps) => {
show_disconnected_clients: (state.ui.snapcast_show_disconnected_clients !== undefined ? state.ui.snapcast_show_disconnected_clients : false),
streams: (state.snapcast.streams ? state.snapcast.streams : null),
groups: (state.snapcast.groups ? state.snapcast.groups : null),
clients: (state.snapcast.clients ? state.snapcast.clients : null),
client_commands: (state.snapcast.client_commands ? state.snapcast.client_commands : {})
clients: (state.snapcast.clients ? state.snapcast.clients : null)
}
}

View File

@ -2,7 +2,7 @@
import React, { PropTypes } from 'react';
import { connect } from 'react-redux';
import { createStore, bindActionCreators } from 'redux';
import { Link } from 'react-router';
import Link from './Link';
import Icon from './Icon'
@ -14,35 +14,8 @@ class Thumbnail extends React.Component{
constructor(props){
super(props);
}
/*
// TODO: ascertain whether this is improving or hindering performance
// The UI appears to work perfectly fine without this
shouldComponentUpdate(nextProps, nextState){
// no images at all, and we already know it
if (!nextProps.image && !this.props.image && !nextProps.images && !this.props.images) return false
// image changed
if (!this.props.image && nextProps.image) return true
if (this.props.image && nextProps.image) return true
if (this.props.image != nextProps.image) return true
// images array changed
if (this.props.images === undefined && nextProps.images ) return true
if (this.props.images && nextProps.images === undefined) return true
if (this.props.images && !nextProps.images || this.props.images.length != nextProps.images.length ) return true
// image item changed
var size = 'medium'
var images = helpers.sizedImages(nextProps.images )
if (this.props.size ) size = this.props.size
if (this.props.images[size] != images[size]) return true
return false
}
*/
mapImageSizes(props = this.props){
mapImageSizes(){
// Single image
if (this.props.image){
@ -50,6 +23,12 @@ class Thumbnail extends React.Component{
// Multiple images
} else if (this.props.images){
var images = this.props.images;
// An array of image objects (eg Artists), so just pick the first one
if (Array.isArray(images) && images.length > 0){
images = images[0];
}
// Default to medium-sized image, but accept size property as override
var size = 'medium';
@ -57,7 +36,10 @@ class Thumbnail extends React.Component{
size = this.props.size;
}
return this.props.images[size];
// Return the requested size
if (images[size]){
return images[size];
}
}
// No images

View File

@ -1,6 +1,6 @@
import React, { PropTypes } from 'react'
import { Link } from 'react-router'
import Link from './Link'
import Icon from './Icon'
import ArtistSentence from './ArtistSentence'
@ -182,51 +182,41 @@ export default class Track extends React.Component{
}
var track = this.props.track;
var className = 'list__item track mouse-draggable mouse-selectable mouse-contextable';
if (this.props.selected) className += ' selected';
if (this.props.can_sort) className += ' can-sort';
if (track.type !== undefined) className += ' '+track.type;
if (track.playing) className += ' playing';
if (this.state.hover) className += ' hover';
var album = '-';
if (track.album){
if (track.album.uri){
album = <URILink type="album" uri={track.album.uri}>{track.album.name}</URILink>;
} else {
album = <span>{track.album.name}</span>;
}
var className = 'list__item list__item--track mouse-draggable mouse-selectable mouse-contextable';
var track_details = [];
var track_actions = [];
if (track.artists){
track_details.push(
<li className="details__item details__item--artists" key="artists">
{track.artists ? <ArtistSentence artists={track.artists} /> : '-'}
</li>
)
}
let track_columns = [];
let track_actions = [];
if (track.album){
if (track.album.uri){
var album = <URILink type="album" uri={track.album.uri}>{track.album.name}</URILink>;
} else {
var album = <span>{track.album.name}</span>;
}
track_details.push(
<li className="details__item details__item--album" key="album">
{album}
</li>
)
}
if (this.props.context == 'history'){
track_columns.push(
<span className="col name" key="name">
{track.name ? track.name : <span className="grey-text">{track.uri}</span>}
{track.explicit ? <span className="flag dark">EXPLICIT</span> : null}
</span>
)
track_columns.push(
<span className="col artists" key="artists">
{track.artists ? <ArtistSentence artists={track.artists} /> : '-'}
</span>
)
track_columns.push(
<span className="col album" key="album">
{album}
</span>
)
track_columns.push(
<span className="col played_at" key="played_at">
var track_middle_column = (
<div className="list__item__column__item list__item__column__item--played_at">
{track.played_at ? <span><Dater type="ago" data={track.played_at} /> ago</span> : '-'}
</span>
</div>
)
} else if (this.props.context == 'queue'){
if (track.added_from && track.added_by){
var type = (track.added_from ? helpers.uriType(track.added_from) : null);
@ -247,101 +237,44 @@ export default class Track extends React.Component{
var link = <URILink type={type} uri={track.added_from}>{type}</URILink>;
}
var added = <span><span className="by">{track.added_by}</span><span className="grey-text from"><span className="label">from </span>{link}</span></span>
var track_middle_column = (
<div className="list__item__column__item list__item__column__item--added">
<span className="by">{track.added_by} </span>
<span className="from">from {link}</span>
</div>
);
} else if (track.added_by){
var added = <span className="by">{track.added_by}</span>
} else {
var added = <span className="empty">-</span>
}
track_columns.push(
<span className="col name" key="name">
{track.name ? track.name : <span className="grey-text">{track.uri}</span>}
{track.explicit ? <span className="flag dark">EXPLICIT</span> : null}
</span>
)
if (this.props.show_source_icon){
track_columns.push(
<Icon type="fontawesome" name={helpers.sourceIcon(track.uri)} className="source" key="source" fixedWidth />
)
}
track_columns.push(
<span className="col artists" key="artists">
{track.artists ? <ArtistSentence artists={track.artists} /> : '-'}
</span>
)
track_columns.push(
<span className="col album" key="album">
{album}
</span>
)
track_columns.push(
<span className="col added" key="added">
{added}
</span>
)
track_columns.push(
<span className="col duration" key="duration">
{track.duration ? <Dater type="length" data={track.duration} /> : null}
</span>
)
} else {
track_columns.push(
<span className="col name" key="name">
{track.name ? track.name : <span className="grey-text">{track.uri}</span>}
{track.explicit ? <span className="flag dark">EXPLICIT</span> : null}
</span>
)
if (this.props.show_source_icon){
track_columns.push(
<Icon type="fontawesome" name={helpers.sourceIcon(track.uri)} className="source" key="source" fixedWidth />
)
}
track_columns.push(
<span className="col artists" key="artists">
{track.artists ? <ArtistSentence artists={track.artists} /> : '-'}
</span>
)
track_columns.push(
<span className="col album" key="album">
{album}
</span>
)
track_columns.push(
<span className="col duration" key="duration">
{track.duration ? <Dater type="length" data={track.duration} /> : '-'}
</span>
)
if (track.popularity !== undefined){
track_columns.push(
<span className="col popularity" key="popularity">
<Popularity popularity={track.popularity} />
</span>
)
var track_middle_column = (
<div className="list__item__column__item list__item__column__item--added">
<span className="by">{track.added_by}</span>
</div>
);
}
}
track_actions.push(
<ContextMenuTrigger className="subtle" key="context" onTrigger={e => this.props.handleContextMenu(e)} />
)
// If we're touchable, and can sort this tracklist
var drag_zone = null;
if (helpers.isTouchDevice() && this.props.can_sort){
className += " has-touch-drag-zone"
className += " list__item--has-drag-zone"
track_actions.push(
drag_zone = (
<span
className="drag-zone touch-draggable mouse-draggable"
className="list__item__column__item list__item__column__item--drag-zone drag-zone touch-draggable mouse-draggable"
key="drag-zone">
<Icon name="drag_indicator" />
</span>
)
);
}
if (this.props.selected) className += ' list__item--selected';
if (this.props.can_sort) className += ' list__item--can-sort';
if (track.type !== undefined) className += ' list__item--'+track.type;
if (track.playing) className += ' list__item--playing';
if (this.state.hover) className += ' list__item--hover';
if (track_middle_column) className += " list__item--has-middle-column";
if (track_details.length > 0) className += " list__item--has-details";
return (
<ErrorBoundary>
<div
@ -355,8 +288,27 @@ export default class Track extends React.Component{
onContextMenu={e => this.props.handleContextMenu(e)}
onTouchStart={e => this.handleTouchStart(e)}
onTouchEnd={e => this.handleTouchEnd(e)}>
{track_actions}
{track_columns}
<div className="list__item__column list__item__column--right">
{drag_zone}
<span className="list__item__column__item list__item__column__item--duration">
{track.duration ? <Dater type="length" data={track.duration} /> : '-'}
</span>
{this.props.show_source_icon ? <span className="list__item__column__item list__item__column__item--source">
<Icon type="fontawesome" name={helpers.sourceIcon(track.uri)} fixedWidth />
</span> : null}
<ContextMenuTrigger className="list__item__column__item--context-menu-trigger subtle" onTrigger={e => this.props.handleContextMenu(e)} />
</div>
<div className="list__item__column list__item__column--name">
<div className="list__item__column__item--name">
{track.name ? track.name : <span className="mid_grey-text">{track.uri}</span>}
{track.explicit ? <span className="flag dark">EXPLICIT</span> : null}
{track.playing ? <Icon className={"js--"+this.props.play_state} name="playing" type="css"></Icon> : null}
</div>
{track_details ? <ul className="list__item__column__item--details">
{track_details}
</ul> : null}
</div>
{track_middle_column ? <div className="list__item__column list__item__column--middle">{track_middle_column}</div> : null}
</div>
</ErrorBoundary>
);

View File

@ -134,7 +134,7 @@ class TrackList extends React.Component{
let touch = e.touches[0];
let over = $(document.elementFromPoint(touch.clientX, touch.clientY));
if (!over.is('.track')){
over = over.closest('.track');
over = over.closest('.list__item--track');
}
$(document).find('.touch-drag-hover').removeClass('touch-drag-hover');
if (over.length > 0){
@ -153,12 +153,12 @@ class TrackList extends React.Component{
if (this.touch_dragging_tracks_keys){
let touch = e.changedTouches[0];
let over = $(document.elementFromPoint(touch.clientX, touch.clientY));
if (!over.is('.track')){
over = over.closest('.track');
if (!over.is('.list__item--track')){
over = over.closest('.list__item--track');
}
if (over.length > 0){
let siblings = over.parent().children('.track');
let dropped_at = siblings.index(over) - 1;
let siblings = over.parent().children('.list__item--track');
let dropped_at = siblings.index(over);
if (this.props.reorderTracks !== undefined){
this.props.reorderTracks(helpers.arrayOf('index',this.digestTracksKeys()),dropped_at);
@ -350,7 +350,7 @@ class TrackList extends React.Component{
}
// Accommodate a single key
let singleton = false
var singleton = false
if (!(keys instanceof Array)){
singleton = true
keys = [keys]
@ -358,20 +358,21 @@ class TrackList extends React.Component{
// Construct a basic track object, based on our unique track key
// This is enough to perform interactions (dragging, selecting, etc)
let array = []
for (let i = 0; i < keys.length; i++){
let key = keys[i].split('@@')
var array = [];
for (var key of keys){
var key_components = key.split('@@')
if (indexes_only){
array.push(key[0])
array.push(key_components[0])
} else {
array.push({
index: parseInt(key[0]),
tlid: parseInt(key[1]),
uri: key[2],
context: key[3],
context_uri: key[4]
key: key,
index: parseInt(key_components[0]),
tlid: parseInt(key_components[1]),
uri: key_components[2],
context: key_components[3],
context_uri: key_components[4]
})
}
}
@ -383,64 +384,19 @@ class TrackList extends React.Component{
}
}
renderHeader(){
if (this.props.noheader ) return null
switch (this.props.context){
case 'history':
return (
<div className="list__item header track">
<div className="liner">
<span className="col name">Name</span>
<span className="col artists">Artists</span>
<span className="col album">Album</span>
<span className="col played_at">Started playing</span>
</div>
</div>
)
break
case 'queue':
return (
<div className="list__item header track">
<div className="liner">
<span className="col name">Name</span>
<span className="col artists">Artists</span>
<span className="col album">Album</span>
<span className="col added">Added by</span>
<span className="col duration">Duration</span>
</div>
</div>
)
break
default:
return (
<div className="list__item header track">
<div className="liner">
<span className="col name">Name</span>
<span className="col artists">Artists</span>
<span className="col album">Album</span>
<span className="col duration">Duration</span>
<span className="col popularity"></span>
</div>
</div>
)
}
}
render(){
if (!this.props.tracks || Object.prototype.toString.call(this.props.tracks) !== '[object Array]' ) return null
if (!this.props.tracks || Object.prototype.toString.call(this.props.tracks) !== '[object Array]' ){
return null;
}
var className = 'list track-list '+this.props.context
var className = 'list list--tracks '+this.props.context
if (this.props.className){
className += ' '+this.props.className
}
return (
<div className={className}>
{ this.renderHeader() }
{
this.props.tracks.map(
(track, index) => {
@ -455,6 +411,7 @@ class TrackList extends React.Component{
context={this.props.context}
can_sort={this.props.context == 'queue' || this.props.context == 'editable-playlist'}
selected={this.props.selected_tracks.includes(track_key)}
play_state={this.props.play_state}
dragger={this.props.dragger}
handleClick={(e) => this.handleClick(e, track_key)}
handleDoubleClick={e => this.handleDoubleClick(e, track_key)}
@ -474,15 +431,9 @@ class TrackList extends React.Component{
}
}
/**
* Export our component
*
* We also integrate our global store, using connect()
**/
const mapStateToProps = (state, ownProps) => {
return {
play_state: state.mopidy.play_state,
slim_mode: state.ui.slim_mode,
selected_tracks: state.ui.selected_tracks,
dragger: state.ui.dragger,

View File

@ -1,6 +1,6 @@
import React from 'react'
import { Link } from 'react-router'
import React from 'react';
import Link from './Link';
export default class URILink extends React.Component{