Basic mobile support (#81)

Mobile specific view. Basically Pony.fm desktop squashed into a mobile view
This commit is contained in:
Josef Citrine 2016-05-19 00:46:58 +01:00
parent 87d25bb8b8
commit ca397f09fc
12 changed files with 311 additions and 40 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

24
public/manifest.json Normal file
View file

@ -0,0 +1,24 @@
{
"short_name": "Pony.fm",
"name": "Pony.fm",
"icons": [
{
"src": "images/icons/app-icon-96.png",
"sizes": "96x96",
"type": "image/png"
},
{
"src": "images/icons/app-icon-144.png",
"sizes": "144x144",
"type": "image/png"
},
{
"src": "images/icons/app-icon-192.png",
"sizes": "192x192",
"type": "image/png"
}
],
"start_url": "/",
"display": "standalone",
"orientation": "portrait"
}

47
public/service-worker.js Normal file
View file

@ -0,0 +1,47 @@
// Pony.fm - A community for pony fan music.
// Copyright (C) 2016 Josef Citrine
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
var urlsToCache = [
'/',
'/build/styles/app.css',
'/build/scripts/app.js',
'/build/scripts/templates.js',
'/templates/directives/player.html',
'/templates/directives/search.html',
'/templates/directives/tracks-list.html',
'/templates/directives/users-list.html',
'/templates/directives/albums-list.html',
'/templates/directives/playlists-list.html',
'/templates/home/index.html',
];
var CACHE_NAME = 'pfm-cache-v1';
// Set the callback for the install step
self.addEventListener('install', function(event) {
// Doesn't do anything right now
// Could never get offline to fully
// work without bugs :(
// Perform install steps
event.waitUntil(
caches.open(CACHE_NAME)
.then(function(cache) {
console.log('Opened cache');
})
);
});

View file

@ -21,9 +21,20 @@ module.exports = angular.module('ponyfm').controller "application", [
$scope.$state = $state $scope.$state = $state
$scope.$stateParams = $stateParams $scope.$stateParams = $stateParams
$scope.isPinnedPlaylistSelected = false $scope.isPinnedPlaylistSelected = false
$scope.menuActive = false
$loadingElement = null $loadingElement = null
loadingStateName = null loadingStateName = null
if 'serviceWorker' of navigator
console.log 'Service Worker is supported'
navigator.serviceWorker.register('service-worker.js').then((reg) ->
console.log 'SW registered', reg
).catch (err) ->
console.log 'SW register failed', err
$scope.menuToggle = () ->
$scope.menuActive = !$scope.menuActive
if window.pfm.error if window.pfm.error
$state.transitionTo 'errors-' + window.pfm.error $state.transitionTo 'errors-' + window.pfm.error
@ -60,6 +71,7 @@ module.exports = angular.module('ponyfm').controller "application", [
statesPreloaded = {} statesPreloaded = {}
$scope.$on '$stateChangeStart', (e, newState, newParams, oldState, oldParams) -> $scope.$on '$stateChangeStart', (e, newState, newParams, oldState, oldParams) ->
$scope.menuActive = false
$scope.isPinnedPlaylistSelected = false $scope.isPinnedPlaylistSelected = false
if newState.name == 'content.playlist' if newState.name == 'content.playlist'

View file

@ -16,7 +16,13 @@
window.handleResize = () -> window.handleResize = () ->
windowHeight = $(window).height() windowHeight = $(window).height()
windowWidth = $(window).width()
isMobile = windowWidth <= 480
$siteBody = $ '.site-body' $siteBody = $ '.site-body'
if isMobile
$siteBody.height windowHeight - $('.now-playing').height() * 2
else
$siteBody.height windowHeight - $('header').height() $siteBody.height windowHeight - $('header').height()
$('.dropdown-menu').each () -> $('.dropdown-menu').each () ->
@ -26,6 +32,7 @@ window.handleResize = () ->
'max-height': newMaxHeight 'max-height': newMaxHeight
$('.stretch-to-bottom').each () -> $('.stretch-to-bottom').each () ->
if !isMobile
$this = $ this $this = $ this
newHeight = windowHeight - $this.offset().top newHeight = windowHeight - $this.offset().top
if newHeight > 0 if newHeight > 0

View file

@ -36,3 +36,4 @@
@import 'components/uploader'; @import 'components/uploader';
@import 'components/search'; @import 'components/search';
@import 'components/track-editor'; @import 'components/track-editor';
@import 'mobile';

View file

@ -31,35 +31,7 @@ html body {
} }
header { header {
> a {
display: block;
float: left;
width: (@pfm-sidebar-size);
height: 64px;
line-height: 42px;
background: #84528A;
color: #fff;
font-size: 24pt;
font-weight: lighter;
position: relative;
z-index: 600;
font-family: 'Josefin Sans', sans-serif;
position: relative;
&:hover {
background: darken(#84528A, 25%);
color: #fff;
text-decoration: none;
}
img.logo {
padding-left: 6px;
width: 100%;
max-width: 90%;
padding-top: 13px;
max-height: 45%
}
}
.user-details { .user-details {
float: right; float: right;
@ -116,15 +88,46 @@ header {
} }
.sidebar { .sidebar {
> a {
display: block;
float: left;
width: (@pfm-sidebar-size);
height: 64px;
line-height: 42px;
background: #84528A;
color: #fff;
font-size: 24pt;
font-weight: lighter;
position: relative;
z-index: 600;
font-family: 'Josefin Sans', sans-serif;
position: relative;
&:hover {
background: darken(#84528A, 25%);
color: #fff;
text-decoration: none;
}
img.logo {
padding-left: 6px;
width: 100%;
max-width: 90%;
padding-top: 13px;
max-height: 45%
}
}
background: #515151; background: #515151;
width: @pfm-sidebar-size; width: @pfm-sidebar-size;
height: 100%; height: 100%;
float: left;
list-style: none; list-style: none;
padding: 0px; padding: 0px;
margin: 0px; margin: 0px;
font-size: 10pt; font-size: 10pt;
position: relative; position: absolute;
bottom: 0;
left: 0;
li { li {
margin: 0px; margin: 0px;
@ -251,3 +254,41 @@ header {
.chart-btn-container { .chart-btn-container {
margin-bottom: 10px; margin-bottom: 10px;
} }
.mobile-header {
display: none;
}
.burger-wrapper {
top:20px;
left: 20px;
position: absolute;
}
.burger {
width: 25px;
height: 20px;
top: 20px;
left: 20px;
}
.bun-top,.meat,.bun-bottom {
position: absolute;
display: block;
border-radius: 4px;
background: white;
width: 25px;
height: 2px;
}
.bun-top {
top: 0;
}
.meat {
top: 9px;
}
.bun-bottom {
bottom: 0;
}

127
resources/assets/styles/mobile.less vendored Normal file
View file

@ -0,0 +1,127 @@
@media only screen and (max-width: 480px) {
html, body {
overflow: hidden !important;
}
body > header {
margin-top: 64px;
}
.site-content {
margin-left: 0px;
overflow: scroll;
}
.now-playing {
margin-left: 0px;
position: fixed;
width: 100%;
bottom: 0;
z-index: 1010;
box-shadow: 0 0 8px rgba(0,0,0,0.5);
}
.user-details {
position: fixed;
top: 0;
right: 0;
z-index: 1002;
}
.mobile-header {
display: block !important;
position: absolute;
z-index: 1001;
background: #84528A;
height: 64px;
width: 100%;
top: 0;
.logo {
text-align: center;
vertical-align: middle;
display: block;
margin-left: auto;
margin-right: auto;
height: 40px;
width: 160px;
padding-top: 16px;
> img {
max-height: 75%;
}
}
}
.sidebar {
left: -170px;
transition: transform 0.5s ease;
transform: translateX(0px) translateZ(0);
perspective: 1000;
backface-visibility: hidden;
will-change: transform;
z-index: 1011;
height: ~"calc(100% - 64px)";
box-shadow: 0 8px 8px rgba(0,0,0,0.5);
> a {
display: none;
}
}
.sidebar.active {
transform: translateX(170px) translateZ(0);
}
.user-details > .avatar {
border: 0 !important;
box-shadow: initial !important;
background: transparent !important;
> img {
border-radius: 20px;
}
> span {
display: none !important;
}
}
.track-player {
margin-right: 0px !important;
.buttons {
li.volume, li.status {
display: none;
}
}
}
input.search-input {
border: 0;
}
.dropdowns {
margin-bottom: 0px;
> li {
margin-bottom: 10px;
> .btn {
font-size: 10pt;
}
}
}
.details-columns {
.cboxElement {
display: none;
}
.right {
float: none;
padding: 0px;
width: auto;
}
.left {
margin-right: 0px;
margin-left: 0px;
margin-top: 10px;
}
}
}

View file

@ -42,9 +42,16 @@
</script> </script>
<header> <header>
<a href="/"> <div class="mobile-header">
<img src="/images/ponyfm-logo-white.svg" class="logo"> <div class="burger-wrapper" ng-click="menuToggle()">
</a> <div class="burger">
<div class="bun-top"></div>
<div class="meat"></div>
<div class="bun-bottom"></div>
</div>
</div>
<a href="/" class="logo"><img src="/images/ponyfm-logo-white.svg"></a>
</div>
<div class="now-playing"> <div class="now-playing">
@if (Auth::check()) @if (Auth::check())
<div class="user-details dropdown"> <div class="user-details dropdown">
@ -65,7 +72,10 @@
</header> </header>
<div class="site-body"> <div class="site-body">
<ul class="sidebar" ng-controller="sidebar"> <ul class="sidebar" ng-controller="sidebar" ng-class="{'active': menuActive}">
<a href="/">
<img src="/images/ponyfm-logo-white.svg" class="logo">
</a>
<li><pfm-search></pfm-search></li> <li><pfm-search></pfm-search></li>
<li ng-class="{selected: stateIncludes('content.tracks') || stateIncludes('content.track')}"><a href="/tracks">Tracks</a></li> <li ng-class="{selected: stateIncludes('content.tracks') || stateIncludes('content.track')}"><a href="/tracks">Tracks</a></li>
<li ng-class="{selected: stateIncludes('content.albums') || stateIncludes('content.album')}"><a href="/albums">Albums</a></li> <li ng-class="{selected: stateIncludes('content.albums') || stateIncludes('content.album')}"><a href="/albums">Albums</a></li>

View file

@ -21,7 +21,9 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" /> <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<title ng-bind="title">Pony.fm</title> <title ng-bind="title">Pony.fm</title>
<meta name="description" content="@{{ description }}" /> <meta name="description" content="@{{ description }}" />
<meta name="viewport" content="width=device-width" /> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
<meta name="theme-color" content="#84528A" />
<link rel="manifest" href="/manifest.json">
<base href="/" /> <base href="/" />
@yield('styles') @yield('styles')