├── .gitignore
├── README.md
├── babel.config.js
├── package.json
├── public
├── icon-128.png
├── icon-16.png
├── icon-48.png
├── index.html
└── manifest.json
├── screenshot.png
├── src
├── App.vue
├── components
│ ├── calendar
│ │ ├── day.vue
│ │ ├── event.vue
│ │ └── index.vue
│ ├── global
│ │ ├── autosize.vue
│ │ └── header.vue
│ ├── note
│ │ ├── index.vue
│ │ └── note.vue
│ └── task
│ │ ├── index.vue
│ │ └── task.vue
├── icons
│ ├── check.vue
│ ├── drag.vue
│ ├── plus.vue
│ ├── refresh.vue
│ └── remove.vue
├── main.js
├── model
│ └── index.js
├── store
│ ├── dummy-data.js
│ └── index.js
└── utils
│ ├── get-token.js
│ └── http-calendar.js
├── vue.config.js
└── yarn.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | /dist
4 |
5 | # local env files
6 | .env.local
7 | .env.*.local
8 |
9 | # Log files
10 | npm-debug.log*
11 | yarn-debug.log*
12 | yarn-error.log*
13 |
14 | # Editor directories and files
15 | .idea
16 | .vscode
17 | *.suo
18 | *.ntvs*
19 | *.njsproj
20 | *.sln
21 | *.sw*
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Frame
2 |
3 | New-tab extension for Chrome and Firefox
4 |
5 |
6 |
7 | ## Install
8 |
9 | - [**Chrome** extension](https://chrome.google.com/webstore/detail/frame/pimalalkfhkmnlhoapdlhilkghboiimc)
10 | - [**Firefox** extension](https://addons.mozilla.org/tr/firefox/addon/new-tab-frame/)
11 |
12 | ## Contribute
13 |
14 | Suggestions and pull requests are highly encouraged!
15 |
16 | In order to make modifications to the extension you'd need to run it locally.
17 |
18 | Please follow the below steps:
19 |
20 | ```sh
21 | git clone https://github.com/ademilter/frame
22 | cd frame
23 | yarn # Install dev dependencies
24 | yarn serve # Listen for file changes and automatically rebuild
25 | yarn build:chrome # Build the extension code so it's ready for Chrome
26 | yarn build:firefox # Build the extension code so it's ready for Firefox
27 | ```
28 |
29 | ## Maintainers
30 |
31 | - [Adem ilter](https://github.com/ademilter)
32 | - [Nazım Can Altınova](https://github.com/canaltinova)
33 |
34 | ## License
35 |
36 | MIT
37 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: ['@vue/app']
3 | }
4 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Frame",
3 | "version": "1.0.0",
4 | "private": true,
5 | "scripts": {
6 | "serve": "vue-cli-service serve",
7 | "build": "yarn run build:chrome && yarn run build:firefox && yarn run clear",
8 | "build:chrome": "vue-cli-service build --dest dist/chrome && yarn run zip:chrome",
9 | "build:firefox": "VUE_APP_PLATFORM=firefox vue-cli-service build --dest dist/firefox && yarn run zip:firefox",
10 | "zip:chrome": "cd dist/chrome/ && bestzip ../chrome.zip **",
11 | "zip:firefox": "cd dist/firefox/ && bestzip ../firefox.zip **",
12 | "clear": "rimraf dist/chrome/ && rimraf dist/firefox/"
13 | },
14 | "dependencies": {
15 | "autosize": "^4.0.2",
16 | "axios": "^0.18.1",
17 | "moment": "^2.22.2",
18 | "vue": "^2.5.16",
19 | "vue-quill-editor": "^3.0.6",
20 | "vuedraggable": "^2.16.0",
21 | "vuex": "^3.0.1",
22 | "vuex-persistedstate": "^2.5.4"
23 | },
24 | "devDependencies": {
25 | "@vue/cli-plugin-babel": "^3.0.0-beta.15",
26 | "@vue/cli-plugin-eslint": "^3.0.0-beta.15",
27 | "@vue/cli-service": "^3.0.0-beta.15",
28 | "@vue/eslint-config-standard": "^3.0.0-rc.3",
29 | "bestzip": "^2.1.1",
30 | "moment-locales-webpack-plugin": "^1.0.7",
31 | "postcss-nested": "^3.0.0",
32 | "postcss-sorting": "^3.1.0",
33 | "pug": "^2.0.3",
34 | "pug-plain-loader": "^1.0.0",
35 | "vue-template-compiler": "^2.5.16"
36 | },
37 | "eslintConfig": {
38 | "root": true,
39 | "env": {
40 | "node": true
41 | },
42 | "extends": [
43 | "plugin:vue/essential",
44 | "@vue/standard"
45 | ],
46 | "rules": {
47 | "vue/script-indent": [
48 | "error",
49 | 2,
50 | {
51 | "baseIndent": 1
52 | }
53 | ],
54 | "indent": "off"
55 | },
56 | "parserOptions": {
57 | "parser": "babel-eslint"
58 | }
59 | },
60 | "postcss": {
61 | "plugins": {
62 | "autoprefixer": {},
63 | "postcss-nested": {},
64 | "postcss-sorting": {
65 | "order": [
66 | "custom-properties",
67 | "declarations",
68 | "rules",
69 | "at-rules"
70 | ],
71 | "properties-order": "alphabetical"
72 | }
73 | }
74 | },
75 | "browserslist": [
76 | "> 1%",
77 | "last 2 versions",
78 | "not ie <= 8"
79 | ]
80 | }
81 |
--------------------------------------------------------------------------------
/public/icon-128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ademilter/Frame/61aaecf96683a5cce8d8443682fc867483a06289/public/icon-128.png
--------------------------------------------------------------------------------
/public/icon-16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ademilter/Frame/61aaecf96683a5cce8d8443682fc867483a06289/public/icon-16.png
--------------------------------------------------------------------------------
/public/icon-48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ademilter/Frame/61aaecf96683a5cce8d8443682fc867483a06289/public/icon-48.png
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Frame
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Frame",
3 | "version": "1.6.2",
4 | "description": "",
5 | "manifest_version": 2,
6 | "applications": {
7 | "gecko": {
8 | "id": "{2e7abee6-b580-4813-8b57-cd2d2b7255cb}",
9 | "strict_min_version": "55.0"
10 | }
11 | },
12 | "chrome_url_overrides": {
13 | "newtab": "index.html"
14 | },
15 | "icons": {
16 | "16": "icon-16.png",
17 | "48": "icon-48.png",
18 | "128": "icon-128.png"
19 | },
20 | "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuOtWSnGxinGiZ3UZOur0f+6E1Ac29X9svVYwjzRi9AjOAcg5A6GgIkM8swJst1g6p9AojfphICj4mOMhZehb70b1oc9r9hwJxQ8al046sFg5IjFPVS5s7z9GjGlGtTdXaNaE7KZ/k8G9b945H/19tMW/HL6gWDeWQ+iXCOrMzvkMM/Iks6UbDae4awr/HiqZ2d5oAjeZlVE85SfUSnLEkEEapwY73JJD14gXXYZMhxtgfBHrVtablLo64/ujeMm53DtElrVTL2AaMirld2RjyvrpYU1AxkI81ljylQz0Tm/Lict+n3WuqYQnuSuvddFayciUo8KHeJ1J4skZa3AvXwIDAQAB",
21 | "oauth2": {
22 | "client_id_firefox": "825939795126-t9v6behgpkvj2vfffgg960gvneqi5umn.apps.googleusercontent.com",
23 | "client_id": "825939795126-mcce3h3b83llk6lid0fjds0b6age97k6.apps.googleusercontent.com",
24 | "scopes": [
25 | "https://www.googleapis.com/auth/calendar"
26 | ]
27 | },
28 | "content_security_policy": "script-src 'self' https://calendar.google.com; object-src 'self'",
29 | "permissions": [
30 | "identity",
31 | "http://*/*",
32 | "https://*/*",
33 | "http://calendar.google.com/calendar/*",
34 | "https://calendar.google.com/calendar/*",
35 | "*://www.googleapis.com/*",
36 | "*://accounts.google.com/*"
37 | ]
38 | }
39 |
--------------------------------------------------------------------------------
/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ademilter/Frame/61aaecf96683a5cce8d8443682fc867483a06289/screenshot.png
--------------------------------------------------------------------------------
/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 | .App
3 |
4 | HeaderView
5 |
6 | .Columns
7 | Notes
8 | Tasks
9 | Calendar
10 |
11 |
12 |
13 |
31 |
32 |
217 |
--------------------------------------------------------------------------------
/src/components/calendar/day.vue:
--------------------------------------------------------------------------------
1 |
2 | .event-day(:class="{ 'today' : isToday }")
3 |
4 | .event-date
5 | h4
6 | | {{ $moment(date).format('dddd') }}
7 | p
8 | small {{ $moment(date).format('D MMMM YYYY') }}
9 | p.today(v-if="isToday")
10 | small Bugün
11 |
12 | .event-list
13 | Event(
14 | v-for="event in events"
15 | :key="event.id"
16 | :event="event")
17 |
18 |
19 |
20 |
39 |
40 |
77 |
--------------------------------------------------------------------------------
/src/components/calendar/event.vue:
--------------------------------------------------------------------------------
1 |
2 | .event-item
3 | h4
4 | | {{ event.summary }}
5 | p
6 | small {{ event.location }}
7 | p
8 | small {{ time }}
9 |
10 |
11 |
12 |
30 |
31 |
38 |
--------------------------------------------------------------------------------
/src/components/calendar/index.vue:
--------------------------------------------------------------------------------
1 |
2 | .Column
3 |
4 | .Column-header
5 | h3.title CALENDAR
6 | button(
7 | class="btn small new"
8 | type="button"
9 | @click="refresh")
10 | iconRefresh
11 | span Refresh
12 |
13 | .Column-body
14 |
15 | .event-group(
16 | v-if="hasCalenderItem || hasToken")
17 | EventDay(
18 | v-for="(events, key) in eventsSortAndGroupBy"
19 | :key="key"
20 | :date="key"
21 | :events="events")
22 |
23 | button.loginWithGoogle.btn(
24 | type="button"
25 | @click="login"
26 | v-else
27 | ) Google ile giriş yap
28 |
29 |
30 |
31 |
110 |
111 |
129 |
--------------------------------------------------------------------------------
/src/components/global/autosize.vue:
--------------------------------------------------------------------------------
1 |
2 | textarea(
3 | @blur="$emit('blur', $event)"
4 | @keydown.esc="$emit('esc', $event)"
5 | @keydown.enter="$emit('enter', $event)"
6 | @input="$emit('input', $event.target.value)")
7 | | {{ value }}
8 |
9 |
10 |
21 |
--------------------------------------------------------------------------------
/src/components/global/header.vue:
--------------------------------------------------------------------------------
1 |
2 | header.Header
3 | .what-time-is
4 | .time {{ time }}
5 | .date {{ date }}
6 |
7 |
8 |
33 |
34 |
59 |
--------------------------------------------------------------------------------
/src/components/note/index.vue:
--------------------------------------------------------------------------------
1 |
2 | .Column
3 |
4 | .Column-header
5 | h3.title NOTE
6 | button(
7 | class="btn small new"
8 | type="button"
9 | @click="addNote")
10 | iconPlus
11 | span New
12 |
13 | .Column-body
14 |
15 | .empty-state(
16 | v-if="!hasNote")
17 | | Geçici notlarını buraya yaz ve unutma!
18 |
19 | Draggable(
20 | v-model="notes"
21 | @start="onDragStart"
22 | @end="onDragEnd"
23 | ref="notes"
24 | :options="{ animation: 120, handle: '.handle' }")
25 |
26 | note(
27 | v-for="note in notes"
28 | :key="note.id"
29 | :note="note")
30 |
31 |
32 |
33 |
86 |
87 |
109 |
110 |
126 |
--------------------------------------------------------------------------------
/src/components/note/note.vue:
--------------------------------------------------------------------------------
1 |
2 | .note
3 |
4 | .handle
5 | icon-drag
6 |
7 | button.remove(
8 | type="button"
9 | v-if="!removing"
10 | @click="timerRemove")
11 | iconRemove
12 |
13 | button.stop(
14 | type="button"
15 | v-if="removing"
16 | @click="stop")
17 | span {{ countdown }}
18 |
19 | quill-editor(
20 | ref="editor"
21 | v-model="content"
22 | @blur="onEditorBlur"
23 | @focus="onEditorFocus"
24 | :options="editorOption")
25 |
26 |
27 |
28 |
113 |
114 |
195 |
--------------------------------------------------------------------------------
/src/components/task/index.vue:
--------------------------------------------------------------------------------
1 |
2 | .Column
3 |
4 | .Column-header
5 | h3.title TASK
6 | button(
7 | class="btn small new"
8 | type="button"
9 | @click="addTask")
10 | iconPlus
11 | span New
12 |
13 | .Column-body
14 |
15 | .active-tasks
16 |
17 | .empty-state(
18 | v-if="!hasActiveTasks")
19 | | Yapacak hiç bir işin yok mu yani?
20 |
21 | Draggable(
22 | v-if="hasActiveTasks"
23 | v-model="tasks"
24 | ref="activeTasks"
25 | @start="onDragStart"
26 | @end="onDragEnd"
27 | :options="{ animation: 120, handle: '.handle' }")
28 |
29 | task(
30 | v-for="task in tasks"
31 | :key="task.id"
32 | v-if="!task.status"
33 | :class="{ 'completed' : task.status }"
34 | :task="task")
35 |
36 | .completed-tasks(
37 | v-if="hasCompletedTasks")
38 |
39 | task(
40 | v-for="task in tasks"
41 | :key="task.id"
42 | v-if="task.status"
43 | :class="{ 'completed' : task.status }"
44 | :task="task")
45 |
46 |
47 |
108 |
109 |
171 |
--------------------------------------------------------------------------------
/src/components/task/task.vue:
--------------------------------------------------------------------------------
1 |
2 | .task
3 |
4 | .handle
5 | icon-drag
6 |
7 | // checkbox
8 | label.task-checkbox(
9 | :class="{ 'check' : status }")
10 | icon-check
11 | input(
12 | type="checkbox"
13 | v-model="status")
14 |
15 | .task-text
16 |
17 | // edit
18 | autosize(
19 | ref="autosize"
20 | rows="1"
21 | placeholder="New task..."
22 | @blur="blur"
23 | @enter="blur"
24 | @esc="blur"
25 | v-if="isEdit"
26 | v-model="text")
27 |
28 | // normal
29 | span.text(
30 | v-if="!isEdit"
31 | @click="isEdit = true")
32 | | {{ text }}
33 | button.remove(
34 | type="button"
35 | v-if="!isEdit && !removing"
36 | @click.stop="timerRemove")
37 | iconRemove
38 | button.stop(
39 | type="button"
40 | v-if="removing"
41 | @click.stop="stop")
42 | span {{ countdown }}
43 |
44 |
45 |
46 |
152 |
153 |
273 |
--------------------------------------------------------------------------------
/src/icons/check.vue:
--------------------------------------------------------------------------------
1 |
2 | svg(
3 | class="icon"
4 | width="22"
5 | height="22"
6 | viewBox="0 0 22 22"
7 | xmlns="http://www.w3.org/2000/svg")
8 | path(
9 | stroke="currentColor"
10 | stroke-width="2"
11 | d="M16 6L9.743 17 6 12.09"
12 | fill="none"
13 | fill-rule="evenodd"
14 | stroke-linecap="round"
15 | stroke-linejoin="round")
16 |
17 |
18 |
23 |
--------------------------------------------------------------------------------
/src/icons/drag.vue:
--------------------------------------------------------------------------------
1 |
2 | svg(
3 | class="icon"
4 | width="20"
5 | height="9"
6 | viewBox="0 0 20 9"
7 | xmlns="http://www.w3.org/2000/svg")
8 | g(
9 | fill="currentColor"
10 | fill-rule="evenodd")
11 | path(d="M0 0h20v1H0zM0 4h20v1H0zM0 8h20v1H0z")
12 |
13 |
14 |
19 |
--------------------------------------------------------------------------------
/src/icons/plus.vue:
--------------------------------------------------------------------------------
1 |
2 |
14 |
15 |
16 |
21 |
--------------------------------------------------------------------------------
/src/icons/refresh.vue:
--------------------------------------------------------------------------------
1 |
2 |
16 |
17 |
18 |
23 |
--------------------------------------------------------------------------------
/src/icons/remove.vue:
--------------------------------------------------------------------------------
1 |
2 | svg(
3 | class="icon"
4 | xmlns="http://www.w3.org/2000/svg"
5 | width="24"
6 | height="24"
7 | viewBox="0 0 24 24"
8 | fill="none"
9 | stroke="currentColor"
10 | stroke-width="2"
11 | stroke-linecap="round"
12 | stroke-linejoin="round")
13 | polyline(points="3 6 5 6 21 6")
14 | path(d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2")
15 | line(x1="10", y1="11", x2="10", y2="17")
16 | line(x1="14", y1="11", x2="14", y2="17")
17 |
18 |
19 |
24 |
--------------------------------------------------------------------------------
/src/main.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import App from './App'
3 | import store from './store'
4 | import moment from 'moment'
5 | import 'moment/locale/tr'
6 |
7 | import Draggable from 'vuedraggable'
8 | import VueQuillEditor from 'vue-quill-editor'
9 |
10 | Vue.component('Draggable', Draggable)
11 | Vue.use(VueQuillEditor, {})
12 |
13 | Vue.prototype.$moment = moment
14 | Vue.prototype.$isDev = process.env.NODE_ENV === 'development'
15 | Vue.config.productionTip = false
16 |
17 | new Vue({
18 | store,
19 | render: h => h(App)
20 | }).$mount('#app')
21 |
22 | if (store.getters.hasToken) {
23 | store.commit('setToken', store.state.token)
24 | // if (store.getters.calendarDataExpire) {
25 | // store.dispatch('getEventList')
26 | // }
27 | }
28 |
29 | // if (store.state.notification === 'default') {
30 | // Notification.requestPermission(function (permission) {
31 | // store.commit('changeNotification', permission)
32 | // })
33 | // }
34 |
35 | if (Vue.prototype.$isDev) {
36 | store.commit('calSetDummyData')
37 | }
38 |
39 | // DEV
40 | if (localStorage.getItem('vuex')) {
41 | store.commit('moveOldData')
42 | }
43 |
--------------------------------------------------------------------------------
/src/model/index.js:
--------------------------------------------------------------------------------
1 | export class Note {
2 | constructor (data = {}) {
3 | this.id = data.id || new Date().getTime()
4 | this.content = data.content || ''
5 | }
6 | }
7 |
8 | export class Task {
9 | constructor (data = {}) {
10 | this.id = data.id || new Date().getTime()
11 | this.status = data.text || false
12 | this.text = data.text || ''
13 | }
14 |
15 | get isEmpty () {
16 | return this.text.trim().length === 0
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/store/dummy-data.js:
--------------------------------------------------------------------------------
1 | export const dummyCalendarList = () => {
2 | return [
3 | {
4 | 'accessRole': 'owner',
5 | 'backgroundColor': '#9fc6e7',
6 | 'defaultReminders': [],
7 | 'etag': '"435465ygfdfgdf"',
8 | 'foregroundColor': '#000000',
9 | 'id': 'gdfdssfdgd544@group.calendar.google.com',
10 | 'kind': 'calendar#calendarListEntry',
11 | 'location': 'ISTANBUL',
12 | 'summary': 'Genel',
13 | 'timeZone': 'Europe/Istanbul'
14 | },
15 | {
16 | 'accessRole': 'owner',
17 | 'backgroundColor': '#b3dc6c',
18 | 'defaultReminders': [{ 'method': 'popup', 'minutes': 30 }],
19 | 'etag': '"sdgfhggdf45"',
20 | 'foregroundColor': '#000000',
21 | 'id': '35efagsa@gmail.com',
22 | 'kind': 'calendar#calendarListEntry',
23 | 'location': 'ISTANBUL',
24 | 'selected': true,
25 | 'summary': 'Özel',
26 | 'timeZone': 'Europe/Istanbul'
27 | },
28 | {
29 | 'accessRole': 'reader',
30 | 'backgroundColor': '#16a765',
31 | 'defaultReminders': [],
32 | 'etag': '"1537106192955000"',
33 | 'foregroundColor': '#000000',
34 | 'id': 'tr.turkish#holiday@group.v.calendar.google.com',
35 | 'kind': 'calendar#calendarListEntry',
36 | 'summary': 'Türkiye\'deki Tatiller',
37 | 'timeZone': 'Europe/Istanbul'
38 | },
39 | {
40 | 'accessRole': 'reader',
41 | 'backgroundColor': '#fbe983',
42 | 'defaultReminders': [],
43 | 'etag': '"1537106200237000"',
44 | 'foregroundColor': '#000000',
45 | 'id': 'ht3jlfaac5lfd6263ulfh4tql8@group.calendar.google.com',
46 | 'kind': 'calendar#calendarListEntry',
47 | 'summary': 'Phases of the Moon',
48 | 'timeZone': 'Europe/Istanbul'
49 | }
50 | ]
51 | }
52 | export const dummyCalendarItems = () => {
53 | return [
54 | {
55 | 'attendees': [
56 | {
57 | 'email': 'ademilter@gmail.com',
58 | 'displayName': 'Adem Ilter',
59 | 'self': true,
60 | 'responseStatus': 'accepted'
61 | }, {
62 | 'email': 'ilyas.teker@gmail.com',
63 | 'displayName': 'ilyas teker',
64 | 'organizer': true,
65 | 'responseStatus': 'accepted'
66 | }
67 | ],
68 | 'created': '2018-09-03T20:52:25.000Z',
69 | 'creator': {
70 | 'email': 'ilyas.teker@gmail.com',
71 | 'displayName': 'ilyas teker'
72 | },
73 | 'description': 'Saati 17:00 yapsak uyar mi abi?',
74 | 'end': { 'dateTime': '2018-09-20T18:00:00+03:00' },
75 | 'etag': '"3074216283874000"',
76 | 'htmlLink': 'https://www.google.com/calendar/event?eid=ZzAwNHV2MzNzbTk4Ymc3NHNlM2NscTRoNGcgYWRlbWlsdGVyQG0',
77 | 'iCalUID': 'g004uv33sm98bg74se3clq4h4g@google.com',
78 | 'id': 'g004uv33sm98bg74se3clq4h4g',
79 | 'kind': 'calendar#event',
80 | 'location': 'Capitol Starbucks',
81 | 'organizer': {
82 | 'email': 'ilyas.teker@gmail.com',
83 | 'displayName': 'ilyas teker'
84 | },
85 | 'reminders': { 'useDefault': true },
86 | 'sequence': 0,
87 | 'start': { 'dateTime': '2018-09-20T17:00:00+03:00' },
88 | 'status': 'confirmed',
89 | 'summary': 'Adem - İlyas Bulusma',
90 | 'updated': '2018-09-16T14:29:01.937Z'
91 | },
92 | {
93 | 'attendees': [
94 | {
95 | 'email': 'ademilter@gmail.com',
96 | 'displayName': 'Adem Ilter',
97 | 'self': true,
98 | 'responseStatus': 'accepted'
99 | }
100 | ],
101 | 'created': '2018-08-21T08:40:53.000Z',
102 | 'creator': {
103 | 'email': 'ademilter@gmail.com',
104 | 'displayName': 'Adem Ilter',
105 | 'self': true
106 | },
107 | 'description': 'Bunun gibi otomatik oluşturulmuş etkinlikler hakkında ayrıntılı bilgi görmek üzere, resmî Google Takvim uygulamasını kullanın. https://g.co/calendar\n\nBu etkinlik, Gmail\'e aldığınız bir e-postan oluşturuldu. https://mail.google.com/mail?extsrc=cal&plid=ACUX6DPFcYfLJYrZOEh7QWpkJORwc7Vr-Jfxirc\n',
108 | 'end': { 'dateTime': '2018-09-23T07:00:00+03:00' },
109 | 'etag': '"3069681706778000"',
110 | 'htmlLink': 'https://www.google.com/calendar/event?eid=XzZ0bG5hcXJsZTVwNmNwYjRkaG1qNHBocGVocW40ZDlwNmRrbjhyOWpjNHBqMnFiOWUxbDM0c3BsY29vM2dxajRkdHI2b29yNjY5cDMwbzlwZWRvbWdvajljcGs3MGNqNGN0aTNhZGoyY2hrbWUgYWRlbWlsdGVyQG0',
111 | 'iCalUID': '7kukuqrfedlm2f9tur593itm3a31iipj2s5f08jdovlcf2r0a9sqhbifhp2dgd56bdig',
112 | 'id': '_6tlnaqrle5p6cpb4dhmj4phpehqn4d9p6dkn8r9jc4pj2qb9e1l34splcoo3gqj4dtr6oor669p30o9pedomgoj9cpk70cj4cti3adj2chkme',
113 | 'kind': 'calendar#event',
114 | 'location': 'İstanbul IST',
115 | 'organizer': { 'email': 'unknownorganizer@calendar.google.com' },
116 | 'reminders': { 'useDefault': false },
117 | 'sequence': 0,
118 | 'start': { 'dateTime': '2018-09-23T05:15:00+03:00' },
119 | 'status': 'confirmed',
120 | 'summary': 'Uçuş hedefi: Elazığ (TK 2642)',
121 | 'updated': '2018-08-21T08:40:53.484Z'
122 | },
123 | {
124 | 'created': '2008-11-18T00:00:00.000Z',
125 | 'creator': {
126 | 'email': 'ht3jlfaac5lfd6263ulfh4tql8@group.calendar.google.com',
127 | 'displayName': 'Phases of the Moon',
128 | 'self': true
129 | },
130 | 'end': { 'date': '2018-09-18' },
131 | 'etag': '"2453932800000000"',
132 | 'htmlLink': 'https://www.google.com/calendar/event?eid=bW9vbnBoYXNlKzE1MzcxMzk3MDAwMDAgaHQzamxmYWFjNWxmZDYyNjN1bGZoNHRxbDhAZw',
133 | 'iCalUID': 'moonphase+1537139700000@google.com',
134 | 'id': 'moonphase+1537139700000',
135 | 'kind': 'calendar#event',
136 | 'organizer': {
137 | 'email': 'ht3jlfaac5lfd6263ulfh4tql8@group.calendar.google.com',
138 | 'displayName': 'Phases of the Moon',
139 | 'self': true
140 | },
141 | 'sequence': 0,
142 | 'start': { 'date': '2018-09-17' },
143 | 'status': 'confirmed',
144 | 'summary': 'İlk dördün 02:15',
145 | 'updated': '2008-11-18T00:00:00.000Z'
146 | },
147 | {
148 | 'created': '2008-11-18T00:00:00.000Z',
149 | 'creator': {
150 | 'email': 'ht3jlfaac5lfd6263ulfh4tql8@group.calendar.google.com',
151 | 'displayName': 'Phases of the Moon',
152 | 'self': true
153 | },
154 | 'end': { 'date': '2018-09-26' },
155 | 'etag': '"2453932800000000"',
156 | 'htmlLink': 'https://www.google.com/calendar/event?eid=bW9vbnBoYXNlKzE1Mzc4NDM5ODAwMDAgaHQzamxmYWFjNWxmZDYyNjN1bGZoNHRxbDhAZw',
157 | 'iCalUID': 'moonphase+1537843980000@google.com',
158 | 'id': 'moonphase+1537843980000',
159 | 'kind': 'calendar#event',
160 | 'organizer': {
161 | 'email': 'ht3jlfaac5lfd6263ulfh4tql8@group.calendar.google.com',
162 | 'displayName': 'Phases of the Moon',
163 | 'self': true
164 | },
165 | 'sequence': 0,
166 | 'start': { 'date': '2018-09-25' },
167 | 'status': 'confirmed',
168 | 'summary': 'Dolunay 05:53',
169 | 'updated': '2008-11-18T00:00:00.000Z'
170 | },
171 | {
172 | 'created': '2008-11-18T00:00:00.000Z',
173 | 'creator': {
174 | 'email': 'ht3jlfaac5lfd6263ulfh4tql8@group.calendar.google.com',
175 | 'displayName': 'Phases of the Moon',
176 | 'self': true
177 | },
178 | 'end': { 'date': '2018-09-20' },
179 | 'etag': '"2453932800000000"',
180 | 'htmlLink': 'https://www.google.com/calendar/event?eid=bW9vbnBoYXNlKzE1Mzc4NDM5ODAwMDAgaHQzamxmYWFjNWxmZDYyNjN1bGZoNHRxbDhAZw',
181 | 'iCalUID': 'moonphase+1537843980000@google.com',
182 | 'id': 'moonphase+1537843980000',
183 | 'kind': 'calendar#event',
184 | 'organizer': {
185 | 'email': 'ht3jlfaac5lfd6263ulfh4tql8@group.calendar.google.com',
186 | 'displayName': 'Phases of the Moon',
187 | 'self': true
188 | },
189 | 'sequence': 0,
190 | 'start': { 'date': '2018-09-20' },
191 | 'status': 'confirmed',
192 | 'summary': 'Dolunay 05:53',
193 | 'updated': '2008-11-18T00:00:00.000Z'
194 | }
195 | ]
196 | }
197 |
--------------------------------------------------------------------------------
/src/store/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import Vuex from 'vuex'
3 | import createPersistedState from 'vuex-persistedstate'
4 | import httpCalendar from '../utils/http-calendar'
5 | import moment from 'moment'
6 | import { Note, Task } from '../model'
7 | import { dummyCalendarList, dummyCalendarItems } from './dummy-data'
8 |
9 | Vue.use(Vuex)
10 |
11 | export default new Vuex.Store({
12 |
13 | plugins: [createPersistedState({
14 | key: 'FrameExtension'
15 | })],
16 |
17 | state: {
18 | // data
19 | notes: [],
20 | tasks: [],
21 | calendarList: [],
22 | calendarItems: [],
23 | // config
24 | token: null,
25 | notification: 'default',
26 | calendarLastUpdate: moment().startOf('hour')
27 | },
28 |
29 | getters: {
30 | hasToken: state => state.token !== null,
31 | calendarDataExpire: state => {
32 | return moment().startOf('hour').format('H') - moment(state.calendarLastUpdate).format('H') > 0
33 | },
34 | allowNotification: state => state.notification === 'granted',
35 | hasNote: state => !!state.notes.length,
36 | hasActiveTasks: state => state.tasks.some(o => !o.status),
37 | hasCompletedTasks: state => state.tasks.some(o => o.status),
38 | hasCalenderItem: state => !!state.calendarItems.length,
39 | eventsSortAndGroupBy: state => {
40 | const sortDates = {}
41 | // groupBy
42 | const groupBy = state.calendarItems.reduce((acc, obj) => {
43 | const key = moment(obj.start.date || obj.start.dateTime).format('YYYY-MM-DD');
44 | (acc[key] || (acc[key] = [])).push(obj)
45 | return acc
46 | }, {})
47 | // sort by timestamp and move new object
48 | Object.keys(groupBy).sort((a, b) => {
49 | return moment(a).format('X') - moment(b).format('X')
50 | }).forEach(key => {
51 | sortDates[key] = groupBy[key]
52 | })
53 | return sortDates
54 | }
55 | },
56 |
57 | actions: {
58 |
59 | afterLogin ({ commit, dispatch }, token) {
60 | commit('changeToken', token)
61 | commit('setToken', token)
62 | dispatch('getEventList')
63 | },
64 |
65 | async getCalendarList ({ commit }) {
66 | const response = await httpCalendar.get('users/me/calendarList')
67 | commit('setCalendarList', response.data)
68 | },
69 |
70 | async getEventList ({ state, getters, dispatch, commit }) {
71 | await dispatch('getCalendarList')
72 |
73 | const fromDate = moment()
74 | const toDate = moment().add(14, 'days')
75 |
76 | const requestList = state.calendarList.map(cal => {
77 | const url = [
78 | 'calendars/',
79 | encodeURIComponent(cal.id),
80 | '/events',
81 | '?timeMin=' + encodeURIComponent(fromDate.toISOString()),
82 | '&timeMax=' + encodeURIComponent(toDate.toISOString()),
83 | '&maxResults=50',
84 | '&orderBy=startTime',
85 | '&singleEvents=true'
86 | ].join('')
87 | return httpCalendar.get(url)
88 | })
89 |
90 | const response = await Promise.all(requestList)
91 | commit('setCalendarItems', response)
92 | commit('updateCalendarLastUpdate')
93 | }
94 |
95 | },
96 |
97 | mutations: {
98 |
99 | changeToken (state, token) {
100 | state.token = token
101 | },
102 |
103 | setToken (state, token) {
104 | httpCalendar.defaults.headers.common.Authorization = `Bearer ${token}`
105 | },
106 |
107 | changeNotification (state, status) {
108 | state.notification = status
109 | },
110 |
111 | updateCalendarLastUpdate (state) {
112 | state.calendarLastUpdate = moment().startOf('hour')
113 | },
114 |
115 | moveOldData (state) {
116 | const oldData = JSON.parse(localStorage.getItem('vuex'))
117 | localStorage.removeItem('vuex')
118 | state.notes = oldData.notes
119 | state.tasks = oldData.tasks
120 | },
121 |
122 | // NOTE
123 |
124 | addNote (state) {
125 | state.notes.unshift(new Note())
126 | },
127 | changeNote (state, { note, content }) {
128 | note.content = content
129 | },
130 | updateNotes (state, newList) {
131 | state.notes = newList
132 | },
133 | removeNote (state, note) {
134 | const index = state.notes.indexOf(note)
135 | if (index > -1) {
136 | state.notes.splice(index, 1)
137 | }
138 | },
139 |
140 | // TASK
141 |
142 | updateTasks (state, newList) {
143 | state.tasks = newList
144 | },
145 | addTask (state) {
146 | state.tasks.unshift(new Task())
147 | },
148 | changeTextTask (state, { task, text }) {
149 | task.text = text
150 | },
151 | changeStatusTask (state, { task, status }) {
152 | task.status = status
153 | },
154 | removeTasks (state, task) {
155 | const index = state.tasks.indexOf(task)
156 | if (index > -1) {
157 | state.tasks.splice(index, 1)
158 | }
159 | },
160 |
161 | // CALENDAR
162 |
163 | calSetDummyData (state) {
164 | state.calendarList = dummyCalendarList()
165 | state.calendarItems = dummyCalendarItems()
166 | },
167 |
168 | setCalendarList (state, data) {
169 | state.calendarList = data.items
170 | },
171 |
172 | setCalendarItems (state, items) {
173 | const data = items.map(item => item.data)
174 | const events = data.map(calendar => calendar.items)
175 | state.calendarItems = events.flat()
176 | }
177 | }
178 |
179 | })
180 |
--------------------------------------------------------------------------------
/src/utils/get-token.js:
--------------------------------------------------------------------------------
1 | /* global chrome */
2 | import store from '../store'
3 |
4 | const authUrl = () => {
5 | const manifest = chrome.runtime.getManifest()
6 | const redirectUrl = chrome.identity.getRedirectURL()
7 | console.info('redirect url', redirectUrl)
8 | return [
9 | 'https://accounts.google.com/o/oauth2/auth',
10 | '?client_id=' + manifest.oauth2.client_id_firefox,
11 | '&response_type=token',
12 | '&redirect_uri=' + encodeURIComponent(redirectUrl),
13 | '&scope=' + manifest.oauth2.scopes.join(',')
14 | ].join('')
15 | }
16 |
17 | const extractAccessToken = redirectUri => {
18 | const m = redirectUri.match(/[#?](.*)/)
19 | if (!m || m.length < 1) {
20 | return null
21 | }
22 | const params = new URLSearchParams(m[1].split('#')[0])
23 | return params.get('access_token')
24 | }
25 |
26 | const getAuthToken = () => {
27 | if (process.env.VUE_APP_PLATFORM === 'firefox') {
28 | chrome.identity.launchWebAuthFlow({
29 | interactive: true,
30 | url: authUrl()
31 | }, async redirectUri => {
32 | const token = extractAccessToken(redirectUri)
33 | store.dispatch('afterLogin', token)
34 | })
35 | } else {
36 | chrome.identity.getAuthToken({
37 | interactive: true
38 | }, async function (token) {
39 | store.dispatch('afterLogin', token)
40 | })
41 | }
42 | }
43 |
44 | export default getAuthToken
45 |
--------------------------------------------------------------------------------
/src/utils/http-calendar.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 | import store from '../store'
3 |
4 | const httpCalendar = axios.create({
5 | baseURL: 'https://www.googleapis.com/calendar/v3/'
6 | })
7 |
8 | // Setting up an interceptor in case of a token
9 | // expiration or unsuccessful authentication.
10 |
11 | httpCalendar.interceptors.response.use(null, error => {
12 | if (error.response.status === 401) {
13 | console.error('token expired')
14 | if (store.getters.hasToken) {
15 | store.commit('changeToken', null)
16 | }
17 | }
18 | return Promise.reject(error)
19 | })
20 |
21 | export default httpCalendar
22 |
--------------------------------------------------------------------------------
/vue.config.js:
--------------------------------------------------------------------------------
1 | const MomentLocalesPlugin = require('moment-locales-webpack-plugin')
2 |
3 | module.exports = {
4 | configureWebpack: {
5 | plugins: [
6 | new MomentLocalesPlugin({
7 | localesToKeep: ['tr']
8 | })
9 | ]
10 | }
11 | }
12 |
--------------------------------------------------------------------------------